【Unity】toLua 中 c# object 与 luavm 之间的交互
ToLua 能够实现 C# 与 Lua 之间的互通,这篇文章分析这一机制的实现原理
1. 如何将一个类型“注入”Lua脚本
在不考虑ToLua
这个库的情况下,先来讨论一下,如何将一个自定义好的class
作为一个第三方库,放到lua
中使用:
将MyPerson类提供给lua调用
MyPerson类的介绍
class MyPerson {
public:
MyPerson(std::string name, int age) :name_(std::move(name)), age_(age) {}
void set_name(const std::string& name) {name_ = name;}
[[nodiscard]] const std::string& get_name() const {return name_;}
void set_age(int age) {age_ = age;}
[[nodiscard]] int get_age() const {return age_;}
private:
std::string name_;
int age_;
};
有上面的这个c++
类,接下来希望放到lua
中使用。
构建mylib库的架子
#define LUA_MY_PERSON "MyPerson"
static const luaL_Reg my_lib[] = {
{"create_my_person", create_my_person},
{nullptr, nullptr}
};
static const luaL_Reg my_person_funcs[] = {
{"get_age", my_person_get_age},
{"set_age", my_person_set_age},
{"get_name", my_person_get_name},
{"set_name", my_person_set_name},
{nullptr, nullptr}
};
int luaopen_mylib(lua_State* L)
{
// 创建 MyPerson metatable.
luaL_newmetatable(L, LUA_MY_PERSON);
lua_newtable(L);
luaL_setfuncs(L, my_person_funcs, 0);
lua_setfield(L, -2, "__index");
lua_pop(L, -1);
// 清空栈,然后创建lib的table, 并且返回.
luaL_newlib(L, my_lib);
return 1;
}
这里从luaopen_mylib
出发,做了两件事情:
- 创建了一张叫做
MyPerson
的metatable
,然后向该metatable
的__index
表中设置了一些函数,相当于构建了一个这样的metatable
:
MyPerson = {
__index = {
get_name = function(self) end,
set_name = function(self, name) end,
get_age = function(self) end,
set_age = function(self, age) end,
}
}
返回了一个
table
,作为require
当前库时的返回结果,执行require
时,拿到的结果实际上是:{ create_my_person = function(name, age) end }
当调用create_my_person
等方法时,lua
会将请求转发到我们编写的库中的函数中执行,接下来我们就来实现这些方法。
实现 create_my_person 方法
用户在lua
中会编写如下的代码对create_my_person
进行调用:
local mylib = require('mylib')
local person = mylib.create_my_person('jack', 18)
在用户调用mylib.create_my_person('jack', 18)
时,luavm
会调用当前的create_my_person((lua_State* L))
函数,调用时栈如下:
图1:调用 create my person 的栈
可以使用luaL_checkstring
, luaL_checkinteger
根据栈上元素的index
进行选取,因此可以写出下面的代码:
static int create_my_person(lua_State* L) {
const std::string name = luaL_checkstring(L, 1);
const int age = static_cast<int>(luaL_checkinteger(L, 2));
new(lua_newuserdata(L, sizeof(MyPerson))) MyPerson(name, age);
luaL_setmetatable(L, LUA_MY_PERSON);
return 1;
}
其中lua_newuserdata
与C
中的malloc
非常相似,开发者可以调用该方法向lua
虚拟机请求一片固定大小的内存,lua
会对这片内存进行管理。
该方法与malloc
一样,只负责开内存,不负责调构造函数,这里这种写法是使用lua_newuserdata
开出来内存之后又调用了MyPerson
的构造函数。
创建好userdata
后,调用luaL_setmetatable
,将之前搭mylib
时注册的MyPerson
表给到了创建好的对象。
函数调用结束后,堆栈如下图所示:
图2:create my person 调用结束时的栈
实现 MyPerson 的成员方法
接着上面的代码,用户在拿到person
后,就会调用person
相关的方法,进行读写操作:
function print_person_info(print_person)
print(print_person:get_name().."'s age is "..print_person:get_age())
end
print_person_info(person)
print("ten years later")
person:set_name('old_'..person:get_name())
person:set_age(person:get_age() + 10)
print_person_info(person)
用户在调用set_name
, set_age
时,其实和python一样,对象本身会作为函数的第一个参数self
进行传递。因此直接写出下面的方法就可以解决:
static int my_person_get_age(lua_State* L){
auto* my_person = reinterpret_cast<MyPerson*>(luaL_checkudata(L, 1, LUA_MY_PERSON));
lua_pushinteger(L, my_person->get_age());
return 1;
}
static int my_person_set_age(lua_State* L){
auto* my_person = reinterpret_cast<MyPerson*>(luaL_checkudata(L, 1, LUA_MY_PERSON));
my_person->set_age(static_cast<int>(luaL_checkinteger(L, 2)));
return 0;
}
static int my_person_get_name(lua_State* L){
auto* my_person = reinterpret_cast<MyPerson*>(luaL_checkudata(L, 1, LUA_MY_PERSON));
lua_pushstring(L, my_person->get_name().c_str());
return 1;
}
static int my_person_set_name(lua_State* L){
auto* my_person = reinterpret_cast<MyPerson*>(luaL_checkudata(L, 1, LUA_MY_PERSON));
my_person->set_name(luaL_checkstring(L, 2));
return 0;
}
在这些方法中,person
作为userdata
被推入栈中,我们可以通过luaL_checkudata
获取到之前通过lua_newuserdata
申请到的内存地址,拿到地址后,将其强转为MyPerson
指针,在进行其他操作即可。
在c++中将这个case跑起来
static const std::string kLuaCode = R"(
local mylib = require('mylib')
local person = mylib.create_my_person('jack', 18)
function print_person_info(print_person)
print(print_person:get_name().."'s age is "..print_person:get_age())
end
print_person_info(person)
print("ten years later")
person:set_name('old_'..person:get_name())
person:set_age(person:get_age() + 10)
print_person_info(person)
)";
LUALIB_API int luaopen_mylib(lua_State* L);
int main() {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaL_requiref(L, "mylib", luaopen_mylib, 0);
lua_pop(L, 1);
if (int ret = luaL_dostring(L, kLuaCode.c_str()); ret != 0) {
std::cout << "error, " << lua_tostring(L, -1) << std::endl;
}
return 0;
}
直接起一个luavm
来跑这段代码即可,特殊的点在于,需要提前调用luaL_requiref
来将mylib
的open
方法进行注册,这样在lua
中调用require('mylib')
时,就不会去本地文件中寻找mylib.so
,而是直接调用已经注册的luaopen_mylib
方法。
将mylib
代码与上面的代码一起编译,运行就可以得到:
~/luavm/cmake-build-debug/luavm.exe
jack's age is 18
ten years later
old_jack's age is 28
解决MyPerson的内存问题
MyPerson没能正确释放内存
发现问题
上面就把一个简单的case跑通了,但是如果我们在lua
中创建一个循环,不断地去跑这个case,就会发现一个问题:
local mylib = require('mylib')
for i = 0, 1000000000000, 1 do
local person = mylib.create_my_person('jack', 18)
end
当我们运行上述lua
代码时,会发现:

