了解如何通过 CGO 在 Go 中使用 C 库
因此让我们使用 C 库!
你可能听说过 CGO,并且知道 Go 可以使用系统中的共享库来发挥 C 语言的强大功能。但是具体怎么做呢?我会解释这个过程,你会发现它其实并不复杂——事实上,它相当简单。
本教程是为初学者准备的,但您需要具备一些 C 编程知识。
我写这篇教程的原因很简单,因为我觉得官方文档以及我读过的教程都有些过于简陋。就我而言,我需要从一个非常简单的情况开始,一个真实的“Hello World”,然后慢慢过渡到使用共享库。所以,这就是我的愿景,我看待事物的方式,我在这里分享给大家。
什么是共享库?
共享库(在 Windows 上也称为动态链接库 (DLL),在 Linux 等基于 Unix 的系统上也称为共享对象 (SO))是包含可同时供多个程序使用的编译代码的文件。这些代码存储在共享库中,无需在每个需要执行该任务的程序中重复执行该任务的代码。这样,程序就可以使用这些库中的函数和过程,而无需将代码直接包含在文件中。
Go 可以利用这一点。当然,就像 Python 或 Rust 一样。
CGO 是啥?
它是一个允许在 Go 代码中调用 C 函数和使用 C 库的工具。它充当 Go 和 C 语言之间的桥梁,使 Go 程序能够整合现有的 C 库并利用现有的 C 代码库。
实际上,它可以这样做:
- 将 Go 的功能传递给 C,但这不是我们今天将要看到的
- 直接从语句上方的“注释”调用 C 函数
import "C"
。或者从.c
项目内部的文件中调用。 - 使用 C 库来使用共享库中已编译的函数和类型。
CGO 随 Go 提供,但您需要一个 C 编译器,例如 Ming 上的 GCC(适用于 Windows)。
CGO 将使用“C”包,其中所有 C 函数和变量均可访问。然后,它将使用 C 编译器链接到您的 Go 项目。
让我们尝试在 Go 中使用 C!
首先,为了理解 CGO 的作用,我们将调用我们创建的 C 函数。
创建一个项目,例如在~/Projects/testCGO
此目录中:
go mod init testcgo
然后创建一个main.go
文件并写入以下内容:
package main
// #include <stdio.h>
// void hello() {
// printf("Hello from C")
//}
import "C"
func main(){
// let's call it
C.hello()
}
在终端中输入go run .
并查看结果。没错,它显示“Hello from C”。
但是,这里的 CGO 是什么?
实际上,CGO 已经使用了语句上方的注释import "C"
,并且共享了“C”包中的函数。
这意味着
hello()
用 C 开发的函数可以像C.hello()
在 Go 中一样访问。“C”包就像一个命名空间,可以在其中访问 C 变量和函数。
但是,我们可以做得更漂亮一些。如果 C 代码行数不多,注释会很有用,但当项目变得庞大时,注释很快就会变得有点烦人。所以,让我们使用真正的 C 源文件。
在同一目录中,创建hello.c
文件:
#include <stdio.h>
void hello() {
printf("Hello from C in another file")
}
并且,hello.h
声明该函数的文件:
void hello();
然后,在您的main.go
文件中,替换内容以获得以下内容:
package main
// #include "hello.h"
import "C"
func main(){
// let's call it
C.hello()
}
这样,我们就可以使用include
C 包含语法了。CGO 不会有问题:
go run main.go
Hello from C in another file
再一次,CGO 接受了上面的注释import "C"
,因为该#include
语句是一个有效的 C 调用,所以它毫无问题地编译了 C 文件。
我们刚刚了解了基础。一边是 C 代码,另一边是 Go 项目,现在我们知道该如何连接两者了。当然,接下来还会有更多限制性的东西需要管理。
如何使用 C 类型?
让我们更改hello.c
文件以接受参数并向某人打招呼。
#include <stdio.h>
// say hello to the name
void hello(char* name) {
printf("Hello %s\n", name);
}
当然,更改头文件以声明该函数:
void hello(char*);
然后,将main.go
文件更改为现在尝试向约翰打招呼:
package main
// #include "hello.h"
import "C"
func main() {
C.hello("John")
}
这将失败...
./main.go:7:10: cannot use "John" (untyped string constant) as *_Ctype_char value in argument to (_Cfunc_hello)
记住这一点非常重要,我们需要将变量从 Go 转换为 C。
Go 简化了多种类型的操作,例如数组、指针和字符串。当你需要从 C 语言发送或接收变量时,它们的类型并不完全相同。因此,我们需要“强制转换”类型。不过,不用担心,过一段时间你就会自然而然地做到。
那么,如何解决这个问题?
我们需要将 Go 修改string
为char*
类型。我们可以使用C.char
类型,但需要手动分配内存。相反,有一种C.CString
类型更容易使用。
func main() {
name := C.CString("Gopher")
C.hello(name)
}
现在,它起作用了!
这是使用 CGO 的“复杂”之处,你需要转换、强制类型转换以及操作变量类型以确保其正常工作。
而且由于它是 C 语言,C 变量没有垃圾回收器,所以你需要在需要时释放内存(使用C.free()
)。
因此让我们使用 C 库!
现在我们已经了解了 CGO 如何编译 C 代码,让我们尝试告诉它链接共享库。
为了举例,我将使用非常简单的“libuuid”。
您需要安装该库的 devel 包才能获取头文件。在 Fedora 上,这是一个简单的sudo dnf install libuuid-devel
命令行。
为了能够生成 UUID,您需要阅读库的文档(是的...RTFM...) - 当然,我已经这样做了,我可以解释如何生成 UUID。
// In C:
// we need a uuid_t variable to initalize
uuid uuid_t;
// then we generate the random string. I use the random form
// but you can use other generate methods.
uuid_generate_random(uuid);
// To get a uuid string, we need to "unparse"
char uuid_str[37];
uuid_unparse_lower(uuid, uuid_str);
// In the uuid_str char*, there is a uuid
好的,那我们尝试一下。
我们将包含CGO 能够找到的 Linuxuuid/uuid.h
常见文件/usr/include
。然后我们将使用这个头文件中的类型和函数。
package main
// #include <uuid/uuid.h>
import "C"
import "fmt"
func main() {
var uuid C.uuid_t
var uuid_str *C.char
uuid_str = (*C.char)(C.malloc(37))
C.uuid_generate_random(uuid)
C.uuid_unparse(uuid, uuid_str)
fmt.Println(C.GoString(uuid_str))
}
这将失败...
这里的第一个问题是 不起作用typdedef
。我们需要阅读错误信息才能理解它实际上uuid_t
是一个uchar*
。但又不完全是……实际上,阅读头文件你会发现它是一个char[16]
。
记住,在 C 语言中, a
char*
就像 achar[]
(我在这里过于简化了)。
但是,libuuid 声明了uuid_t
的大小为 16 个字符,这意味着,使用指针形式,我们需要用 来分配内存malloc
。
因此让我们将该行改为:
var uuid *C.uchar
uuid = (*C.uchar)(C.malloc(16))
你需要了解一些 C 语言才能开始工作。这里,我做的是一个简单的 C 语言
uuid = (uchar*)malloc(16)
转置,并将其与 Go 中的“C”包进行转换。
我们再跑一次,然后……
/usr/lib/golang/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
/usr/bin/ld: /tmp/go-link-645695464/000001.o: in function `_cgo_b1eb6bfc450b_Cfunc_uuid_generate_random':
/tmp/go-build/cgo-gcc-prolog:49: undefined reference to `uuid_generate_random'
/usr/bin/ld: /tmp/go-link-645695464/000001.o: in function `_cgo_b1eb6bfc450b_Cfunc_uuid_unparse':
/tmp/go-build/cgo-gcc-prolog:62: undefined reference to `uuid_unparse'
collect2: error: ld returned 1 exit status
现在该使用“链接器”了。当然,我们需要告诉编译器使用
libuuid.so
共享库。
理解问题所在。之前,我们使用了用 CGO 编译的 C 源文件。但现在,我们想使用“已编译”的源文件来生成“.so”库(或.dll
用于 Windows)。头文件的作用仅仅是提供函数声明(名称、参数和返回类型)。
这在 C/C++ 中很常见——这使得编译非常智能和快速,因为我们不需要编译库。我们只需“链接”它们。
为了通知 CGO 链接共享库,我们可以向编译器添加特定的标志。因为libuuid
它很简单-luuid
(可以理解-l uuid
)。这将链接libuuid.so
到我们的二进制文件。Go 建议在注释中以#cgo:
语句的形式指定这些参数。
在头文件的包含上面,仅附加一条特殊说明:
// #cgo LDFLAGS: -luuid
// #include <uuid/uuid.h>
import "C"
现在好了
go run .
78137255-35a3-4f61-af7c-e04bf9eb513a
就是这样,您有一个由 C 共享库生成的 UUID。
整个源代码是:
package main
// #cgo LDFLAGS: -luuid
// #include <uuid/uuid.h>
import "C"
import "fmt"
func main() {
var uuid *C.uchar
var uuid_str *C.char
uuid = (*C.uchar)(C.malloc(16))
uuid_str = (*C.char)(C.malloc(37))
C.uuid_generate_random(uuid)
C.uuid_unparse(uuid, uuid_str)
fmt.Println(C.GoString(uuid_str))
}
效果确实不错,但实用吗?不……
让它变得更好
通过动态类型转换来调用 C 函数是不切实际的、没有吸引力的,而且根本不容易维护。
使用 C 语言,函数使用起来更加简单:
uuid_t uuid;
uuid_generate_random(uuid);
char *str = malloc(37); // because 36 chars + \0
uuid_unparse_lower(uuid, str);
// and I can return "str" variable
// that contains a UUID
然后...
我们真正想要的是什么?一个返回 UUID 的函数。所以我们要做一件非常实际的事情:
用 C 编写一个函数,使我们的工作更轻松,并确保我们可以在 Go 程序中访问它。
好的,尝试一下:
package main
// #cgo LDFLAGS: -luuid
//
// #include <uuid/uuid.h>
// #include <stdlib.h>
//
// // create a uuid function in C to return a uuid char*
// char* _go_uuid() {
// uuid_t uuid;
// uuid_generate_random(uuid);
// char *str = malloc(37);
// uuid_unparse_lower(uuid, str);
// return str;
// }
import "C"
import "fmt"
// uuid generates a UUID using the C shared library.
// It returns a Go string.
func uuid() string {
return C.GoString(C._go_uuid())
}
func main() {
// and now it's simple to use
myuuid := uuid() // this is a go string now
fmt.Println(myuuid)
}
当然,我们可以_go_uuid()
在 C 源文件中创建该函数,并创建一个.h
文件来声明我们的函数。然后,包含它go_uuid.h
。
当我们想要将共享库绑定到 Go 时,我们在这里所做的非常常见。我们创建了一些辅助函数来转换类型并调用 C 函数,而无需用户C
自己使用包本身。
这就是https://github.com/go-gst/go-gst、https://github.com/go-gl/glfw,甚至https://fyne.io/如何使用系统库来提出许多功能。
提醒
因此,当您想要使用共享库时需要记住以下几点:
- “C”包可以访问 C 函数、类型和变量
- 您可以使用“C”包导入上方的注释来包含头文件”
- 您可以使用注释中的语句向编译器
LDFLAGS
提供CFLAGS
#cgo
- 你经常需要将类型转换为 Go 类型,或者将 Go 类型转换为 C
- 您可以在 C 语言中创建注释帮助程序,以简化库的使用
结论
C 并非唯一允许生成共享库的语言。当然,你也可以用 Go、Rust、Python 等语言生成它们。但迄今为止,C 语言仍然是生成库最广泛的语言。
可以使用 C 或从共享库调用 C 函数,从而实现 Go 的多种强大功能。
显然,我们更倾向于完全用 Go 开发的库。这样可以避免对用户必须在系统上安装或与用户共享库(以.so
或 的形式.dll
)的依赖。由于 Go 通常使用静态编译,因此强制使用静态编译有点“可惜”。但这在实践中非常有用。例如,功能强大的 Gstreamer 库完全用 Go 重新创建会非常复杂。它是用 C 语言创建的,并且在许多平台上运行良好。因此,依赖这个库是将音频和视频流转换为 Go 语言的绝佳解决方案。
你需要一些 C 语言知识才能在 Go 中链接库。但你不必是这方面的专家。你只需要找到正确的变量类型转换,并适时创建一个辅助函数即可。
无论如何,我希望我的文章能为您开辟道路,消除一些误解,并且您能够在 Go 中使用共享库!
鏂囩珷鏉ユ簮锛�https://dev.to/metal3d/understand-how-to-use-c-libraries-in-go-with-cgo-3dbn