Go与C互调
意义与应用场景
我们知道,几乎所有的操作系统都是基于c/c++语言写的,因此这些操作系统提供的api都是c/c++的。而例如Java、Python和Golang这类高级语言(c/c++也是高级语言),只提供了语言内置的系统api,因此,如果我们希望调用c/c++语言实现的系统api,就需要使用高级语言调c/c++库和代码的能力。在Java中,我们有JNI,而在Python中,我们也有ctypes模块和Python扩展。当然,Golang也是可以调用c/c++的,go与c互调的核心部件是cgo。
因此,Go调c/c++的意义是方便地使用go调用c/c++语言实现的一些功能和api,而c/c++调Go的意义也是一样。
例如,我们需要用Go来实现一套更底层的网络协议,由于Go只支持到传输层,因此如果我们需要实现更底层的网络协议,就需要调用系统用c/c++实现的更底层网络api。
Go调c
下面是一个用cgo实现的Hello World程序
import "C"
//#include <stdio.h>
import "C"
func HelloWorld() {
C.puts(C.CString("hello world"))
}
可以看到,我们调用了stdio这个库中的puts函数来向控制台打印一个字符串,并且要将Go类型的字符串使用C.CString函数转成C的字符串,并且stdio.h这个头文件是通过Go的注释来引入的,最重要的一点,我们调用C代码的入口都是C这个包。
当然,我们也可以在注释中定义一个c/c++函数,然后用C包来调用我们定义的函数
/*#include <stdio.h>
void SayHello(const char* str) {
puts(str);
}
*/
import "C"
func HelloWorld() {
C.SayHello(C.CString("hello world"))
}
更一般地,我们在c/c++开发中,通常是先创建一个.h的头文件,在头文件中定义函数签名,然后再创建.c或.cpp文件用c/c++实现函数,用cgo也能实现这一点,最终调用函数时,只需要在注释中include进头文件即可
// say_hello.h
void SayHello2(const char*);
// say_hello.c
#include <stdio.h>
#include "say_hello.h"
void SayHello2(const char* str) {
puts(str);
}
// xxx.go
*/
#include "say_hello.h"
*/
import "C"
func HelloWorld() {
C.SayHello2(C.CString("hello word2"))
}
上面的代码是用c实现了SayHello2这个函数,并且,由于我们在使用SayHello2这个函数时,只引入了头文件,因此我们并不知道这个函数的实现在哪,也不知道其是用什么语言实现的,因此如果我们想用Go来实现c/c++定义的函数呢?
// say_hello3.h
void SayHello3(_GoString_);
// say_hello3.go
//#include "say_hello3.h"
import "C"
import "fmt"
//export SayHello3
func SayHello3(str string) {
fmt.Println(str)
}
// xxx.go
//#include "say_hello3.h"
import "C"
func HelloWorld() {
C.SayHello3("hello world3")
}
上面的代码在say_hello3.h头文件中定义了SayHello3函数,并在say_hello3.go文件中用go实现了这个c函数,最后还是在xxx.go中用注释include了say_hello3.h头文件,用C包调用这个用go实现的c函数。
Go调lib中的c
使用静态库
由于想要在go中调用lib中的c代码,就需要先导入一个c的函数签名,在c/c++编程中一般是把函数签名定义在.h文件中,.h文件以源码方式进行分发。因此我们想要go调用lib中的c,也是先把函数定义在.h文件中,然后在go中和前面一样导入.h文件。但这时是没有.c文件了,而是把.c文件编译成了lib,因此我们还需要指定去哪里找.h文件中定义的函数的具体实现代码。
// say_hello.h
void SayHello2(const char*);
// say_hello.c
#include <stdio.h>
#include "say_hello.h"
void SayHello2(const char* str) {
puts(str);
}
// say_hello.go
//#cgo LDFLAGS: -L${SRCDIR}/libs -lsay_hello
//
//#include "say_hello.h"
import "C"
func HelloWorld() {
C.SayHello2(C.CString("hello world"))
}
上面的代码,我们首先需要编译出一个lib文件,使用如下命令
gcc -c -o say_hello.o say_hello.c
ar rcs libsay_hello.a say_hello.o
这两行命令会生成一个libsay_hello.a的lib文件,我们将这个lib文件放到和say_hello.go同路径的libs目录,这是因为我们在say_hello.go文件的最开始的注释中写了一行cgo的编译指令,表示在链接的时候去当前文件的libs目录下找libsay_hello这个文件
使用动态库
动态链接库和静态链接库最大的区别就是:
静态链接库会把库打进最终生成的生成产物中,因此运行时,找库会在最终生成产物中找,因为好处是方便库的组织和最终产物的发布,而坏处就是最终产物将比较大
动态链接库的不会将库打进最终产物中,而是会在运行时去预定的路径去找相关库文件,跟静态链接库相比,动态链接库假定这些库在系统的多个软件中都会使用到,而这些软件将会使用同一个动态链接库文件,因此可以大大减小这些软件的包大小。因此,动态链接库的好处就是多个软件or进程可以共享同一个动态链接库可以大大减小最终产物的包何种;而坏处就是库管理比较麻烦,还有可能会有多个软件共用的动态链接库版本不一致导致的问题。动态链接库的软件在安装时一般都会将其带有的动态链接库copy到系统的动态链接库查找路径中去。
由于静态链接库和动态链接库只有编译和运行的差异,因此代码是一致的。生成一个动态链接库使用命令gcc -shared -o libsay_hello_shared.so
,go对静态链接库和动态链接库的Load没有区别对待,也只需要一行cgo指令就行#cgo LDFLAGS: -L${SRCDIR}/libs -lsay_hello_shared
这样直接运行go程序会报异常,说找不到so,这就是前面说到的动态链接库在运行时才去跑so所在的文件,而系统默认只会在系统的默认目录和程序的当前目录找到so文件,这时需要我们把so文件copy到执行程序的当前目录。
c调Go
在Java中,由于jvm中c和Java代码的运行内存不在同一块,因此在c调java时,一般是采用类似于反射的机制来执行的。而抛开Java不开,如果我们要实现一个c调用一门高级语言的代码,一般来说只有如下几种方法:
- 将高级语言进行交叉编译,编译成c的静态链接库or动态链接库,这样c就能像调用普通库一样调用高级语言的实现
- 在c代码的运行时再跑一个高级代码的运行时
- 在链接时进行函数替换