图3:循环创建person时,内存很高
这个进程占用的内存会越来越大,这说明某些地方发生了内存泄漏。
问题的原因
这里MyPerson
类中,使用了std::string
,std::string
对象的内存分布如下:

图4:std::string的内存分布(参考自《C++ string 源码实现对比》)
当我们创建一个std::string
,并且为他赋值时,他会分配一片新的内存,并且将字符串内容存储在新分配的内存中,而luavm
管理内存时,只会将_M_dataplus
的部分干掉,而不会触发string
的析构,因此堆上分配的空间不会被释放。
解决的思路:__gc函数
lua
中提供了一种机制,如果你的metatable
中含有__gc
方法,那么在gc
要删除这个对象时,就会先调用你内置的__gc
方法,可以通过这个case
来体验:
function create_useless_data()
local test_meta = {
__gc = function()
print('gc')
end
}
return setmetatable({}, test_meta)
end
create_useless_data()
collectgarbage("collect")
使用lua
运行,可以观察到:
gc
这里创建了一个含有__gc
方法的metatable
,我们通过create_useless_data
函数,创建出来了一个没有用的对象,然后使用collectgarbage("collect")
触发了gc
的全流程,进行了标记清除,清除useless
对象时,由于metatable.__gc
方法存在,因此调用该方法,执行结束后,才对对象进行释放。
说明:
在lua 5.1版本中,只有userdata上绑定的__gc方法会被调用
在lua 5.2及其往后的版本中,table上绑定的__gc方法也能被正常调用
基于此,我们可以得到两种方法,来解决内存没法正常释放的问题:
- 触发
__gc
时,显式调用析构函数; - 在
mylib
中通过new, delete
管理所有内存,luavm
中只持有指针;
方案1:显式调用析构函数
为MyPerson
添加__gc
成员方法on_lua_gc
,然后创建一个luaCFunction
进行调用:
static int my_person_on_lua_gc(lua_State* L){
auto* my_person = reinterpret_cast<MyPerson*>(luaL_checkudata(L, 1, LUA_MY_PERSON));
my_person->~MyPerson();
return 0;
}
将新的my_person_on_lua_gc
方法,注册到MyPerson
的Metatable
中:
int luaopen_mylib(lua_State* L)
{
luaL_newmetatable(L, LUA_MY_PERSON);
// 新增这两行,将my_person_on_lua_gc这个函数,作为__gc设置给LUA_MY_PERSON
lua_pushcfunction(L, my_person_on_lua_gc);
lua_setfield(L, -2, "__gc");
// .. 下面的部分都一样,省略
lua_pop(L, -1);
luaL_newlib(L, my_lib);
return 1;
}
这个方案很好理解,就是在free
掉内存之前,先调用默认析构函数,将MyPerson
下面的内容进行析构。
方案2: mylib中管理内存, lua中只管理指针
改造create_my_person 方法:
static int create_my_person(lua_State* L) {
std::string name = luaL_checkstring(L, 1);
int age = static_cast<int>(luaL_checkinteger(L, 2));
*reinterpret_cast<MyPerson**>(lua_newuserdata(L, sizeof(MyPerson*))) = new MyPerson(name, age);
luaL_setmetatable(L, LUA_MY_PERSON);
return 1;
}
这里有点绕,我们通过lua_newuserdata
申请对象时,申请的不再是一整个对象的空间,而是一个指针的空间。
这里的lua_newuserdata
返回的是my_person
指针的指针,拿到这个指针的指针之后,我们使用new
方法创建一个MyPerson
,赋值给这个指针的指针。
改造 __gc 方法
static int my_person_on_lua_gc(lua_State* L){
auto* my_person = *reinterpret_cast<MyPerson**>(luaL_checkudata(L, 1, LUA_MY_PERSON));
delete my_person;
return 0;
}
这里获取my_person
的方式也要进行修改,因为luavm
中记录的userdata
不是MyPerson
的完整数据了,而是MyPerson
的地址。
这里拿到MyPerson
地址的地址,反取一下,就可以拿到my_person
指针了,对这个地址调用delete
方法即可。
改造其他成员方法
这里的改造思路与__gc
方法完全一致,就不放重复的代码了。
验证与对比

