0%

在Go中运行Lua脚本(四)

将Go对象绑定至lua

通过lua调用go对象的成员函数

与c、c++类似,将原生go对象传入lua,并由lua调用其成员函数。有如下步骤:

1. 注册元表

以gin.Context类为例,将Context类的成员函数,以元表的形似,提前注入lua虚拟机

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
// func (c *Context) GetRawData() ([]byte, error)
func lua_Context_GetRawData(L *lua.State) int {
receiver := (*gin.Context)(*(*unsafe.Pointer)(l.ToUserdata(1)))
result0, result1 := receiver.GetRawData()
L.PushBytes(result0)
if result1 == nil {
L.PushNil()
} else {
L.PushString(result1.Error())
}
return 2
}

// 注册元表
func RegistGoMetaTable(L *lua.State) {
created := l.NewMetaTable("Context")
var funcs = map[string]lua.LuaGoFunction{
"GetRawData" = lua_Context_GetRawData,
}
l.PushString("__index")
l.CreateTable(0, len(funcs))
for fname, f := range funcs {
l.PushGoFunction(f)
l.SetField(-2, fname)
}
l.SetTable(-3)
l.Pop(1)
}

2. 将go对象,传入lua虚拟机,并设置元表

1
2
3
4
5
6
7
8
9
10
11
func Hello(l *Lua.State, ptr *gin.Context) {
// 将Context的结构体指针,通过Userdata的形式,传入lua虚拟机
value := reflect.ValueOf(ptr)
ptrVal := unsafe.Pointer(value.Pointer())
rawptr := l.NewUserdata(unsafe.Sizeof(ptrVal))
*(*unsafe.Pointer)(rawptr) = unsafe.Pointer(ptrVal)

// 并对Userdata设置Context元表
l.LGetMetaTable("Context")
l.SetMetaTable(-2)
}

3. 在lua中调用Context对象的方法

1
2
3
4
5
-- 参数c是Userdata类型,并有Context元表
local function Hello(c)
-- 会执行go中的lua_Context_GetRawData方法
local data, err = c:GetRawData()
end

在lua中还原go继承关系的思考

通过元表,已经可以实现调用go对象的成员函数。

现在以gin.Engine类为例

1
2
3
4
type Engine struct {
RouterGroup
// ...
}

如果想调用Engine.RouterGroup.BasePath方法呢?

  • 一、直接编写lua_Engine_BasePath方法,并写入Engine元表
    1
    2
    3
    4
    5
    6
    // go实现
    func lua_Engine_BasePath(L *lua.State) int {
    receiver := (*gin.Engine)(*(*unsafe.Pointer)(l.ToUserdata(1)))
    result0 := receiver.BasePath()
    //...
    }
    1
    2
    -- lua调用
    local path = engine:BasePath()
  • 二、编写lua_RouterGroup_Handle方法,写入RouterGroup元表,然后让lua对象调用到RouterGroup元表中的方法
    1
    2
    3
    4
    5
    6
    // go实现
    func lua_RouterGroup_Handle(L *lua.State) int {
    receiver := (*gin.RouterGroup)(*(*unsafe.Pointer)(l.ToUserdata(1)))
    result0 := receiver.BasePath()
    //...
    }
    1
    2
    3
    -- lua调用
    local routerGroup = engine.RouterGroup
    local path = routerGroup:BasePath()

方式一更加直接简单;方式二则更贴合go的组合继承逻辑,且代码复用性更好。

考虑到绑定代码,基本都是生成工具生成的,方式二在生成代码时,也更加合理

接下来,就是思考如何实现了

1. 尝试通过元表嵌套

已知:如果元表的index操作符是一张表,那么对index表的索引是走常规的流程,可以触发引发另一次元方法

则可以尝试通过元表的嵌套,来模拟go的继承

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// func (group *RouterGroup) BasePath() string
func lua_RouterGroup_BasePath(L *lua.State) int {
receiver := (*gin.RouterGroup)(*(*unsafe.Pointer)(l.ToUserdata(1)))
result0 := receiver.BasePath()
L.PushString(result0)
return 1
}

// 注册RouterGroup元表
func RegistGoMetaTable(l *lua.State) {
created := l.NewMetaTable("RouterGroup")
var funcs = map[string]lua.LuaGoFunction{
"BasePath": lua_RouterGroup_BasePath,
}
l.PushString("__index")
l.CreateTable(0, len(funcs))
for fname, f := range funcs {
l.PushGoFunction(f)
l.SetField(-2, fname)
}
l.SetTable(-3)
l.Pop(1)
}

