Lua 调用 C/C++/Rust/Go 代码

初步尝试了一下,遇到一些坑,不过总体还算顺利。

Lua 可以通过 Lua 中 require,dofile,loadfile,dostring,loadstring,loadlib,load 加载各种模块,代码执行。

下面实现的调用都是通过生成 .so 文件,然后给 Lua 使用完成的。不得不说 C 语言及其内存相关已经成为大多数语言 FFI 的事实标准,很多语言都提供了相关的交互功能也有资料可供参考。

本文代码 https://gitee.com/kkbt/lua-libs-demo
示例代码包括 justfile ,包含所有构建,查看信息,运行的指令。使用 just 调用就行了。

其实 Lua 调用其他语言函数,有一个更好的选择就是 LuaJIT 的 FFI ,确实很好用,但是一直听说这个项目前途未卜,FFI 库是和 LuaJIT 高度绑定来着,连带着情况也不明。不过大概 Lua 以及 LuaJIT 等也并非不可替代,只是麻烦一些。Lua 作为嵌入语言,不知道多少是用的官方解释器,还是自己程序框架提供的。个人项目 ObcsapiGo 是用的纯 Go 解释器来着,嵌入程序中拓展功能。如果作为胶水语言或者拓展用途,需要代码量也不高。一些用途选择 JS 也还可以接受,或者使用其他脚本语言都可以。不过说回来 Lua 设计的确实很精巧,模块也多,引入使用方便,也是值得一用的。

顺便说一下,LuaJIT 有一个比较活跃的分支版本是 openresty 来维护的。

https://gitee.com/kkbt/lua-libs-demo/blob/master/src/myclib.c

可能出现问题,lua.h 找不到。Linux 在安装完 Lua5.4 之后,头文件目录为 /usr/include/lua5.4 。所以使用具体路径就可以了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// mylib.c
#include <stdio.h>
#include "lua5.4/lua.h"
#include "lua5.4/lauxlib.h"

static int l_test (lua_State *L) {
    printf("hello world\n");
    return 0;
}

static const struct luaL_Reg alib [] = {
    {"test", l_test},
    {NULL, NULL}
};

// loader函数 myclib 决定 require "myclib" 的命名
int luaopen_myclib(lua_State *L) {
    // 新建一个库(table),把函数加入这个库中,并返回
    luaL_newlib(L, alib);
    return 1;
}

https://gitee.com/kkbt/lua-libs-demo/blob/master/src/testc.lua

1
2
local mylib = require "myclib"
mylib.test()        --> hello world

和 C 一样的,几乎。通过标注为 extern "C" 按 C 语言编译那一部分就行了。

https://gitee.com/kkbt/lua-libs-demo/blob/master/src/mycpplib.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#include <stdio.h>
#include "lua5.4/lua.h"
#include "lua5.4/lauxlib.h"


// 函数必须以C的形式被导出
extern "C"  int hello (lua_State *L) {
    printf("hello world , there is cpp code !");
    return 0;
}

static const struct luaL_Reg alib [] = {
    {"hello", hello},
    {NULL, NULL}
};

// loader函数 myclib 决定 require "myclib" 的命名
// 必须为 luaopen_xxx
extern "C" int luaopen_mycpplib(lua_State *L) {
    // 新建一个库(table),把函数加入这个库中,并返回
    luaL_newlib(L, alib);
    return 1;
}

https://gitee.com/kkbt/lua-libs-demo/blob/master/src/testcpp.lua

1
2
local mylib = require "mycpplib"
mylib.hello()  

使用 mlua ,这是一个 Rust Lua 之间的绑定。

https://gitee.com/kkbt/lua-libs-demo/blob/master/myrslib/Cargo.toml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[package]
name = "myrslib"
version = "0.1.0"
edition = "2021"
authors = ["恐咖兵糖 <0@ftls.xyz>"]

[lib]
crate-type = ["cdylib"]

[dependencies]
mlua = { version = "0.9.1", features = ["lua54", "module"] }

https://gitee.com/kkbt/lua-libs-demo/blob/master/myrslib/src/lib.rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use mlua::prelude::*;

fn hello(_: &Lua, name: String) -> LuaResult<()> {
    println!("hello, {}!", name);
    Ok(())
}

// 定义 Lua 
#[mlua::lua_module]
fn my_rs_module(lua: &Lua) -> LuaResult<LuaTable> {
    let exports = lua.create_table()?;
    exports.set("hello", lua.create_function(hello)?)?;
    Ok(exports)
}

https://gitee.com/kkbt/lua-libs-demo/blob/master/src/testrs.lua

1
2
3
local mod = require("my_rs_module")

print(mod.hello("kkbt"))

Golang 使用 CGO 生成可以被 C 语言使用的 .so ,没找到好用的绑定。类似 mlua 这样的,倒是 Lua 解释器 VM 有一些或者说好几个。可能和 Golang 语言定位有关吧。