图5:方案1效果,调用析构函数

图6:方案2效果,lua只持有指针
运行发现实际运行的效果与我们的预期有所不同,他们的内存竟然都在缓慢的增长?
从原理来看,都能够正常的调用到__gc
方法,但是给MyPerson
的构造函数与析构函数添加计数后发现,方案2中未被释放的对象数量竟然在逐渐上涨。
猜测是因为lua
中gc
的速度跟不上分配对象的速度,导致内存一直释放不掉。如果我们在lua
代码中加入定期主动gc
,可以暂时解决问题:
local mylib = require('mylib')
for i = 0, 1000000000000, 1 do
local person = mylib.create_my_person('jack', 18)
if (i % 1000000 == 0) then
collectgarbage("collect")
end
end
修改后再重新运行,能够观察到内存稳定在一定区间内。
2. ToLua如何将 C# 对象“注入”Lua脚本
tolua
:https://github.com/topameng/toluatolua_runtime
:https://github.com/topameng/tolua_runtime
基本用法
在 C# 中执行 lua代码
ToLua
包装了Lua
,在C#
中提供了C#
与lua
互通的能力,用户可以通过C#
的接口来创建luastate
,然后在里面运行Lua
代码:
using UnityEngine;
using LuaInterface;
using System;
public class HelloWorld : MonoBehaviour
{
void Awake()
{
LuaState lua = new LuaState();
lua.Start();
string hello =
@"
print('hello tolua#')
";
lua.DoString(hello, "HelloWorld.cs");
lua.CheckTop();
lua.Dispose();
lua = null;
}
}
将 C# 的类型开放给 lua 使用
同样的,通过toLua
,我们也能够将C#
中已经编写好的代码交给Lua
来使用:
创建c#类
public class MyPerson {
public static MyPerson Create(string name, int age) { return new MyPerson(name, age); }
public void SetName(string name) { this.name = name; }
public string GetName() { return this.name; }
public void SetAge(int age) { this.age = age; }
public int GetAge() { return this.age; }
private MyPerson(string name, int age)
{
this.name = name;
this.age = age;
}
private string name;
private int age;
}
通过 tolua 生成 wrap 文件
首先需要修改ToLua
的 CustomerSettings.cs
文件,修改其中的customTypeList
添加刚才编写的MyPerson
类型:
public static class CustomSettings
{
//在这里添加你要导出注册到lua的类型列表
public static BindType[] customTypeList =
{
_GT(typeof(MyPerson)),
_GT(typeof(LuaInjectionStation)),
_GT(typeof(InjectType)),
_GT(typeof(Debugger)).SetNameSpace(null),
// ... 剩余的忽略.
}
}
配置好之后,点击unity
中的Lua > Generate All
可以看到在项目的Source/Generate
目录下已经生成了MyPersonWrap
文件,这说明可以在lua
中使用这个类型了。
c#中使用luabinder绑定库
using UnityEngine;
using LuaInterface;
using System;
public class HelloWorld : MonoBehaviour
{
void Awake()
{
LuaState lua = new LuaState();
lua.Start();
LuaBinder.Bind(lua); // 新增这一行,用于绑定所有库.
string hello =
@"
print_person_info(person)
";
lua.DoString(hello, "HelloWorld.cs");
lua.CheckTop();
lua.Dispose();
lua = null;
}
}
编写lua代码
local person = MyPerson.Create('jack', 18)
function print_person_info(print_person)
print(print_person:GetName().."'s age is "..print_person:GetAge())
end
print_person_info(person)
print('ten years later')
person:SetName('old_'..person:GetName())
person:SetAge(person:GetAge() + 10)
print_person_info(person)
把之前的case小改一下拿来用即可。
运行
直接在Editor
中运行就可以看到结果

