将Go对象绑定至lua
通过lua调用go对象的成员函数
与c、c++类似,将原生go对象传入lua,并由lua调用其成员函数。有如下步骤:
1. 注册元表
以gin.Context类为例,将Context类的成员函数,以元表的形似,提前注入lua虚拟机
1 | // func (c *Context) GetRawData() ([]byte, error) |
2. 将go对象,传入lua虚拟机,并设置元表
1 | func Hello(l *Lua.State, ptr *gin.Context) { |
3. 在lua中调用Context对象的方法
1 | -- 参数c是Userdata类型,并有Context元表 |
在lua中还原go继承关系的思考
通过元表,已经可以实现调用go对象的成员函数。
现在以gin.Engine类为例
1 | type Engine struct { |
如果想调用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 | // func (group *RouterGroup) BasePath() string |
将go结构体指针传入lua虚拟机
1 | func PushGoPointer(l *lua.State, ptr *gin.Engine) { |
在lua中调用
1 | --此时的调用逻辑为: engine userdata => Engine元表 => RouterGroup元表 => lua_RouterGroup_BasePath方法 |
结论
通过嵌套元表的形式实现的继承,是可以在lua中通过父结构体指针,调用到子结构体的成员方法的。
然而在实际使用过程中,却导致了程序崩溃
问题出在对lua userdata的使用上
将结构体指针通过userdata传入lua后
1
2
3
4
5
6
7func PushGoPointer(l *lua.State, ptr *gin.Engine) {
//...
// 由于unsafe.Pointer是底层的、非类型安全的指针,Go 编译器不会保留其原始类型的元信息
// 一旦脱离当前上下文环境,ptrVal就是一个纯指针了
*(*unsafe.Pointer)(rawptr) = ptrVal
//...
}通过RouterGroup触发lua_RouterGroup_BasePath方法后,某些情况下会正常工作,某些情况下则会导致程序崩溃
1
2
3
4
5
6
7
8func 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 | func lua_Engine_GetMemVar(L *lua.State) int { |
在lua中调用
1 | --先获取成员变量 |
结论
通过获取成员变量的方式,可以正常调用到子结构体的方法
但是无法规避多层嵌套的复杂结构带来的影响,调用子结构方法,需要知晓结构体的完整构成,还需要再每个类型的原表中添加一个GetMemVar方法
总结
由于lua userdata的实现问题,直接传入lua的go结构体指针,最终会丢失元信息
go是组合继承,lua原表只能实现单继承
go结构体继承有普通类型嵌入,指针嵌入,接口嵌入
由于上诉几点问题,在lua中完整还原go的结构体及其继承关系,并没有比较简单通用的办法。
传入lua的go对象,始终需要手动在go层保留其元信息,lua元表也无法准确还原出go的多种继承方式
需要自行设计一套完整的go、lua映射逻辑才行