cgo调用 c 动态库

1,899 阅读2分钟

cgo是go和c的桥梁,它的功能十分的强大,可以在go中调用c的函数,也可以在c中调用go写的函数。

通过自己的调研,使用go调用c动态库,有2种方案可以实现

  1. 把编译好的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

image.png

使用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文件,而无需重启服务