// 注册Engine元表,然后将RouterGroup元表设置为Engine元表的元表
func RegistGoMetaTable(L *lua.State) {
created := l.NewMetaTable("Engine")
var funcs = map[string]lua.LuaGoFunction{
// ...
}
l.PushString("__index")
l.CreateTable(0, len(funcs))
for fname, f := range funcs {
l.PushGoFunction(f)
l.SetField(-2, fname)
}
l.SetTable(-3)
l.Pop(1)

// 将RouterGroup设置为Engine的元表
l.LGetMetaTable("RouterGroup")
l.SetMetaTable(-2)
}

将go结构体指针传入lua虚拟机

1
2
3
4
5
6
7
8
9
func PushGoPointer(l *lua.State, ptr *gin.Engine)  {
value := reflect.ValueOf(ptr)
ptrVal := unsafe.Pointer(value.Pointer())
rawptr := l.NewUserdata(unsafe.Sizeof(ptrVal))
*(*unsafe.Pointer)(rawptr) = ptrVal

l.LGetMetaTable("Engine")
l.SetMetaTable(-2)
}

在lua中调用

1
2
--此时的调用逻辑为: engine userdata => Engine元表 => RouterGroup元表 => lua_RouterGroup_BasePath方法
local path = engine:BasePath()

结论

通过嵌套元表的形式实现的继承,是可以在lua中通过父结构体指针,调用到子结构体的成员方法的。

然而在实际使用过程中,却导致了程序崩溃

问题出在对lua userdata的使用上

  1. 将结构体指针通过userdata传入lua后

    1
    2
    3
    4
    5
    6
    7
    func PushGoPointer(l *lua.State, ptr *gin.Engine)  {
    //...
    // 由于unsafe.Pointer是底层的、非类型安全的指针,Go 编译器不会保留其原始类型的元信息
    // 一旦脱离当前上下文环境,ptrVal就是一个纯指针了
    *(*unsafe.Pointer)(rawptr) = ptrVal
    //...
    }
  2. 通过RouterGroup触发lua_RouterGroup_BasePath方法后,某些情况下会正常工作,某些情况下则会导致程序崩溃

    1
    2
    3
    4
    5
    6
    7
    8
    func lua_RouterGroup_BasePath(L *lua.State) int {
    //......
    // (*(*unsafe.Pointer)(l.ToUserdata(1))),对go来说,取到了一个不包含元信息的纯指针
    // 对一个纯指针进行类型强转(*gin.RouterGroup),go会直接套用RouterGroup类型的底层布局到这个指针上,导致行为未定义
    // 这个指针实际指向的是*gin.Engine
    receiver := (*gin.RouterGroup)(*(*unsafe.Pointer)(l.ToUserdata(1)))
    //......
    }

2. 尝试先获取结构体的成员变量,再由成员变量调用子结构体的函数

go实现

上诉代码不变,在Engine原表中,新增一个获取成员变量的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func lua_Engine_GetMemVar(L *lua.State) int {
receiver := (*gin.Engine)(*(*unsafe.Pointer)(l.ToUserdata(1)))
if receiver == nil {
L.ArgError(1, "ginmodule.GinModule is nil.")
return -1
}
name := L.ToString(2)

// ...

// 通过反射获取成员变量
value := reflect.ValueOf(receiver)
value = value.Elem()
current = value.FieldByName(name)

// ...
// 将成员变量传入lua
PushGoPointer(l, current)
}

在lua中调用

1
2
3
4
--先获取成员变量
local routerGroup = engine:GetMemVar("RouterGroup")
--再通过成员变量调用其BasePath方法
local path = routerGroup:BasePath()

结论

通过获取成员变量的方式,可以正常调用到子结构体的方法

但是无法规避多层嵌套的复杂结构带来的影响,调用子结构方法,需要知晓结构体的完整构成,还需要再每个类型的原表中添加一个GetMemVar方法

总结

  1. 由于lua userdata的实现问题,直接传入lua的go结构体指针,最终会丢失元信息

  2. go是组合继承,lua原表只能实现单继承

  3. go结构体继承有普通类型嵌入,指针嵌入,接口嵌入

由于上诉几点问题,在lua中完整还原go的结构体及其继承关系,并没有比较简单通用的办法。

传入lua的go对象,始终需要手动在go层保留其元信息,lua元表也无法准确还原出go的多种继承方式

需要自行设计一套完整的go、lua映射逻辑才行