使用Golang编写共享库,其他语言调用 作为取代C语言的一种选择,Golang语言越来越流行,尤其在云原生和容器界很多基础软件都用golang编写。作为基础软件的一环,底层库的开发也是非常重要,基于golang进行底层库开发也成了一种需求。从golang 1.5 版开始,go编译器通过-buildmode标识。被称为Go执行模式构建模式,它可是实现将Go包编译成多种格式,包括Go归档、Go共享库、C归档、C共享库和以及在Golang1.8 中引入的动态插件。 今天我们就实例展示一下如何将Golang包编译成C共享库,然后供其他语言调用。在这种构建模式下,编译器输出一个标准的共享对象二进制文件(.so) 将Go函数公开为C风格的API。本文,将展示如何创建可以从C 、 Python 、 Ruby 、 Node 和 Java调用的Go库。 编写Golang共享库假设我们已经编写了一个Golang库,要把将其提供给其他语言调用。在将代码编译成共享库使用,必须支持: 包必须为main打包。编译器会将包及其所有依赖项构建到单个共享对象二进制文件中。 源必须imort包“C”. 使用//export注释以注释希望其他语言可以访问的函数。 一个空的main必须声明函数。 下面的Golang代码,export Add, Cosine, Sort和Log函数共调用: package mainimport 'C'import ('fmt''math''sort''sync')var count intvar mtx sync.Mutexfunc Add(a, b int) int {return a + b}func Cosine(x float64) float64 {return math.Cos(x)}func Sort(vals []int) {sort.Ints(vals)}func SortPtr(vals *[]int) {Sort(*vals)}func Log(msg string) int {mtx.Lock()defer mtx.Unlock()fmt.Println(msg)count++return count}func LogPtr(msg *string) int {return Log(*msg);}func main() {} 该包使用-buildmode=c-shared选项,可以在构建时创建共享对象二进制文件:
完成后,编译器输出两个文件,一个是C头文件chonghcong.h,另一个为chonghcong.so共享对象文件: -rw-rw-r-- 1 1779 Dec 9 15:50 chongchong.h-rw-rw-r-- 1 3748095 Dec 9 15:50 chongchong.so 请注意,.so 文件大约为3.7Mb,对这么简单的几个函数,相对来说编译的库文件较大,这也是golang的缺点之一,主要因为编译后的库中要嵌入整个Golang运行时(类似于编译单个静态可执行文件)。 头文件头文件定义了映射到Go兼容类型的C类型,其内容如下(部分截图,主要是cgo语法) ... 共享对象文件编译器生成的另一个文件是64位ELF共享对象二进制文件。 我们可以使用 file命令。
使用 nm和 grep命令,可以检查Golang函数是否被成功export。 nm chongchong.so | grep -e 'T Add' -e 'T Cosine' -e 'T Sort' -e 'T Log'00000000000d0db0 T Add00000000000d0e30 T Cosine00000000000d0f30 T Log00000000000d0eb0 T Sort 接下来,将实例展示如何从其他语言调用导出的函数。 C调用有两种方法可以使用共享库从C调用Go函数: 代码可以在编译时与共享库静态绑定,但在运行时动态链接。 Golang函数在运行时动态加载和绑定。 动态链接在这种方法中,我们使用头文件来静态引用共享对象文件中导出的类型和函数。 代码简单干净如下图(部分打印语句省略):
接下来编译代码,指定共享对象库: gcc -o example cc.c ./chongchong.so 报错
根据提示,我们添加-std=gnu99选项再进行编译 gcc -o example cc.c ./chongchong.so -std=gnu99 然后执行程序 ./example ,二进制链接到chongchong.so库,产生下面的输出。
动态加载在这种方法中,C代码使用动态链接加载程序库(libdl.so) 动态加载并绑定到导出的符号。 使用定义在dhfcn.h如dlopen打开库文件,用dlsym查找符号, dlerror检索错误,最后用dlclose关闭共享库文件。 #include <stdlib.h>#include <stdio.h>#include <dlfcn.h>typedef long long go_int;typedef double go_float64;typedef struct{void *arr; go_int len; go_int cap;} go_slice;typedef struct{const char *p; go_int len;} go_str;int main(int argc, char **argv) {void *handle;char *error;handle = dlopen ('./chongchong.so', RTLD_LAZY);if (!handle) {fputs (dlerror(), stderr);exit(1);}go_int (*add)(go_int, go_int) = dlsym(handle, 'Add');if ((error = dlerror()) != NULL) {fputs(error, stderr);exit(1);}go_int sum = (*add)(12, 99);printf('chongchong.Add(12, 99) = %d\n', sum);go_float64 (*cosine)(go_float64) = dlsym(handle, 'Cosine');if ((error = dlerror()) != NULL) {fputs(error, stderr);exit(1);}go_float64 cos = (*cosine)(1.0);printf('chongchong.Cosine(1) = %f\n', cos);void (*sort)(go_slice) = dlsym(handle, 'Sort');if ((error = dlerror()) != NULL) {fputs(error, stderr);exit(1);}go_int data[5] = {44,23,7,66,2};go_slice nums = {data, 5, 5};sort(nums);printf('chongchong.Sort(44,23,7,66,2): ');for (int i = 0; i < 5; i++){printf('%d,', ((go_int *)data)[i]);}printf('\n');go_int (*log)(go_str) = dlsym(handle, 'Log');if ((error = dlerror()) != NULL) {fputs(error, stderr);exit(1);}go_str msg = {'Hello from C!', 13};log(msg);dlclose(handle);} 在上述代码中自定义了C类型:go_int,go_float,go_slice和go_str。用dlsym函数加载函数符号并将它们分配给各自的函数指针。然后编译代码:
执行代码时,C 二进制文件加载并链接到共享库chongchong.so产生以下输出: ./example1chongchong.Add(12, 99) = 111chongchong.Cosine(1) = 0.540302chongchong.Sort(44,23,7,66,2): 2,7,23,44,66,Hello from C! PythonPython中,调用C共享库非常容易,可以使用ctypes库调用导出的Go函数,如下面的代码片段所示。
这 lib变量表示从共享目标文件加载的符号。 Python类 GoString和 GoSlice映射到它们各自的 C 结构类型。 执行 Python 代码时,它会调用共享对象中的 Go 函数,产生以下输出: python cc.pychongchong.Add(12,99) = 111chongchong.Cosine(1) = 0.540302chongchong.Sort(74,4,122,9,12) = [4, 9, 12, 74, 122]Hello Python! rubyRuby中调oG函数和Python也类似,使用FFI gem动态加载和调用导出的Go函数,如下面的代码片段所示。
在Ruby中,FFI::libraryclass被扩展为声明一个加载导出符号的类。GoSlice和GoString类映射到它们各自的C结构。当运行代码时,它会调用导出的Go函数,如下所示: ruby cc.rbchongchong.Add(12,99) = 111chongchong.Cosine(1) = 0.540302chongchong.Sort(74,4,122,9,12) = [4, 9, 12, 74, 122]Hello Ruby! node.jsNode 使用一个名为的外部函数库node-ffi(和其他依赖库)动态加载和调用导出的Go函数,如下面的代码片段所示:
ffi对象管理从共享库加载的符号。节点Sturct对象用于创建GoSlice和 GoString映射到它们各自的C结构。当运行代码时,它会调用导出的Go函数,如下所示: chongchong.Add(12, 99) = 111chongchong.Cosine(1) = 0.5403023058681398chongchong.Sort([12,54,9,423,9] = [ 0, 9, 12, 54, 423 ]Hello Node! Java为在Java中调用导出的Go函数,需要Java Native Access或JNA,,如以下代码片段所示: Java接口Chonghcong从chongchong.so共享库文件加载的符号。类GoSlice和 GoString映射到它们各自的C结构表示。当代码编译运行时,它调用导出的Go函数,如下所示: 用javac编译该代码,记得用-cp 引入jna的jar包
会编译成功Cc.class,然后java执行 java -cp .:jna.jar Ccchongchong.Add(12, 99) = 111chongchong.Cosine(1.0) = 0.5403023058681398chongchong.Sort(53,11,5,2,88) = [2 5 11 53 88 ]Hello Java! 结论本文中 ,我们介绍了如何创建一个Golang的函数共享库,通过将Golang包编译成C风格的共享库,Go程序员可以使用共享对象二进制文件集成,然后可以在各种语言,包括C、Python、Ruby、Node、Java 等轻松地使用。 |
|
来自: 菌心说 > 《编程+、计算机、信息技术》