cgo是go和c的桥梁,它的功能十分的强大,可以在go中调用c的函数,也可以在c中调用go写的函数。
通过自己的调研,使用go调用c动态库,有2种方案可以实现
- 把编译好的so文件放在相应的lib目录中,使用C编译器选项让go程序能找到这个lib库和头文件。
首先我们编写一段简单的c代码,编译成so文件
number.h 代码
int TwoSum(int a,int b);
number.c 代码
#include "number.h"
int TwoSum(int a,int b){
return (a+b);
}
编译
gcc -fPIC -shared -o lib/libnumber.so ./include/number.c
使用go调用动态库
package main
/*
// 头文件的位置,相对于源文件是当前目录,所以是 .,头文件在多个目录时写多个 #cgo CFLAGS: ...
#cgo CFLAGS: -I./include
// 从哪里加载动态库,位置与文件名,-ladd 加载 libadd.so 文件
#cgo LDFLAGS: -L./lib -lnumber -Wl,-rpath,lib
#include "number.h"
*/
import "C"
import "fmt"
func main() {
val := C.TwoSum(1, 2022)
fmt.Printf("TwoSum func value:%d \n", val)
}
第一种方案,有个弊端动态库文件必须在go代码编译的时候,就需要编译,无法做到so文件的动态更新
第二种方案,使用动态dlopen的方式来加载动态库。 dlopen 能把一个动态库加载到内存中,返回一个handler,这个handler可以通过dlsym来加载动态库中的函数符号。代码如下
package main
import "fmt"
import "errors"
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
typedef int (*TwoSum)(int,int);
// wrapper function
int TwoSumCall(void* f, int a,int b) {
return ((TwoSum) f)(a,b);
}
*/
import "C"
func TwoSum(a, b int) (int, error) {
handler := C.dlopen(C.CString("/Users/wangrui/go/src/go-so-arm/dlopen/lib/libnumber.so"), C.RTLD_LAZY)
funcPtr := C.dlsym(handler, C.CString("TwoSum"))
result, err := C.TwoSumCall(funcPtr, C.int(a), C.int(b))
if err != nil {
return 0, errors.New(fmt.Sprintf("TwoSumCall return %v, err: %v", result, err))
}
defer C.dlclose(handler)
return int(result), nil
}
func main() {
v, _ := TwoSum(2, 13)
fmt.Printf("TwoSum func value:%d \n", v)
}
我们可以看到,实际上我们用注释的方式自己写了一个函数TwoSumCall,它的第一个参数是void*f 表示的是函数符号,TwoSum,就是C.dlsym和C.dlopen获取出来的函数符号。
在go代码中,我们实际上是调用了C.TwoSumCall这个函数。使用这种方式加载so,可以实现在不修改方法签名的前提下,动态替换掉so文件,而无需重启服务