[译]从其他语言调用Go函数


从1.5版本开始,Go编译器通过-buildmode选项引入了对多种构建模式的支持。被称为Go执行模式,这些构建模式扩展了go tool的功能,可以将Go包编译到多种格式,包括Go打包文档,Go共享库,C打包文档,C共享库,以及1.8版本引入的Go动态插件。

本文介绍如何将Go包编译为C共享库。这种构建模式下编译器会输出一个标准模式的共享对象二进制文件(.so),这个文件将Go函数以C语言风格的API对外暴露。同时我们将讨论如何创建可以从C、Python、Ruby、Node和Java调用的Go库。

所有的代码都可以在Github上找到。

Go代码

假设我们已经写了一个非常棒的Go库,我们想让它同样可以被其他语言使用。下面有四点要求是将代码编译为共享库所必须的。

  • 包名必须是main。编译器将会把包代码和所有依赖统一构建到一个单一的共享对象二进制文件。
  • 源代码必须倒入伪包“C”
  • 使用export注释来标记你想要让其他语言访问使用的函数
  • 必须声明一个空main函数
    下面的Go代码导出函数有Add、Cos、Sort和Log。不可否认,这个awesome(令人惊叹的)软件包并没有你看到的那么令人印象深刻。然而,它的各种函数签名将帮助我们探索类型映射的含义。
package main
import "C"
import (
 "fmt"
 "math"
 "sort"
 "sync"
)
var count int
var mtx sync.Mutex
//export Add
func Add(a, b int) int { return a + b }
//export Cosine
func Cosine(x float64) float64 { return math.Cos(x) }
//export Sort
func Sort(vals []int) { sort.Ints(vals) }
//export Log
func Log(msg string) int {
  mtx.Lock()
  defer mtx.Unlock()
  fmt.Println(msg)
  count++
  return count
}
func main() {}

使用-buildmode=c-shared编译选项将其编译为共享对象二进制文件:

go build -o awesome.so -buildmode=c-shared awesome.go

上面的操作完成之后,编译器将会输出两个文件:C语言头文件awesome.h,共享对象二进制文件awesome.so

-rw-rw-r-- 1 tacey tacey 1.7K 10月 26 20:28 awesome.h
-rw-rw-r-- 1 tacey tacey 2.3M 10月 26 20:28 awesome.so

注意,对于如此小的库.so文件就大约有2MB的大小。这是因为整个Go运行时机制和依赖包都被塞进了一个共享对象二进制文件中(类似编译一个单一静态执行文件)

头文件

头文件定义了映射Go兼容类型的C类型(使用CGO语义,这里不讨论)

/* Code generated by cmd/cgo; DO NOT EDIT. */
...
typedef long long GoInt64;
typedef GoInt64 GoInt;
...
typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
...
extern GoInt Add(GoInt p0, GoInt p1);
extern GoFloat64 Cosine(GoFloat64 p0);
...

共享对象文件

编译器生成的其他文件是一个64位ELF共享对象二进制文件(在Linux环境下),我们可以通过file命令来验证它的信息

$> file awesome.so
awesome.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=7074746a2466bbe39ed8350cf34aeff4d4d95b36, not stripped

使用nmgrep命令我们可以检视到我们的go函数已经被暴露:

$> nm awesome.so | grep -e "T Add" -e "T Cosine" -e "T Sort" -e "T Log"
00000000000e9560 T Add
00000000000e95f0 T Cosine
00000000000e96f0 T Log
00000000000e9680 T Sort

下面我们将尝试如何从其他语言调研嗯这些被暴露的函数

从C语言

在C语言中有两种调用共享对象二进制文件中Go函数的方法。一是在编译时源代码静态绑定到一起,但是运行时动态链接。二是Go函数符号在运行时动态加载和绑定。

动态链接

这种方法下我们使用头文件静态引用共享对象文件中的函数和类型。简单示例如下(一些输出语句被忽略了)

