使用Golang编写共享库,其他语言调用

文章图片1

作为取代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选项,可以在构建时创建共享对象二进制文件:

go build -o chongchong.so -buildmode=c-shared chonghcong.go

完成后,编译器输出两个文件,一个是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语法)

文章图片2

...

共享对象文件

编译器生成的另一个文件是64位ELF共享对象二进制文件。 我们可以使用 file命令。

file chongchong.sochongchong.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not stripped

使用 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
文章图片3

接下来,将实例展示如何从其他语言调用导出的函数。

C调用

有两种方法可以使用共享库从C调用Go函数:

代码可以在编译时与共享库静态绑定,但在运行时动态链接。

Golang函数在运行时动态加载和绑定。

动态链接

在这种方法中,我们使用头文件来静态引用共享对象文件中导出的类型和函数。 代码简单干净如下图(部分打印语句省略):

#include <stdio.h>#include 'chongchong.h'int main() {GoInt a = 12;GoInt b = 99;printf('chongchong.Add(12,99) = %d\n', Add(a, b));printf('chongchong.Cosine(1) = %f\n', (float)(Cosine(1.0)));GoInt data[6] = {77, 12, 5, 99, 28, 23};GoSlice nums = {data, 6, 6};Sort(nums);for (int i = 0; i < 6; i++){printf('%d,', ((GoInt *)nums.data)[i]);}GoString msg = {'Hello from C!', 13};Log(msg);}

接下来编译代码,指定共享对象库:

gcc -o example cc.c ./chongchong.so

报错

c.c: In function 'main’:cc.c:12: error: 'for’ loop initial declarations are only allowed in C99 modecc.c:12: note: use option -std=c99 or -std=gnu99 to compile your code

根据提示,我们添加-std=gnu99选项再进行编译

gcc -o example cc.c ./chongchong.so -std=gnu99

然后执行程序

./example

,二进制链接到chongchong.so库,产生下面的输出。

chongchong.Add(12,99) = 111chongchong.Cosine(1) = 0.540302Hello from C!5,12,23,28,77,99,
动态加载

在这种方法中,C代码使用动态链接加载程序库(libdl.so) 动态加载并绑定到导出的符号。 使用定义在dhfcn.h如dlopen打开库文件,用dlsym查找符号, dlerror检索错误,最后用dlclose关闭共享库文件。

#include #include #include 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函数加载函数符号并将它们分配给各自的函数指针。然后编译代码:

gcc -o example1 cc1.c -ldl -std=gnu99

执行代码时,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!
Python

Python中,调用C共享库非常容易,可以使用ctypes库调用导出的Go函数,如下面的代码片段所示。

from __future__ import print_functionfrom ctypes import *lib = cdll.LoadLibrary('./chongchong.so')lib.Add.argtypes = [c_longlong, c_longlong]lib.Add.restype = c_longlongprint('chongchong.Add(12,99) = %d' % lib.Add(12,99))lib.Cosine.argtypes = [c_double]lib.Cosine.restype = c_doubleprint('chongchong.Cosine(1) = %f' % lib.Cosine(1))class GoSlice(Structure):_fields_ = [('data', POINTER(c_void_p)), ('len', c_longlong), ('cap', c_longlong)]nums = GoSlice((c_void_p * 5)(74, 4, 122, 9, 12), 5, 5)lib.Sort.argtypes = [GoSlice]lib.Sort.restype = Nonelib.Sort(nums)print('chongchong.Sort(74,4,122,9,12) = %s' % [nums.data[i] for i in range(nums.len)])class GoString(Structure):_fields_ = [('p', c_char_p), ('n', c_longlong)]lib.Log.argtypes = [GoString]lib.Log.restype = c_longlongmsg = GoString(b'Hello Python!', 13)

这 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!
ruby

Ruby中调oG函数和Python也类似,使用FFI gem动态加载和调用导出的Go函数,如下面的代码片段所示。

require 'ffi'module Ccextend FFI::Libraryffi_lib './chongchong.so'# define class GoSlice to map to:# C type struct { void *data; GoInt len; GoInt cap; }class GoSlice < FFI::Structlayout :data, :pointer,:len, :long_long,:cap, :long_longendclass GoString < FFI::Structlayout :p, :pointer,:len, :long_longendattach_function :Add, [:long_long, :long_long], :long_longattach_function :Cosine, [:double], :doubleattach_function :Sort, [GoSlice.by_value], :voidattach_function :Log, [GoString.by_value], :intendprint 'chongchong.Add(12, 99) = ', Cc.Add(12, 99), '\n'print 'chongchong.Cosine(1) = ', Cc.Cosine(1), '\n'nums = [92,101,3,44,7]ptr = FFI::MemoryPointer.new :long_long, nums.sizeptr.write_array_of_long_long numsslice = Chongchong::GoSlice.newslice[:data] = ptrslice[:len] = nums.sizeslice[:cap] = nums.sizeCc.Sort(slice)sorted = slice[:data].read_array_of_long_long nums.sizeprint 'chongchong.Sort(', nums, ') = ', sorted, '\n'msg = 'Hello Ruby!'gostr = Cc::GoString.newgostr[:p] = FFI::MemoryPointer.from_string(msg)gostr[:len] = msg.size

在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.js

Node 使用一个名为的外部函数库node-ffi(和其他依赖库)动态加载和调用导出的Go函数,如下面的代码片段所示:

require 'ffi'# Module that represents shared libmodule Ccextend FFI::Libraryffi_lib './chongchong.so'class GoSlice < FFI::Structlayout :data, :pointer,:len, :long_long,:cap, :long_longendclass GoString < FFI::Structlayout :p, :pointer,:len, :long_longendattach_function :Add, [:long_long, :long_long], :long_longattach_function :Cosine, [:double], :doubleattach_function :Sort, [GoSlice.by_value], :voidattach_function :Log, [GoString.by_value], :intendprint 'chongchong.Add(12, 99) = ', Cc.Add(12, 99), '\n'print 'chongchong.Cosine(1) = ', Cc.Cosine(1), '\n'nums = [92,101,3,44,7]ptr = FFI::MemoryPointer.new :long_long, nums.sizeptr.write_array_of_long_long nums…

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,,如以下代码片段所示:

文章图片4

Java接口Chonghcong从chongchong.so共享库文件加载的符号。类GoSlice和 GoString映射到它们各自的C结构表示。当代码编译运行时,它调用导出的Go函数,如下所示:

用javac编译该代码,记得用-cp 引入jna的jar包

javac -cp jna.jar Cc.java

会编译成功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 等轻松地使用。