图6:lua 调用 c# 方法
C# 对象 bind 原理
LuaBinder.cs
LuaBinder
是由toLua
自动生成的, 他根据CustomSettings
中
public static class LuaBinder
{
public static void Bind(LuaState L)
{
float t = Time.realtimeSinceStartup;
L.BeginModule(null);
MyPersonWrap.Register(L);
LuaInterface_DebuggerWrap.Register(L);
LuaProfilerWrap.Register(L);
L.BeginModule("LuaInterface");
LuaInterface_LuaInjectionStationWrap.Register(L);
LuaInterface_InjectTypeWrap.Register(L);
L.EndModule();
L.BeginModule("UnityEngine");
UnityEngine_ComponentWrap.Register(L);
// ...
}
// ...
}
刚才的C#
代码中,我们调用LuaBinder.Bind(lua)
将对一些unity
内置的类型,以及我们编写的C#
类型绑定到了lua
环境中。
通过代码可以看到,这里分了多个模块:空模块、LuaInterface
、UnityEngine
我们的MyPersonWrap
注册在默认模块下,而Compoment
等常用的UnityEngine
类型封装在UnityEngine
模块下。
接下来就顺着这个注册的流程来观察,tolua
是怎么操作的。
注册模块到lua
BeginModule
public bool BeginModule(string name)
{
#if UNITY_EDITOR
if (name != null)
{
LuaTypes type = LuaType(-1);
if (type != LuaTypes.LUA_TTABLE)
{
throw new LuaException("open global module first");
}
}
#endif
if (LuaDLL.tolua_beginmodule(L, name))
{
++beginCount;
return true;
}
LuaSetTop(0);
throw new LuaException(string.Format("create table {0} fail", name));
}
这里调用了预编译好的LuaDLL
中的tolua_beginmodule
方法,可以进入tolua_runtime
中查看:
void pushmodule(lua_State *L, const char *str)
{
luaL_Buffer b;
luaL_buffinit(L, &b);
if (sb.len > 0)
{
luaL_addlstring(&b, sb.buffer, sb.len);
luaL_addchar(&b, '.');
}
luaL_addstring(&b, str);
luaL_pushresult(&b);
sb.buffer = lua_tolstring(L, -1, &sb.len);
}
LUALIB_API bool tolua_beginmodule(lua_State *L, const char *name)
{
if (name != NULL)
{
lua_pushstring(L, name); //stack key
lua_rawget(L, -2); //stack value
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
lua_newtable(L); //stack table
lua_pushstring(L, "__index");
lua_pushcfunction(L, module_index_event);
lua_rawset(L, -3);
lua_pushstring(L, name); //stack table name
lua_pushstring(L, ".name"); //stack table name ".name"
pushmodule(L, name); //stack table name ".name" module
lua_rawset(L, -4); //stack table name
lua_pushvalue(L, -2); //stack table name table
lua_rawset(L, -4); //stack table
lua_pushvalue(L, -1);
lua_setmetatable(L, -2);
return true;
}
else if (lua_istable(L, -1))
{
if (lua_getmetatable(L, -1) == 0)
{
lua_pushstring(L, "__index");
lua_pushcfunction(L, module_index_event);
lua_rawset(L, -3);
lua_pushstring(L, name); //stack table name
lua_pushstring(L, ".name"); //stack table name ".name"
pushmodule(L, name); //stack table name ".name" module
lua_rawset(L, -4); //stack table name
lua_pushvalue(L, -2); //stack table name table
lua_rawset(L, -4); //stack table
lua_pushvalue(L, -1);
lua_setmetatable(L, -2);
}
else
{
lua_pushstring(L, ".name");
lua_gettable(L, -3);
sb.buffer = lua_tolstring(L, -1, &sb.len);
lua_pop(L, 2);
}
return true;
}
return false;
}
else
{
lua_pushvalue(L, LUA_GLOBALSINDEX);
return true;
}
}
首次执行时,传入的name
为nil
,会将_G
全局Table
放在栈顶
再次执行L.BeginModule("LuaInterface")
时,其堆栈变化如下:
图7:执行 tolua_beginmodule 时的栈变化
可以看出,其主要作用就是创建了一张新的table
,分别为其设置__index
, .name
, metatable
,并且将其加入了_G
全局表中。
EndModule
public void EndModule()
{
--beginCount;
LuaDLL.tolua_endmodule(L);
}
LUALIB_API void tolua_endmodule(lua_State *L)
{
lua_pop(L, 1);
int len = (int)sb.len;
while(len-- >= 0)
{
if (sb.buffer[len] == '.')
{
sb.len = len;
return;
}
}
sb.len = 0;
}
EndModule
就是将刚才编辑好的_G["ModuleName"]
给pop
掉,然后再将sb
中记录的namespace
退回一层。
注册Class到Lua
从上面可以看出,Module
的结构和C#
中namespace
的结构完全相同,有了namespace
后,就要向里面注册各种class
了,class
的注册就是对各个Wrap
调用Register
方法:
public class MyPersonWrap
{
public static void Register(LuaState L)
{
L.BeginClass(typeof(MyPerson), typeof(System.Object));
L.RegFunction("Create", Create);
L.RegFunction("SetName", SetName);
L.RegFunction("GetName", GetName);
L.RegFunction("SetAge", SetAge);
L.RegFunction("GetAge", GetAge);
L.RegFunction("__tostring", ToLua.op_ToString);
L.EndClass();
}
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int Create(IntPtr L)
{
try
{
ToLua.CheckArgsCount(L, 2);
string arg0 = ToLua.CheckString(L, 1);
int arg1 = (int)LuaDLL.luaL_checknumber(L, 2);
MyPerson o = MyPerson.Create(arg0, arg1);
ToLua.PushObject(L, o);
return 1;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
// .. 其他方法省略.
}
可以看到MyPersonWrap
中根据每个方法的类型、参数数量对原有的C#
函数进行了包装,使用luaL_checknumber
获取参数,转发到C#
中执行。
而Register
方法,将这些包装好的方法注册到lua
环境中,接下来主要看Register
方法的实现。
Begin Class
public int BeginClass(Type t, Type baseType, string name = null)
{
if (beginCount == 0)
{
throw new LuaException("must call BeginModule first");
}
int baseMetaRef = 0;
int reference = 0;
if (name == null)
{
name = GetToLuaTypeName(t);
}
if (baseType != null && !metaMap.TryGetValue(baseType, out baseMetaRef))
{
LuaCreateTable();
// public static int LUA_REGISTRYINDEX = -10000;
baseMetaRef = LuaRef(LuaIndexes.LUA_REGISTRYINDEX);
BindTypeRef(baseMetaRef, baseType);
}
if (metaMap.TryGetValue(t, out reference))
{
LuaDLL.tolua_beginclass(L, name, baseMetaRef, reference);
RegFunction("__gc", Collect);
}
else
{
reference = LuaDLL.tolua_beginclass(L, name, baseMetaRef);
RegFunction("__gc", Collect);
BindTypeRef(reference, t);
}
return reference;
}
这个函数会确认t
, baseType
已经有ref
的id
,并且确认他们的__gc
都已经被注册为Collect
函数。
这里LUA_REGISTRYINDEX
是一个特殊的index
,调用luaL_ref
时,传入LUA_REGISTRYINDEX
时,会存入lua的注册表中。
通过观察LuaBinder
的代码可以看出,这里维护了两个成员,:metaMap
,typeMap
,他们在c#
中记录了t
和ref_id
的映射关系。
继续观察tolua_beginclass
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int tolua_beginclass(IntPtr L, string name, int baseMetaRef, int reference = -1);
可以看出来,当metaMap
中能够找到t
时,说明t
作为其他类的baseType
已经进行过注册,reference
会传递其注册过的值,而找不到时,会传-1
。
static void _addtoloaded(lua_State *L)
{
lua_getref(L, LUA_RIDX_LOADED);
_pushfullname(L, -3); // 相当于 lua_pushstring("UnityEngine.Compoment")
lua_pushvalue(L, -3);
lua_rawset(L, -3);
lua_pop(L, 1);
}
LUALIB_API int tolua_beginclass(lua_State *L, const char *name, int baseType, int ref)
{
int reference = ref;
lua_pushstring(L, name);
lua_newtable(L);
_addtoloaded(L);
if (ref == LUA_REFNIL)
{
lua_newtable(L);
lua_pushvalue(L, -1);
reference = luaL_ref(L, LUA_REGISTRYINDEX);
}
else
{
lua_getref(L, reference);
}
if (baseType != 0)
{
lua_getref(L, baseType);
lua_setmetatable(L, -2);
}
lua_pushlightuserdata(L, &tag);
lua_pushnumber(L, 1);
lua_rawset(L, -3);
lua_pushstring(L, ".name");
_pushfullname(L, -4);
lua_rawset(L, -3);
lua_pushstring(L, ".ref");
lua_pushinteger(L, reference);
lua_rawset(L, -3);
lua_pushstring(L, "__call");
lua_pushcfunction(L, class_new_event);
lua_rawset(L, -3);
tolua_setindex(L);
tolua_setnewindex(L);
return reference;
}
首先会对当前类建立一个
table
,并且将其加到一张LUA_RIDX_LOADED
的表中,如果这个类型不存在,就创建这个类型,并且
ref
到LUA_REGISTRYINDEX
中;如果存在
baseType
,就将baseType
的table
取出,作为metatable
塞给当前类型的table
;接下来对当前类型的table进行一系列操作:
meta_table[magic_number] = 1 meta_table[".name"] = "MyPerson" meta_table[".ref"] = ref_id meta_table["__call"] = c_func_class_new_event -- tolua_setindex meta_table["__index"] = c_func_class_index_event -- tolua_setnewindex meta_table["__newindex"] = c_func_class_newindex_event
在tolua_beginclass
结束后,C#
中还会调用RegFunction("__gc", Collect);
将c#
中的Collect
方法注册进来。
接下来就可以继续向这张table
中注册其他的方法和成员了。
EndClass
在类型注册结束后,会调用L.EndClass();
,简单看一下发生了什么:
public void EndClass()
{
LuaDLL.tolua_endclass(L);
}
LUALIB_API void tolua_endclass(lua_State *L)
{
lua_setmetatable(L, -2);
lua_rawset(L, -3);
}
这里将编辑完的meta_table
设置给了MyPerson
的Table
,并且将其设置到了正在编辑的Module Table
中。
图9:创建 Class 时的Table关联
RegFunction
Wrap
会调用RegFunction
来注册函数:
public void RegFunction(string name, LuaCSFunction func)
{
IntPtr fn = Marshal.GetFunctionPointerForDelegate(func);
LuaDLL.tolua_function(L, name, fn);
}
LUALIB_API void tolua_function(lua_State *L, const char *name, lua_CFunction fn)
{
lua_pushstring(L, name);
tolua_pushcfunction(L, fn);
lua_rawset(L, -3);
}
tolua_function
的实现就与我们之前写的完全一样了,唯一不同的点就在于,这里使用Marshal.GetFunctionPointerForDelegate(func);
将MyPersonWrap::SetName
这样的委托转为一个函数指针,将转换后的地址放到了lua
里。
RegVar
一些c#
类中,含有一些成员变量,供外部访问,这里也支持使用RegVar
方法来对这种变量进行注册:
public void RegVar(string name, LuaCSFunction get, LuaCSFunction set)
{
IntPtr fget = IntPtr.Zero;
IntPtr fset = IntPtr.Zero;
if (get != null)
{
fget = Marshal.GetFunctionPointerForDelegate(get);
}
if (set != null)
{
fset = Marshal.GetFunctionPointerForDelegate(set);
}
LuaDLL.tolua_variable(L, name, fget, fset);
}
LUALIB_API void tolua_variable(lua_State *L, const char *name, lua_CFunction get, lua_CFunction set)
{
lua_pushlightuserdata(L, &gettag);
lua_rawget(L, -2);
if (!lua_istable(L, -1))
{
/* create .get table, leaving it at the top */
lua_pop(L, 1);
lua_newtable(L);
lua_pushlightuserdata(L, &gettag);
lua_pushvalue(L, -2);
lua_rawset(L, -4);
}
lua_pushstring(L, name);
//lua_pushcfunction(L, get);
tolua_pushcfunction(L, get);
lua_rawset(L, -3); /* store variable */
lua_pop(L, 1); /* pop .get table */
/* set func */
if (set != NULL)
{
lua_pushlightuserdata(L, &settag);
lua_rawget(L, -2);
if (!lua_istable(L, -1))
{
/* create .set table, leaving it at the top */
lua_pop(L, 1);
lua_newtable(L);
lua_pushlightuserdata(L, &settag);
lua_pushvalue(L, -2);
lua_rawset(L, -4);
}
lua_pushstring(L, name);
//lua_pushcfunction(L, set);
tolua_pushcfunction(L, set);
lua_rawset(L, -3); /* store variable */
lua_pop(L, 1); /* pop .set table */
}
}
这里就不做太多的讲解了,简单来说,就是在Class MetaTable
中维护了两张表,gettag
为索引的get_table
, settag
为索引的settable
。
通过ClassMetaTable[gettag]["var_name"]
就可以找到对应的getter
Lua中对c#对象的创建 & 使用
创建 c# 对象
当用户在lua
中调用Vector3(0,0,360)
、MyPerson.Create()
时,之前编写的class_new_event
、MypersonWrap.Create
会被触发。我们来看看tolua
是如何创建c#
对象并返回的。
class_new_event 调用构造函数
static int class_new_event(lua_State *L)
{
if (!lua_istable(L, 1))
{
return luaL_typerror(L, 1, "table");
}
int count = lua_gettop(L);
lua_pushvalue(L,1);
if (lua_getmetatable(L,-1))
{
lua_remove(L,-2);
lua_pushstring(L, "New");
lua_rawget(L,-2);
if (lua_isfunction(L,-1))
{
for (int i = 2; i <= count; i++)
{
lua_pushvalue(L, i);
}
lua_call(L, count - 1, 1);
return 1;
}
lua_settop(L,3);
}
return luaL_error(L,"attempt to perform ctor operation failed");
}
如果我们的类没有仅用默认构造函数,或是编写了public
的构造函数,那么tolua
会根据我们编写的构造函数创建New
方法:
加入我们实现了四种不同的构造函数:
public class MyPerson {
public MyPerson(string name, int age)
{
this.name = name;
this.age = age;
}
public MyPerson(string name)
{
this.name = name;
}
public MyPerson(int age)
{
this.age = age;
}
public MyPerson() {}
private string name;
private int age;
}
那么在MyPersonWrap
中,会有:
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _CreateMyPerson(IntPtr L)
{
try
{
int count = LuaDLL.lua_gettop(L);
if (count == 0)
{
MyPerson obj = new MyPerson();
ToLua.PushObject(L, obj);
return 1;
}
else if (count == 1 && TypeChecker.CheckTypes<int>(L, 1))
{
int arg0 = (int)LuaDLL.lua_tonumber(L, 1);
MyPerson obj = new MyPerson(arg0);
ToLua.PushObject(L, obj);
return 1;
}
else if (count == 1 && TypeChecker.CheckTypes<string>(L, 1))
{
string arg0 = ToLua.ToString(L, 1);
MyPerson obj = new MyPerson(arg0);
ToLua.PushObject(L, obj);
return 1;
}
else if (count == 2)
{
string arg0 = ToLua.CheckString(L, 1);
int arg1 = (int)LuaDLL.luaL_checknumber(L, 2);
MyPerson obj = new MyPerson(arg0, arg1);
ToLua.PushObject(L, obj);
return 1;
}
else
{
return LuaDLL.luaL_throw(L, "invalid arguments to ctor method: MyPerson.New");
}
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
tolua会根据参数的数量和类型,调用不同的构造函数,最后调用ToLua.PushObject
来将c#
对象压栈,进行返回。
PushObject 将 c# 对象转为 userdata
继续来看ToLua.PushObject
的实现:
public static void PushObject(IntPtr L, object o)
{
if (o == null || o.Equals(null))
{
LuaDLL.lua_pushnil(L);
}
else
{
if (o is Enum)
{
ToLua.Push(L, (Enum)o);
}
else
{
PushUserObject(L, o);
}
}
}
//o 不为 null
static void PushUserObject(IntPtr L, object o)
{
Type type = o.GetType();
int reference = LuaStatic.GetMetaReference(L, type);
if (reference <= 0)
{
reference = LoadPreType(L, type);
}
PushUserData(L, o, reference);
}
public static void PushUserData(IntPtr L, object o, int reference)
{
int index;
ObjectTranslator translator = ObjectTranslator.Get(L);
if (translator.Getudata(o, out index))
{
if (LuaDLL.tolua_pushudata(L, index))
{
return;
}
translator.Destroyudata(index);
}
index = translator.AddObject(o);
LuaDLL.tolua_pushnewudata(L, reference, index);
}
这里会在PushUserObject
中准备好Object
在lua
中的ref_id
,随后进入PushUserData
。
在PushUserData
中,对每一个LuaState
都会维护一个ObjectTranslator
,这个ObjectTranslator
负责为每一个 Object
分配一个id
。
// ObjectTranslator [objects 为 LuaObjectPool]
public int AddObject(object obj)
{
int index = objects.Add(obj);
if (!TypeChecker.IsValueType(obj.GetType()))
{
objectsBackMap[obj] = index;
}
return index;
}
// LuaObjectPool
public int Add(object obj)
{
int pos = -1;
if (head.index != 0)
{
pos = head.index;
list[pos].obj = obj;
head.index = list[pos].index;
}
else
{
pos = list.Count;
list.Add(new PoolNode(pos, obj));
count = pos + 1;
}
return pos;
}
ObjectTranslator
下维护了一个LuaObjectPool
,这个Pool
维护了一个链表,所有正在使用中的Object
都会注册在链表中。
Add
操作实际上就是向链表中添加了一个节点,获取到的index
就是Object
在List
中的位置,然后再在ObjectTranslator
对object --> index
的映射关系做了缓存
拿到Object
对应的id
后,就可以调用tolua_pushnewudata
将这个id
作为userdata
压入lua
栈中:
新对象
LUALIB_API void tolua_newudata(lua_State *L, int val)
{
int* pointer = (int*)lua_newuserdata(L, sizeof(int));
lua_pushvalue(L, TOLUA_NOPEER);
lua_setfenv(L, -2);
*pointer = val;
}
LUALIB_API void tolua_pushnewudata(lua_State *L, int metaRef, int index)
{
lua_getref(L, LUA_RIDX_UBOX);
tolua_newudata(L, index);
lua_getref(L, metaRef);
lua_setmetatable(L, -2);
lua_pushvalue(L, -1);
lua_rawseti(L, -3, index);
lua_remove(L, -2);
}
tolua
中的每一个userdata
都只占用一个int
的空间大小,在压入id
作为userdata
后,通过lua_getref
通过类型的ref_id
获取到class Table
设为userdata
的metatable
。最后以index
为key
,将这个userdata
设入LUA_RIDX_UBOX
中。
同一个对象再次push
同一个对象再次push时,就会从刚才的LUA_RIDX_UBOX
中直接拿出来:
LUALIB_API bool tolua_pushudata(lua_State *L, int index)
{
lua_getref(L, LUA_RIDX_UBOX); // stack: ubox
lua_rawgeti(L, -1, index); // stack: ubox, obj
if (!lua_isnil(L, -1))
{
lua_remove(L, -2); // stack: obj
return true;
}
lua_pop(L, 2);
return false;
}
这样就完成了对象的创建 & 压栈,创建好的对象能够正确的被赋予metatable
。
销毁 c# 对象
在注册类型时,就已经对metatable
写入了__gc
方法,这里的gc
方法全部被倒到了Collect
函数中处理:
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
public static int Collect(IntPtr L)
{
int udata = LuaDLL.tolua_rawnetobj(L, 1);
if (udata != -1)
{
ObjectTranslator translator = GetTranslator(L);
translator.RemoveObject(udata);
}
return 0;
}
这里会使用LuaDLL.tolua_rawnetobj(L, 1)
获取UserObject
,结合tolua_rawnetobj
来看,拿到的就是刚才在translator
中分配的index
。
接下来userObject
会调用RemoveObject
来回收这个c# Object
//lua gc一个对象(lua 库不再引用,但不代表c#没使用)
public void RemoveObject(int udata)
{
//只有lua gc才能移除
object o = objects.Remove(udata);
if (o != null)
{
// 对于 enum 的特殊处理,暂时不考虑.
if (!TypeChecker.IsValueType(o.GetType()))
{
RemoveObject(o, udata);
}
if (LogGC)
{
Debugger.Log("gc object {0}, id {1}", o, udata);
}
}
}
// objects.Remove ()
public object Remove(int pos)
{
if (pos > 0 && pos < count)
{
object o = list[pos].obj;
list[pos].obj = null;
list[pos].index = head.index;
head.index = pos;
return o;
}
return null;
}
这里的remove
操作就非常简单了,在List
中,将相应的节点删除,那么对应Object
在C#
的内存空间中,就会变成无用的一片内存,这样c#
中对应的元素就会被c#
的gc清除。
与此同时,ObjectTranslator
中缓存的映射关系也会被清除,清楚后,下一次不同的Object
被分配到List
上相同index
时,由于ObjectTranslator
Miss,会重新触发tolua_pushnewudata
,来覆盖luavm
中的LUA_RIDX_UBOX
。
调用 c# 对象相关的方法
函数调用
回顾一下之前c#
中的函数:
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int SetName(IntPtr L)
{
try
{
ToLua.CheckArgsCount(L, 2);
MyPerson obj = (MyPerson)ToLua.CheckObject<MyPerson>(L, 1);
string arg0 = ToLua.CheckString(L, 2);
obj.SetName(arg0);
return 0;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
这个函数被注释为MonoPInvokeCallbackAttribute
,文档中对其的解释为:
此属性对静态函数有效,Mono 的预前编译器使用它来生成支持本机调用回调到托管代码所需的代码。
您必须指定调用此代码的委托的类型。
所以相当于编译时通过Marshal.GetFunctionPointerForDelegate(func);
拿到的函数地址,给到luavm
后调用时,会调用到Mono
编译器生成的可以直接调用的地址。
getter setter & 下标访问
当我们使用RegVar
注册成员变量时,会将该变量的getter
, setter
注册到该类的metatable
下的&getter_tag
, &setter_tag
这两个key
对应的table
中。
在lua中访问成员时,由于该成员没有在table
中注册,因此会将成员访问请求转发到__index
中进行处理。
在BeginClass
时,为所有metatable
设置了__index = class_index_event()
,接下来就看一下class_index_event
是如何与之前注册的表一起工作的:
static int class_index_event(lua_State *L)
{
int t = lua_type(L, 1);
if (t == LUA_TUSERDATA)
{
lua_getfenv(L,1);
if (!lua_rawequal(L, -1, TOLUA_NOPEER)) // stack: t k env
{
// 由于这里只讨论userdata,所以不会进入到这个分支,直接忽略.
};
lua_settop(L,2);
lua_pushvalue(L, 1); // stack: obj key obj
while (lua_getmetatable(L, -1) != 0)
{
lua_remove(L, -2); // stack: obj key mt
if (lua_isnumber(L,2)) // check if key is a numeric value
{
lua_pushstring(L,".geti");
lua_rawget(L,-2); // stack: obj key mt func
if (lua_isfunction(L,-1))
{
lua_pushvalue(L,1);
lua_pushvalue(L,2);
lua_call(L,2,1);
return 1;
}
}
else
{
lua_pushvalue(L, 2); // stack: obj key mt key
lua_rawget(L, -2); // stack: obj key mt value
if (!lua_isnil(L, -1))
{
return 1;
}
lua_pop(L, 1);
lua_pushlightuserdata(L, &gettag);
lua_rawget(L, -2); //stack: obj key mt tget
if (lua_istable(L, -1))
{
lua_pushvalue(L, 2); //stack: obj key mt tget key
lua_rawget(L, -2); //stack: obj key mt tget value
if (lua_isfunction(L, -1))
{
lua_pushvalue(L, 1);
lua_call(L, 1, 1);
return 1;
}
}
}
lua_settop(L, 3);
}
lua_settop(L, 2);
int *udata = (int*)lua_touserdata(L, 1);
if (*udata == LUA_NULL_USERDATA)
{
return luaL_error(L, "attemp to index %s on a nil value", lua_tostring(L, 2));
}
if (toluaflags & FLAG_INDEX_ERROR)
{
return luaL_error(L, "field or property %s does not exist", lua_tostring(L, 2));
}
}
else if(t == LUA_TTABLE)
{
// 由于这里只讨论userdata,所以不会进入到这个分支,直接忽略.
}
lua_pushnil(L);
return 1;
}
在用户编写: my_object.target_field
时,实际上由class_index_event(my_object, "target_field")
方法进行处理。
结合之前BeginClass
部分谈到的根据继承关系设置的metatable
链条来看,这里会顺着c#
中的继承关系逐层向下进行查找。
对于每一层metatable
:
- 尝试直接在
metatable
中通过用户访问的字段直接获取到值,如果能拿到的话就直接返回 - 尝试获取
getter_table
,并且在getter_table
中通过相应的字段找到getter
函数,如果能找到的话,就调用这个getter
函数,获取值。 - 什么都找不到就去找下一层继承关系的
metatable
(也就是对当前的metatable
取metatable
)
当遍历完所有的metatable
后,还是找不到时,就返回nil
。
可以看到,这里可能会存在用户使用数字下标来访问的情况,如:
local num = myarray[10]
这种情况就会在递归寻找的过程中查找是否有.geti
方法,如果有的话就用.geti
来获取。
其中.geti
实现如下:
// System_ArrayWrap.cs
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int get_Item(IntPtr L)
{
try
{
Array obj = ToLua.ToObject(L, 1) as Array;
if (obj == null)
{
throw new LuaException("trying to index an invalid object reference");
}
int index = (int)LuaDLL.lua_tointeger(L, 2);
if (index >= obj.Length)
{
throw new LuaException("array index out of bounds: " + index + " " + obj.Length);
}
Type t = obj.GetType().GetElementType();
if (t.IsValueType)
{
// 特殊类型的处理, 这里不重要,忽略...
}
object val = obj.GetValue(index);
ToLua.Push(L, val);
return 1;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
3. 对于现有方法的讨论
以刚才的MyPerson
为例,他在整体流程中的所有相关操作如下:
图10:userdata 相关交互流程
现有方案中,luavm
与c#
, luavm
与tolua_runtime
之间的交互中,涉及到app
环境与wasm
环境的切换,可能产生问题,可能产生问题的环节有三个:
tolua_runtime
调用lua_newuserdata
;luavm
通过RegFunction
提供的函数指针调用wasm
中的c#
函数;luavm
垃圾回收时的内存释放;
接下来分开讨论。
a. lua_newuserdata的处理
lua_newuserdata
的功能类似于malloc
,会根据调用者给出的参数开出一片特定大小的内存,返回给调用者,供调用者使用。
在当前项目中的问题是lua_newuserdata
开的内存开在app
环境中,而runtime
跑在wasm
中,无法直接使用。
能够解决这个问题的方法是,lua_newuserdata
时同时开一片同样大小的wasm
的内存,作为返回值返回给调用者,但是在实现的细节上存在两种方案:
- 在
wrap
层调用lua_newuserdata
后,调用wasm
的内存分配器再分配同一片内存; - 直接修改
lua
中lua_newuserdata
的代码,在调用luaM_realloc_
时动手,只分配wasm
内存,而不分配app
内存,这样可以少用点空间。
第二种方案会侵入到luavm
内部,可能会有一些开发量,但是好处是能节省一半的空间,目前没有仔细研究protobuf
,json
这些第三方库创建userdata
的逻辑,如果只使用tolua
中的userdata
只记录index
的这种思路的话,那这里改动能节省下来的空间就很小,但是如果会大片大片开内存的话,还是能节省很多空间的。
b. luavm到wasm的函数调用
这里il2cpp
后,拿到委托的地址,应该是一个wasm
地址,那这里 luavm
从 table
拿到回调地址后调用时是如何处理的?能够正确的完成调用么?
c. lauvm垃圾回收时正确回收内存
lua5.1中的垃圾回收分为以下阶段:
lu_byte gcstate:存放GC状态,分别有以下几种:GCSpause(暂停阶段)、GCSpropagate(传播阶段,用于遍历灰色节点检查对象的引用情况)、GCSsweepstring(字符串回收阶段),GCSsweep(回收阶段,用于对除了字符串之外的所有其他数据类型进行回收)和GCSfinalize(终止阶段)。
在gcsweep
阶段会对执行freeobj
来对各种类型的数据进行回收,对于userdata
会直接luaM_freemem(L, o, sizeudata(gco2u(o)));
来回收相应的内存。
随后在gcsfinalize
阶段,会遍历所有待finalize
的userdata
,对于每一个userdata
,调用GCTM
方法尝试调用其注册的__gc
方法。
在这个阶段如果不做修改,luavm
是无法正确释放wasm
中的内存的,这里比较合适的方案就是在GCTM
这个阶段,执行完__gc
方法后,对于要finalize
的对象,找到其对应的wasm
地址,进行释放。