在 LuaJIT 中调用 Go 函数

今天同事在公司群里转发了一篇文章:
Calling Go Functions from Other Languages

其原理是

  1. 通过编译时指定 -buildmode=c-shared 选项,把 Go 程序编译成 C 的动态链接库。

  2. 由其他语言通过 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.sogo 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

  1. 被编译的程序必须是 package main 下面的。

  2. 导出的函数需要加 cgo //export 修饰(见 awesome.go 开头的 import "C" 和函数抬头的 //export)。另外函数签名只能包含基础类型。

前者可以写一个 package main 的入口文件,然后由该文件导入其他模块内的内容,这么做来绕过。
但是后者确实是个坎,毕竟有 cgo 的实现上限制。这意味着,想要随心所欲地导入任意 Go 包是不可能的,至少需要包上一层。
此外,相对于 C 语言,用 Go 编译出的动态链接库做 FFI 的资料比较少,不能保证其中没有坑。

说完坏的一面,是时候补上一个光明的尾巴。至少编写 Lua 所缺少的功能时,除了用 C/C++,我们可以有多一种选择。毕竟 Go 的库不少,而且比起
C/C++,实现业务的难度会小一些。也许在将来,我们可以看到更多这方面的实践。

你可能感兴趣的:(cgo,luajit)