今天同事在公司群里转发了一篇文章:
Calling Go Functions from Other Languages
其原理是
通过编译时指定
-buildmode=c-shared
选项,把 Go 程序编译成 C 的动态链接库。由其他语言通过 FFI 的形式,去调用动态链接库的函数。
于是,只要能支持 FFI 的语言,就能调用事先编译到动态链接库里 Go 的函数。
想到 LuaJIT 也支持 FFI,我试着在 LuaJIT 代码里实现对 Go 函数的调用。
原文中的 Go 代码如下:
// awesome.go
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() {}
编译出 awesome.so
:go build -o awesome.so -buildmode=c-shared awesome.go
随同生成的还有一个 awesome.h
头文件。
接下来就是用 ffi 去调用暴露出来的几个 Go 函数:
local ffi = require "ffi"
local awesome = ffi.load("./awesome.so")
FFI 调用需要知道链接库中的符号的类型,这时候 awesome.h
就派上用场了。我们仅需从中复制用得上的那部分声明:
ffi.cdef[[
typedef long long GoInt64;
typedef GoInt64 GoInt;
typedef double GoFloat64;
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);
extern void Sort(GoSlice p0);
extern GoInt Log(GoString p0);
]]
剩下就是用 LuaJIT 的 FFI api,做好类型转换:
print("awesome.Add(12, 99) = ", awesome.Add(12, 99))
print("awesome.Cosine(1) = ", awesome.Cosine(1))
local slice = ffi.new("GoSlice")
local data = {12,54,0,423,9}
slice.data = ffi.cast("void*", ffi.new("GoInt[?]", #data, data))
slice.len = 5;
slice.cap = 5;
awesome.Sort(slice)
local sorted_data = ffi.cast("GoInt*", slice.data)
print("\nAfter sort:")
for i = 0, 4 do
print(sorted_data[i])
end
print()
local go_str = ffi.new("GoString")
local s = "Hello LuaJIT!"
go_str.p = s;
go_str.n = #s;
awesome.Log(go_str);
运行输出如下:
awesome.Add(12, 99) = 111LL
awesome.Cosine(1) = 0.54030230586814
After sort:
0LL
9LL
12LL
54LL
423LL
Hello LuaJIT!
数字后面带 LL 后缀是因为 GoInt
是 long long 类型的~
在欢呼 Go 程序可以为我所用之前,先泼一盆冷水。首先,-buildmode=c-shared
有两点要求:(见go help buildmode
)
被编译的程序必须是 package main 下面的。
导出的函数需要加 cgo //export 修饰(见
awesome.go
开头的import "C"
和函数抬头的//export
)。另外函数签名只能包含基础类型。
前者可以写一个 package main 的入口文件,然后由该文件导入其他模块内的内容,这么做来绕过。
但是后者确实是个坎,毕竟有 cgo 的实现上限制。这意味着,想要随心所欲地导入任意 Go 包是不可能的,至少需要包上一层。
此外,相对于 C 语言,用 Go 编译出的动态链接库做 FFI 的资料比较少,不能保证其中没有坑。
说完坏的一面,是时候补上一个光明的尾巴。至少编写 Lua 所缺少的功能时,除了用 C/C++,我们可以有多一种选择。毕竟 Go 的库不少,而且比起
C/C++,实现业务的难度会小一些。也许在将来,我们可以看到更多这方面的实践。