#include <stdio.h>
#include "awesome.h"
int main() {
    GoInt a = 12;
    GoInt b = 99;
    printf("awesome.Add(12,99) = %d\n", Add(a, b));
    printf("awesome.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 client client1.c ./awesome.so

当执行是,二进制链接awesome.so库产生下面的输出

$> ./client
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(77,12,5,99,28,23): 5,12,23,28,77,99,
Hello from C!

动态加载

在此方法下,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 ("./awesome.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("awesome.Add(12, 99) = %d\n", sum);

 go_float64 (*cosine)(go_float64) = dlsym(handle, "Cosine");
 go_float64 cos = (*cosine)(1.0);

 void (*sort)(go_slice) = dlsym(handle, "Sort");
 go_int data[5] = {44,23,7,66,2};
 go_slice nums = {data, 5, 5};
 sort(nums);

 go_int (*log)(go_str) = dlsym(handle, "Log");
 go_str msg = {"Hello from C!", 13};
 log(msg);

 dlclose(handle);
}

在这个版本中,代码使用他自己定义的C类型:go_int, go_float, go_slice, 和 go_str (为了说明问题,可以用awesome.h)。函数dlsym加载函数符号,并将它们赋给各自的函数指针。

接下来,可以编译代码,将其与dl库(不是Awesome.so)链接,如下所示:

$> gcc -o client client2.c -ldl

当代码执行时,C二进制加载和连接到共享库awesome.so产生下面的输出。

$> ./client
awesome.Add(12, 99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(44,23,7,66,2): 2,7,23,44,66,
Hello from C!

从Python

在Python中会更简单一些。使用ctype外部函数库像下面这样调用被暴露的函数

from ctypes import *
lib = cdll.LoadLibrary("./awesome.so")
lib.Add.argtypes = [c_longlong, c_longlong]
print "awesome.Add(12,99) = %d" % lib.Add(12,99)
lib.Cosine.argtypes = [c_double]
lib.Cosine.restype = c_double 
cos = lib.Cosine(1)
print "awesome.Cosine(1) = %f" % cos
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 = None
lib.Sort(nums)
class GoString(Structure):
    _fields_ = [("p", c_char_p), ("n", c_longlong)]
lib.Log.argtypes = [GoString]
msg = GoString(b"Hello Python!", 13)
lib.Log(msg)

lib变量表示从共享对象文件中加载的符号。Python类GoStringGoSlice映射对应的C结构体对象。运行Python代码嗲用Go函数产生下面的输出。

$> python client.py
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(74,4,122,9,12) = [ 4 9 12 74 122 ]
Hello Python!

从Java

可以使用Java原生访问项目或者JNA来调用被暴露的Go函数。

import com.sun.jna.*;
public class Client {
  public interface Awesome extends Library {
    public class GoSlice extends Structure {
      ...
      public Pointer data;
      public long len;
      public long cap;
    }
  
    public class GoString extends Structure {
      ...
      public String p;
      public long n;
    }
    public long Add(long a, long b);
    public double Cosine(double val);
    public void Sort(GoSlice.ByValue vals);
    public long Log(GoString.ByValue str);
  }
  static public void main(String argv[]) {
    Awesome awesome = (Awesome) Native.loadLibrary(
      "./awesome.so", Awesome.class);
    System.out.printf(... awesome.Add(12, 99));
    System.out.printf(... awesome.Cosine(1.0));
    long[] nums = new long[]{53,11,5,2,88};
    Memory arr = new Memory(... Native.getNativeSize(Long.TYPE));
    Awesome.GoSlice.ByValue slice = new Awesome.GoSlice.ByValue();
    slice.data = arr;
    slice.len = nums.length;
    slice.cap = nums.length;
    awesome.Sort(slice);
    Awesome.GoString.ByValue str = new Awesome.GoString.ByValue();
    str.p = "Hello Java!";
    str.n = str.p.length();
    System.out.printf(... awesome.Log(str));
  }
}

Java接口Awesome表示从awesome.so加载的符号。类GoSliceGoString映射对应的C结构体。当代码被编译运行,调用被暴露的G哦函数产生下面的输出

$> javac -cp jna.jar Client.java
$> java -cp .:jna.jar Client
awesome.Add(12, 99) = 111
awesome.Cosine(1.0) = 0.5403023058681398
awesome.Sort(53,11,5,2,88) = [2 5 11 53 88 ]
Hello Java!

从Ruby

从Ruby中调用Go函数的方法模式和Python中差不多。只不过Rubu是通过FFI gem来动态加载调用:

require 'ffi'
module Awesome
  extend FFI::Library
  ffi_lib './awesome.so'
  class GoSlice < FFI::Struct
    layout :data,  :pointer,
           :len,   :long_long,
           :cap,   :long_long
  end
  class GoString < FFI::Struct
    layout :p,     :pointer,
           :len,   :long_long
  end
  attach_function :Add, [:long_long, :long_long], :long_long
  attach_function :Cosine, [:double], :double
  attach_function :Sort, [GoSlice.by_value], :void
  attach_function :Log, [GoString.by_value], :int
end

print "awesome.Add(12, 99) = ",  Awesome.Add(12, 99), "\n"
print "awesome.Cosine(1) = ", Awesome.Cosine(1), "\n"
nums = [92,101,3,44,7]
ptr = FFI::MemoryPointer.new :long_long, nums.size
ptr.write_array_of_long_long  nums
slice = Awesome::GoSlice.new
slice[:data] = ptr
slice[:len] = nums.size
slice[:cap] = nums.size
Awesome.Sort(slice)
msg = "Hello Ruby!"
gostr = Awesome::GoString.new
gostr[:p] = FFI::MemoryPointer.from_string(msg)
gostr[:len] = msg.size
Awesome.Log(gostr)

FFI::library被扩展声明加载被暴露的符号。类GoSliceGoString映射对应的C结构体。当执行代码调用Go函数时产生下面的输出

从NodeJS

NodeJS使用被称作node-ffi的外部函数库(以及其他依赖库)来动态加载和调用被暴露的Go函数

var ref = require("ref");
var ffi = require("ffi");
var Struct = require("ref-struct")
var ArrayType = require("ref-array")
var LongArray = ArrayType(ref.types.longlong);
var GoSlice = Struct({
  data: LongArray,
  len:  "longlong",
  cap: "longlong"
});
var GoString = Struct({
  p: "string",
  n: "longlong"
});
var awesome = ffi.Library("./awesome.so", {
  Add: ["longlong", ["longlong", "longlong"]],
  Cosine: ["double", ["double"]],
  Sort: ["void", [GoSlice]],
  Log: ["longlong", [GoString]]
});
console.log("awesome.Add(12, 99) = ", awesome.Add(12, 99));
console.log("awesome.Cosine(1) = ", awesome.Cosine(1));
nums = LongArray([12,54,0,423,9]);
var slice = new GoSlice();
slice["data"] = nums;
slice["len"] = 5;
slice["cap"] = 5;
awesome.Sort(slice);
str = new GoString();
str["p"] = "Hello Node!";
str["n"] = 11;
awesome.Log(str);

ffi对象管理从动态库加载的符号。Struct对象创建GoSliceGoString来映射对应的C结构体。运行代码调用被暴露的Go函数产生下面的输出:

awesome.Add(12, 99) =  111
awesome.Cosine(1) =  0.5403023058681398
awesome.Sort([12,54,9,423,9] =  [ 0, 9, 12, 54, 423 ]
Hello Node!

总结

本文展示了如何创建一个可以从其他语言中使用的Go库。通过将Go包编译成C风格的共享库,Go程序员可以使用共享对象二进制文件的进程内集成,轻松地让他们的项目与C、Python、Ruby、Node、Java等语言一起工作。所以,下次在Go中创建非常棒的功能API时,记得与非Go开发人员共享。

posted @ 2021-10-26 21:42  Tacey Wong  阅读(382)  评论(0编辑  收藏  举报