下面代码我没调通,这部分应该属于 CGO 的知识。

这个 package.loadlib 还挺难用的,调试了一段额外时间。报错不太详细。然后下面代码字符串传入似乎也有点问题。供参考吧

https://gitee.com/kkbt/lua-libs-demo/blob/master/mygolib/main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

// 一些注释是必须的 是给 cgo 使用的参数
// 使用的是低级功能 导出为 c 可用的函数

// #include <stdlib.h>
import "C"
import (
	"fmt"
	"unsafe"
)

func main() { // 占位
}

//export Hello
func Hello() {
	fmt.Println("Hello")
}

//export SayHello
func SayHello(s *C.char) {
	fmt.Println("GoCode: Run SayHello", C.GoString(s))
}

//export Free
func Free(p unsafe.Pointer) {
	C.free(p)
}

https://gitee.com/kkbt/lua-libs-demo/blob/master/src/testgo.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- Load the shared library
print(package.loadlib("./golib.so", "Hello"))

local hi = package.loadlib("./golib.so", "Hello")
local sayHello = package.loadlib("./golib.so", "SayHello")


if hi then
    local result = hi()
    print("Result:", result)  
else
    print("Failed to load the shared library")
end

sayHello()

如果能使用 lua.h 大概定义一个 luaopen_xxx 接收 lua_State 类型参数的函数就可以使用 local mylib = require "mygolib" 的写法了,经过尝试,发现 CGO 好像不支持 C 的宏,报错 ./main.go:43:2: could not determine kind of name for C.luaL_newlib ,也可能是函数参数类型错误。

所以尝试了 lua_createtable 函数。但是最后 Lua 调用,显示的是 nil ,内存地址不知道指向哪里了,或者是缺少一些步骤。报错 attempt to call a nil value (field 'Hello2')

如果按照 C Lua module 写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

// 一些注释是必须的 是给 cgo 使用的参数
// 使用的是低级功能 导出为 c 可用的函数

//#cgo CFLAGS: -I/usr/include
//#cgo LDFLAGS: -L/lib/x86_64-linux-gnu -llua5.4
//#include "lua5.4/lauxlib.h"
//#include "lua5.4/lua.h"
//#include <stdlib.h>
import "C"
import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() { // 占位
}

//export Hello2
func Hello2(L *C.struct_lua_State) C.int {
	fmt.Println("hello world")
	return 0
}

// ---------- c 代码改写 ----------

//export luaopen_golib2
func luaopen_golib2(L *C.struct_lua_State) C.int {
	// 获取 Hello2 函数的 reflect.Value
	hello2Value := reflect.ValueOf(Hello2)

	// 获取 Hello2 函数的指针
	ptr := unsafe.Pointer(hello2Value.Pointer())

	var myclib_funcs = []C.struct_luaL_Reg{
		{C.CString("Hello2"), (*[0]byte)(ptr)},
		{nil, nil},
	}
	// C.luaL_newlib(L, myclib_funcs)
	// unsafe.Sizeof(myclib_funcs) = 24
	fmt.Println(unsafe.Sizeof(myclib_funcs), unsafe.Sizeof(myclib_funcs[0])) // 24 16 ?
	// C.lua_createtable(L, C.int(0), C.int(unsafe.Sizeof(myclib_funcs)/unsafe.Sizeof(myclib_funcs[0])-1))
	C.lua_createtable(L, C.int(0), C.int(0))

	return 1
}

这个 loader 函数地址八成是错误的,毕竟直接调用的是建 table ,可能缺少 setfunctions 之类的。编译后函数地址也不知道是不是正确的。不过想想感觉 Go 实现起来麻烦了一些,不如写个 Lua 脚本把函数都 package.loadlib 然后导出这样,导入也可以做到 require 这么写。

另外,头文件里 luaL_newlib 是这样的,做了好几个工作来新建 lib ,确实少了好多步

1
2
#define luaL_newlib(L,l)  \
  (luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))

此外,还有 Free 内存的问题。需要考虑的还挺多的

各种语言生成的 .so 都可以用 readelf --dyn-syms libs/myclib.so 查看 entry 。C/Rust 实现了 lua 的 loader ,所以可看到其中一行是 luaopen... 开头的,后面跟的字符串需要和文件名去掉 .so 后一致。

然后 Lua 的 require ,不可以加入路径,否则会查找带路径的 entry 。比如原本 readelf 可以看到

1
94: 000000000000b250    76 FUNC    GLOBAL DEFAULT   12 luaopen_my_rs_module

如果带路径,Lua 会试图找 luaopen_filefolder_my_rs_module ,就找不到了。会报错。就像 Lua 的 require 约定俗成得模块名,文件名必须是一致的,这些林林总总的细节也是需要注意的。