本文将致力于研究xLua中Lua访问C#源码分析,反射,生成代码的用法
要理解xLua的源码,其实挺难的,需要的必备知识比较多:C#、Lua、反射、LuaC API
一.前言
从官网faq中的信息可以得出lua和C#的交互技术有生成代码和反射两种方式,官方没有对此作详细的解释。本文致力于从源码上分析梳理展开这部分技术。

参考:faq.md
来看这个demo,C#侧的类DerivedClass可以在Lua中当成table来使用:CS.Tutorial.DerivedClass。这个实现不是理所当然的,有大量代码在对其进行实现,下文将探索幕后的内容。

二.Demo
来看一个Demo,所有脚本改动的内容都有对比,见下图。
Demo展示的内容是:通过DoString执行Lua代码时,lua中访问C#时(比如下面的local a,local b local c),是执行了C#代码的
cs
local a = CS.UnityEngine.Vector3
local b = CS.UnityEngine.Vector3.up
local c = CS.UnityEngine.Time.deltaTime
打开场景U3DScripting

LuaTestScript.lua.txt

LuaBehaviour.cs

LuaEnv.cs

ObjectTranslator.cs

运行打印:

在Console中的打印可以看出,在lua中首次访问类时(比如Vector3),会先走LuaEnv中的元方法:
Lua
function metatable:__index(key)
print('***###', key)
local fqn = rawget(self,'.fqn')
fqn = ((fqn and fqn .. '.') or '') .. key
然后从这行进入C#
Lua
local obj = import_type(fqn)
一直执行到ObjectTranslator.TryDelayWrapLoader,这个函数是本文的核心
cs
public bool TryDelayWrapLoader(RealStatePtr L, Type type)
阅读这个方法,可以得出其通过3种方式将C#类本身和类的成员注册到Lua,后文将做进一步分析(知道大致逻辑即可,无需精细研究)
2.1.走CodeEmit类
cs
if (!DelegateBridge.Gen_Flag && !type.IsEnum() && !typeof(Delegate).IsAssignableFrom(type) && Utils.IsPublic(type))
{
UnityEngine.Debug.Log("TryDelayWrapLoader: " + type + " -> CodeEmit");
Type wrap = ce.EmitTypeWrap(type);
MethodInfo method = wrap.GetMethod("__Register", BindingFlags.Static | BindingFlags.Public);
method.Invoke(null, new object[] { L });
}
2.2.走Utils.ReflectionWrap(反射)
cs
UnityEngine.Debug.Log("TryDelayWrapLoader: " + type + " -> ReflectionWrap");
Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
2.3.走生成代码
cs
if (delayWrap.TryGetValue(type, out loader))
{
// 有生成代码:走 XxxWrap.__Register
UnityEngine.Debug.Log("TryDelayWrapLoader: " + type + " -> GenWrap.__Register");
delayWrap.Remove(type);
loader(L);
}
三.Lua访问C#方式一:走CodeEmit类
cs
if (!DelegateBridge.Gen_Flag && !type.IsEnum() && !typeof(Delegate).IsAssignableFrom(type) && Utils.IsPublic(type))
{
Type wrap = ce.EmitTypeWrap(type);
MethodInfo method = wrap.GetMethod("__Register", BindingFlags.Static | BindingFlags.Public);
method.Invoke(null, new object[] { L });
}
把这3行代码做的事放一起说:在内存中生成*Wrap类,然后将类中的成员挂到Lua的表/元表上,从而支持Lua → C# 的调用。
实现上主要在CodeEmit.EmitTypeWrap中,其比较难于理解需要较多System.Reflection.Emit中API的掌握,不然看不懂。这里先不展开,等用到了再深入研究。
cs
public Type EmitTypeWrap(Type toBeWrap)
参考:生成动态方法和程序集
四.Lua访问C#方式二:走 Utils.ReflectionWrap(反射)
cs
Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
cs
public static void ReflectionWrap(RealStatePtr L, Type type, bool privateAccessible)
cs
static void makeReflectionWrap(RealStatePtr L, Type type, int cls_field, int cls_getter, int cls_setter,
int obj_field, int obj_getter, int obj_setter, int obj_meta, out LuaCSFunction item_getter, out LuaCSFunction item_setter, BindingFlags access)
ReflectionWrap中通过反射将type的成员挂到lua侧类名这个表中,核心实现在makeReflectionWrap中。通过调用LuaAPI.lua_rawset会执行类似 t[k] = v 的操作,然后将键和值都从栈中弹出,lua_rawset对栈顶附近的数据布局有严格的要求:
-
表 (Table):位于栈上的有效索引
index处。 -
键 (Key):位于栈顶下方(
-2位置)。 -
值 (Value):位于栈顶(
-1位置)。
五.Lua访问C#方式三:走生成代码
5.1.收集
通过GetGenConfig,扫一遍当前域里几乎所有类型,把配置合并进几个静态列表
在MergeCfg中会收集特性:LuaCallCSharpAttribute
5.2.生成
由Generator.GenOne完成写入文件,由genCode保存文件文本内容,下图是为Vector2类生成UnityEngine_Vector2Wrap.cs的过程

5.3.运行
第一次Lua访问C#类中成员时,由Wrap文件调用RegisterFunc注册,来看一个例子:
LuaTestScript.lua.txt
Lua
function start()
print("lua start...")
local d = CS.XLuaTest.NoGc.a1
end

从调用堆栈可以看出调用链为:LuaBehaviour.Start->LuaTestScript.Start->...->XLuaTestNoGcWrap.__Register->Utils.RegisterFunc进行注册

