# 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 来维护的。 ## C 语言 https://gitee.com/kkbt/lua-libs-demo/blob/master/src/myclib.c 可能出现问题,lua.h 找不到。Linux 在安装完 Lua5.4 之后,头文件目录为 /usr/include/lua5.4 。所以使用具体路径就可以了。 ```c // mylib.c #include #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 ```lua local mylib = require "myclib" mylib.test() --> hello world ``` ## C++ 语言 和 C 一样的,几乎。通过标注为 `extern "C"` 按 C 语言编译那一部分就行了。 https://gitee.com/kkbt/lua-libs-demo/blob/master/src/mycpplib.cpp ```cpp #include #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 ```lua local mylib = require "mycpplib" mylib.hello() ``` ## Rust 语言 使用 mlua ,这是一个 Rust Lua 之间的绑定。 https://gitee.com/kkbt/lua-libs-demo/blob/master/myrslib/Cargo.toml ```toml [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 ```rust use mlua::prelude::*; fn hello(_: &Lua, name: String) -> LuaResult<()> { println!("hello, {}!", name); Ok(()) } // 定义 Lua #[mlua::lua_module] fn my_rs_module(lua: &Lua) -> LuaResult { 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 ```lua local mod = require("my_rs_module") print(mod.hello("kkbt")) ``` ## Golang 语言 Golang 使用 CGO 生成可以被 C 语言使用的 .so ,没找到好用的绑定。类似 mlua 这样的,倒是 Lua 解释器 VM 有一些或者说好几个。可能和 Golang 语言定位有关吧。 下面代码我没调通,这部分应该属于 CGO 的知识。 这个 `package.loadlib` 还挺难用的,调试了一段额外时间。报错不太详细。然后下面代码字符串传入似乎也有点问题。供参考吧 https://gitee.com/kkbt/lua-libs-demo/blob/master/mygolib/main.go ```go package main // 一些注释是必须的 是给 cgo 使用的参数 // 使用的是低级功能 导出为 c 可用的函数 // #include 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 ```lua -- 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 写法: ```go 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 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 ,确实少了好多步 ```c #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 可以看到 ``` 94: 000000000000b250 76 FUNC GLOBAL DEFAULT 12 luaopen_my_rs_module ``` 如果带路径,Lua 会试图找 `luaopen_filefolder_my_rs_module` ,就找不到了。会报错。就像 Lua 的 `require` 约定俗成得模块名,文件名必须是一致的,这些林林总总的细节也是需要注意的。