干货:xlua 是怎么和C#通信的?(一)
在回答这个问题前我默认你已经用过xlua进行平时的开发了,只是想通过这篇文章搞懂底层原理。如果还没用过xlua的,可以去下面的链接去下载:https://github.com/Tencent/xLua/tree/master/build
下面进入正题:
一、C#是怎么调用到lua端的接口的?
这个接口是我们项目中用到的lua接口,那么C#是怎么调用到lua层的这个接口呢?
function OnLoginComplete()
--登录完成会调用此接口 ,lua这边要做一些处理
print("C# call lua ok!")
end
搞明白这个问题,我们先要搞懂几个类LuaEnv,LuaFunction,LuaTable,LuaBase
LuaEnv 是整个lua的上下文,游戏一启动就会创建这个对象并且做一些lua和C#之间的初始化工作,同时LuaFunction,LuaTable也会持有这个对象,整个C#调用lua相关代码都和它有关系,非常重要。
LuaFunction 顾名思义这是和函数相关的类,通过这个类就可以去操作lua的方法,一会我们就会通过这个类去调用”OnLoginComplete“方法
LuaTable 顾名思义这是和table表相关的类,通过这个类就可以去操作lua的表,设置属性值,获取属性值等
LuaBase 是LuaFunction 和 LuaTable 的基类,同时持有了LuaEnv对象。
通过几句话的介绍我们很难去真正理解它,直接上代码
LuaFunction func = env.Global.Get<LuaFunction>(&#34;OnLoginComplete&#34;);env是LuaEnv对象,Global是LuaTable对象,它是lua层的全局表,通过这个类接口就可以获取到”OnLoginComplete“ 函数然后调用func.Call 方法就会打印出”C# call lua ok!“
同样的你也可以通过类似的方法去获取到lua层的其他数据。比如table表,string类型的值等等。
这里我们只是简单的介绍下C#调用lua接口的流程,并不会花大篇幅去介绍它,如果想了解更多的同学可以点击下面的链接了解更多。
xLua/XLua教程.md at master · Tencent/xLua
二、下面我们再深入些,看看LuaTable 和 LuaFunction 为什么能调用到lua中方法?
打开LuaTable 的 Get 方法我们看一看
public void Get<TKey, TValue>(TKey key, out TValue value)
{
#if THREAD_SAFE || HOTFIX_ENABLE
lock (luaEnv.luaEnvLock)
{
#endif
var L = luaEnv.L;
var translator = luaEnv.translator;
int oldTop = LuaAPI.lua_gettop(L);
LuaAPI.lua_getref(L, luaReference);
translator.PushByType(L, key);
if (0 != LuaAPI.xlua_pgettable(L, -2))
{
string err = LuaAPI.lua_tostring(L, -1);
LuaAPI.lua_settop(L, oldTop);
throw new Exception(&#34;get field [&#34; + key + &#34;] error:&#34; + err);
}
LuaTypes lua_type = LuaAPI.lua_type(L, -1);
Type type_of_value = typeof(TValue);
if (lua_type == LuaTypes.LUA_TNIL && type_of_value.IsValueType())
{
throw new InvalidCastException(&#34;can not assign nil to &#34; + type_of_value.GetFriendlyName());
}
try
{
translator.Get(L, -1, out value);
}
catch (Exception e)
{
throw e;
}
finally
{
LuaAPI.lua_settop(L, oldTop);
}
#if THREAD_SAFE || HOTFIX_ENABLE
}
#endif
}
然后我们在打开LuaFunction 的 Call方法 看一看
public object[] Call(object[] args, Type[] returnTypes)
{
#if THREAD_SAFE || HOTFIX_ENABLE
lock (luaEnv.luaEnvLock)
{
#endif
int nArgs = 0;
var L = luaEnv.L;
var translator = luaEnv.translator;
int oldTop = LuaAPI.lua_gettop(L);
int errFunc = LuaAPI.load_error_func(L, luaEnv.errorFuncRef);
LuaAPI.lua_getref(L, luaReference);
if (args != null)
{
nArgs = args.Length;
for (int i = 0; i < args.Length; i++)
{
translator.PushAny(L, args);
}
}
int error = LuaAPI.lua_pcall(L, nArgs, -1, errFunc);
if (error != 0)
luaEnv.ThrowExceptionFromError(oldTop);
LuaAPI.lua_remove(L, errFunc);
if (returnTypes != null)
return translator.popValues(L, oldTop, returnTypes);
else
return translator.popValues(L, oldTop);
#if THREAD_SAFE || HOTFIX_ENABLE
}
#endif
}通过这两段代码我们看到,他们都用到了两个类ObjectTranslator 和LuaAPI
ObjectTranslator 这个类我们先不表它,让我们看看LuaAPI 是个什么东西?打开类文件我们看下。跳到了LuaDll文件。
上图我们看到这个类都是调用了外部类接口。这个类只是做了一些接口的声明和封装。
很多小伙伴到这一步就不会再往底层跟下去了,其实这个类是通过关键字extern 和 DllImport标签和外部lua原生层通信的。再往深研究我们就得看源码了,那源码在哪呢?还记得我们一开始让下载的xlua源码吗,就在里面我们看下,打开XluaTest ->build --> xlua.c 文件,我们看到这里面各个方法的定义和LuaDLL文件中的方法一模一样。
xlua.c 是对lua原生代码的封装,再往里跟就是我们的lua原生C代码了。那原生C代码在哪呢?
三、lua源代码结构
就在同级目录下,这个目录下有很多版本,还有luajit(这个不展开讲), 我们打开lua-5.3.5看下
lua源代码都放在src文件夹下,代码不多,一共60多个文件,这就是lua原生代码的全部,是不很小巧。从这里我们可以看到原生代码包括了几个功能,词法分析,语法分析,语义分析,虚拟机,GC等。是不麻雀虽小五脏俱全,源码分为四部分,
第一部分:虚拟机运转的核心功能
lapi.c C语言接口
lctype.c C标准库中ctype相关实现
ldebug.c debug接口
ldo.c 函数调用以及栈管理
lfunc.c 函数原型及闭包管理
lgc.c 垃圾回收
lmem.c 内存管理接口
lobject.c对象操作的一些函数
lopcodes.c 虚拟机的字节码定义
lstate.c 全局状态机
lstring.c字符串池
ltable.c 表类型的相关操作
ltm.c 元方法
lvm.c 虚拟机
lzio.c 输入流接口
lua核心部分仅包括lua虚拟机的运转。
(1)lopcodes.c控制了lua虚拟机的行为
(2)lvm.c定义了虚拟机对opcode的解析和运作,API以luaV为前缀
(3)lstate.c,lua虚拟机的外在数据形式是一个lua_State结构体,描述了lua虚拟机当前的状态,
全局State引用了整个虚拟机的所有数据,API以luaE为前缀。
(4)ldo.c定义了函数的调用即返回,相关API以luaD为前缀
(5)lua最复杂也最重要的三种数据类型是function、table、string,他们的实现定义在lfunc.c、ltable.c、lstring.c
中,三组内部API分别以luaF、luaH、luaS为前缀命名,不同的数据类型被统一定义在lobject.c中,API以luaO为前缀。
(6)ltm.c定义了元表的处理,API以luaT为前缀
(7)核心系统还用到了两个基础设备:内存管理lmem.c,API以luaM为前缀;带缓冲的流处理lzio.c,API以luaZ为前缀。
(8)lgc.c定义了垃圾回收机制,也是核心系统里最为复杂的部分,API以luaC为前缀。
(9)lapi.c, lua是一门嵌入式语言,可以和宿主机系统相结合。所以必须提供和宿主机系统交互的API,
这些API以C函数的形式提供,在lapi.c中实现,API直接以lua为前缀。
第二部分:源代码解析及预编译字节码
lcode.c 代码生成器
ldump.c 序列化预编译的lua字节码
llex.c 词法分析器
lparser.c解析器
lundump.c还原预编译的字节码
只有核心代码和一个虚拟机还不能让lua程序运行起来,还需要有外部输入经过解析才可以运行起来。
(1)lparser.c、llex.c,外部输入的文本程序代码需要经过解析得到内部的数据结构(常量opcode的集合),
这个过程通过lparser.c完成,API前缀为luaY,这个过程还需要通过词法分析llex.c完成,API前缀为luaX。
(2)lcode.c,解析完外部文本代码,还需要最终生成虚拟机理解的数据,这个步骤在lcode.c中完成,API前缀为luaK。
(3)ldump.c、lundump.c,为了加快代码翻译的流程,可以采用预编译的过程,把运行时编译的结果生成字节码,
这个过程以及逆过程由ldump.c和lundump.c完成,API前缀为luaU。
所以当我们开发完lua代码后实际的流程是这样的,lua文件加载 ->词法分析->语法分析->语义分析->虚拟机执行我们的逻辑。
第三部分:内嵌库
lauxlib.c 库编写用到的辅助函数库
lbaselib.c 基础库
lbitlib.c 位操作库
lcorolib.c 协程库
ldblib.c debug库
linit.c 内嵌库的初始化
liolib.c IO库
lmathlib.c 数学库
loadlib.c 动态扩展库管理
loslib.c OS库
lstrlib.c 字符串库
ltablib.c 表处理库
嵌入式语言可以不提供库及函数,全部由宿主系统注入到State中即可,但是lua官方版本提供了少量的库,
比如一些基础函数,如pairs、error、setmetatable、type等。lua允许自由加载需要的部分,以控制最终
执行文件的体积和内存的占用量。
(1)lualib.h,主动加载这些内嵌库进入lua_State是在lualib.h中的API实现。
(2)loadlib.c、limit.c:在lua 5.0之前,lua并没有统一的模块管理机制,因为早期lua定位是嵌入式语言,后来人们
倾向于把lua作为一门独立的编程语言来使用,那么统一的模块化管理就变得很由必要,这样才能让丰富的第三方库可以
协同工作。在lua 5.1引入了官方推介的模块管理机制,require、module,并允许从C语言编写的动态库中加载模块,
在lua 5.2做了简化。我们可以在loadlib.c中找到实现,内嵌库的初始化API则在limit.c中找到。
(3)lib.c,其他的基础库在lib.c为后缀的文件中实现。
第四部分:可执行的解析器,字节码编译器
lua.c解释器
luac.c 字节码编译器
早期lua主要在嵌入式系统中使用,所以源代码通常被编译成动态库或静态库被宿主机系统加载或链接,
但随着lua第三方库越来越丰富,人们开始把lua作为一门独立的语言来使用,lua的官方版本里提供了
一个单独的解析器,在lua.c中实现,luac.c实现了一个简单的字节码编译器,可以预编译文本的lua源程序。
四、总结
知道了lua原生代码的整个架构,回过头我们再看下LuaDll文件就知道了我们调用lua层代码到底发生了什么,Xlua是对lua原生代码的封装,通过xlua的类库我们就获取lua层方法然后调用执行我们的逻辑,那lua层想要调用C#层Unity的代码该怎么办呢?
请查看下面的两篇文章
页:
[1]