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 。所以使用具体路径就可以了。
// 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
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
#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
local mylib = require "mycpplib"
mylib.hello()
Rust 语言
使用 mlua ,这是一个 Rust Lua 之间的绑定。
https://gitee.com/kkbt/lua-libs-demo/blob/master/myrslib/Cargo.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
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
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
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
-- 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 写法:
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 ,确实少了好多步
#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
约定俗成得模块名,文件名必须是一致的,这些林林总总的细节也是需要注意的。
欢迎赞赏~
赞赏