首页 > 其他分享 >xlua 原理浅析(Wrap文件形式)

xlua 原理浅析(Wrap文件形式)

时间:2024-07-01 22:43:27浏览次数:1  
标签:__ xLuaWrapTest obj xlua lua CS Wrap type 浅析

对xlua CSharpCallLua 和 LuaCallCSharp 以Wrap文件注册形式的大致流程梳理。
废话不多说,我们要提出两个问题

  1. C#是如何调用lua的
  2. lua是如何调用C#的

前置知识

资料

  1. lua参考手册 https://cloudwu.github.io/lua53doc/manual.html
  2. xlua https://github.com/Tencent/xLua xlua源码在build目录

以下内容参考上面的资料,只做对理解本文必要的概念的简单说明。

  1. C API 也就是宿主程序跟 Lua 通讯用的一组 C 函数。 所有的 API 函数按相关的类型以及常量都声明在头文件 lua.h 中。
  2. 栈 Lua 使用一个 虚拟栈 来和 C 互传值。 栈上的的每个元素都是一个 Lua 值 (nil,数字,字符串,等等)。顶部位置为n,底部位置为1。为了方便,也可以传-1值获取栈顶,和传n是一样的。同理传-n可以获取栈底。
  3. C Funtion 为了和lua通过栈通讯,不再是普通的传参和返回值,return的是实际返回值的个数。实际的返回值按顺序压入栈。

原文:

为了正确的和 Lua 通讯, C 函数必须使用下列协议。 这个协议定义了参数以及返回值传递方法: C 函数通过 Lua 中的栈来接受参数, 参数以正序入栈(第一个参数首先入栈)。 因此,当函数开始的时候, lua_gettop(L) 可以返回函数收到的参数个数。 第一个参数(如果有的话)在索引 1 的地方, 而最后一个参数在索引 lua_gettop(L) 处。 当需要向 Lua 返回值的时候, C 函数只需要把它们以正序压到堆栈上(第一个返回值最先压入), 然后返回这些返回值的个数。 在这些返回值之下的,堆栈上的东西都会被 Lua 丢掉。 和 Lua 函数一样,从 Lua 中调用 C 函数也可以有很多返回值。

ps: lua_gettop(L) 获取栈顶所在的位置n。

调用一个 C funtion时栈内的结构是这样的
...
cfuntion
arg1
arg2 ——》lua_gettop(L)

CSharpCallLua

我们先看C#是如何调用lua的,下面是一段简单的C#代码

LuaEnv luaenv = new LuaEnv(); //xlua
luaenv.DoString("print(123)");

看下DoString做了啥

 public object[] DoString(byte[] chunk, string chunkName = "chunk", LuaTable env = null)
{
#if THREAD_SAFE || HOTFIX_ENABLE
	lock (luaEnvLock)
	{
#endif
		var _L = L;
		int oldTop = LuaAPI.lua_gettop(_L); //获取栈顶index
		int errFunc = LuaAPI.load_error_func(_L, errorFuncRef); //获取xlua在注册表中的errFunc,然后压栈
		if (LuaAPI.xluaL_loadbuffer(_L, chunk, chunk.Length, chunkName) == 0) //xluaL_loadbuffer内部调用lua_load
		{
			if (env != null) // 多env需要考虑,一般为空
			{
				env.push(_L);
				LuaAPI.lua_setfenv(_L, -2);
			}

			if (LuaAPI.lua_pcall(_L, 0, -1, errFunc) == 0) //安全模式调用,没有错误返回true,把函数函数返回值压栈,否则压入错误信息。
			{
				LuaAPI.lua_remove(_L, errFunc);
				return translator.popValues(_L, oldTop);
			}
		}
		else
			ThrowExceptionFromError(oldTop);

		return null;
#if THREAD_SAFE || HOTFIX_ENABLE
	}
#endif
}

重点是xluaL_loadbuffer,chunk 是传入的转成byte数组的字符串,内部调用lua_load

lua_load: 加载一段 Lua 代码块,但不运行它。 如果没有错误, lua_load 把一个编译好的代码块作为一个 Lua 函数压到栈顶。 否则,压入错误消息。

这样,一块字符串代码块就转换成了可执行的函数,这个函数被压入栈顶。然后我们调用lua_pcall 执行这个函数,若执行成功没有错误且这段代码有返回值,新的栈顶位置会移动,translator.popValues内部通过比较newTop和传入的oldTop判断有几个返回值,然后获取这些值转换到C#层。

当然我们的代码只是简单执行了print(123)没有返回值。

xlua中C#调用lua都依赖DOString接口,以文件形式加载lua就写 DoString("require 'byfile'") 一个道理。

luaCallCSharp

C#里定义好了一个类

public class xLuaWrapTest
{
	public static void StaticFunction()
	{
		Debug.Log("Call StaticFunction");

	}
	public void Function()
	{
		Debug.Log("Call Funtion");
	}
}

这是lua内的调用

local a = CS.xLuaWrapTest(); //已经生成xLuaWrapTestWrap
a:Function()

先来分解这两行代码

local a --1. 声明一个local变量
CS.xLuaWrapTest --2. CS中索引xLuaWrapTest类
xLuaWrapTest() --3. 调用构造函数
a = xLuaWrapTest() --4. 赋值

a:Function --5. 索引Function函数
Function() --6. 调用Function函数

先看第一行local a = CS.xLuaWrapTest(); ,其中两个过程涉及lua端调用C#

  1. CS.xLuaWrapTest
  2. xLuaWrapTest()

CS.xLuaWrapTest and xLuaWrapTest()

首先lua中_G要有CS这个table,这个过程在LuaEnv初始化时通过DoString一段lua字符串实现,下面捡出重点部分

local metatable = {}
local rawget = rawget
local setmetatable = setmetatable
local import_type = xlua.import_type

function metatable:__index(key)
	local fqn = rawget(self,'.fqn')
	fqn = ((fqn and fqn .. '.') or '') .. key

	local obj = import_type(fqn) //import_type 是一个 C funtion, 对应StaticLuaCallbacks.ImportType

	if obj == nil then
		-- It might be an assembly, so we load it too.
		obj = { ['.fqn'] = fqn }
		setmetatable(obj, metatable)
	elseif obj == true then
		return rawget(self, key)
	end

	-- Cache this lookup
	rawset(self, key, obj)
	return obj
end
function metatable:__newindex()
    error('No such type: ' .. rawget(self,'.fqn'), 2)
end
function metatable:__call(...)
...
end

CS = CS or {}
setmetatable(CS, metatable) 
...

CS 的元表实现了__index方法
CS.xLuaWrapTest 这个过程把 “xLuaWrapTest”作为参数传入__index方法,返回obj,只要不为空,说明我们拿到了xLuaWrapTest这个类在lua端的注册

xlua.import_type 对应 StaticLuaCallbacks.ImportType,在 CS.xLuaWrapTest 索引过程中,从这里转向C#

public static int ImportType(RealStatePtr L)
{
    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        string className = LuaAPI.lua_tostring(L, 1);
        Type type = translator.FindType(className);
        if (type != null)
        {
            if (translator.GetTypeId(L, type) >= 0)
            {
                 LuaAPI.lua_pushboolean(L, true);
            }
            else
            {
                return LuaAPI.luaL_error(L, "can not load type " + type);
            }
        }
        else
        {
            LuaAPI.lua_pushnil(L);
        }
        return 1;
    }
    catch (System.Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in xlua.import_type:" + e);
    }
}

public int GetTypeId(RealStatePtr L, Type type)
代码太长就把不放了,简单讲下。

translator内部有有一个<Type,typeId>的cache,调用只有第一次会进行注册工作,其他时候直接返回,因为需要的type已经注册在lua端了

介绍一下注册表这个概念!!!
任何可以接受有效索引的函数同时也接受 伪索引。 伪索引指代一些可以被 C code 访问得到 Lua 值,而它们又不在栈内。 这用于访问注册表以及 C 函数的上值(参见 §4.4)。
Lua 提供了一个 注册表, 这是一个预定义出来的表, 可以用来保存任何 C 代码想保存的 Lua 值。LUA_REGISTRYINDEX这个伪索引用于访问注册表,用户可以自定义键值对放在这个表中

如果第一次索引,CS的table中是没有xLuaWrapTest的calsstable的,注册表中也没有对应xLuaWrapTest的objmetatable(calsstable和objmetatable是两张表,后面会提到),所以第一次会调用TryDelayWrapLoader(xlua的延迟加载,只有lua端第一次调用时才会注册C#端的类到lua端)

TryDelayWrapLoader
会调用生成的Wrap文件中的__Register静态方法,这个方法可以分为两个过程

public static void __Register(RealStatePtr L)
{
	ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
	System.Type type = typeof(xLuaWrapTest);
	Utils.BeginObjectRegister(type, L, translator, 0, 1, 0, 0);

	Utils.RegisterFunc(L, Utils.METHOD_IDX, "Function", _m_Function);

	Utils.EndObjectRegister(type, L, translator, null, null,
							null, null, null);

	Utils.BeginClassRegister(type, L, __CreateInstance, 1, 0, 0);

	Utils.RegisterFunc(L, Utils.CLS_IDX, "StaticFunction", _m_StaticFunction_xlua_st_);
	Utils.EndClassRegister(type, L, translator);
}

第一个过程

BeginObjectRegister 
RegisterFunc
EndObjectRegister

一句话说就是为该类xluaWrapTest的实例对象生成一个metatable。用于在lua中索引C#对象的方法,getter等,不一句话就是额,看下面

  1. BeginObjectRegister 生成以下几张表依次压栈,其中metatable在注册表中以[typeNameStr] = metable 的形式注册
    metatable : {__name = type.FullName} 放入注册表
    methods 如果有,new一张新表压栈,否则压入nil
    getter 同上
    setter 同上

  2. RegisterFunc 做了这几件事

    1. GetFunctionPointerForDelegate 将C# delegate 转为 IntPtr 指向原函数fn
    2. xlua_push_csharp_function 将fn压栈,然后生成一个原函数fn的闭包,称为 fnWrap
    3. methods[函数名字符串] = fnWrap

public static IntPtr GetFunctionPointerForDelegate (TDelegate d); 将一个托管代码中的C#委托转换为可以在非托管代码中使用的函数指针

  1. EndObjectRegister 继续填充metatable的字段
metatable.__index = obj_indexer // 通过gen_obj_indexer生成
metatable.__newindex = obj_newindexer //通过gen_obj_newindexer生成

obj_indexer是一个闭包,把前面生成的methods,getters和一些其他的参数封进去,这个函数就变成了该类的专有,可以通过传参调用拿到该类专有的方法

gen_obj_indexer 生成obj_indexer的函数,有7个上值,两个参数

//upvalue --- [1]: methods, [2]:getters, [3]:csindexer, [4]:base, [5]:indexfuncs, [6]:arrayindexer, [7]:baseindex
//param --- [1]: obj, [2]: key
传入的key即是 在lua中 a.b 时的b(同a[b])
索引顺序是先看methods表里有没有,有就直接返回,然后看getters,一个个的查找key值
具体顺序看源码

gen_obj_newindexer 作为metatable中__newindex 的值
和indexer同理,只不过这个是索引赋值。

总结:
上面这部分在注册表中注册了一个key为xLuaWrapTest(类名),值为这个xLuaWrapTest类对应的metatable, 给它起个名叫objmetatable吧, 其中有2个元方法
__index和__newindex,对应分别是索引操作,和索引赋值操作

obj_newindexer同理只不过换成了赋值操作

objmetatable这个表用于实例对象查找各个实例方法(methods,getters),和为对象字段赋值,因为在注册表中,可以随时通过下面的方式拿到

lua_pushstring(typeNameStr);
lua_rawget(L, LUA_REGISTRYINDEX);
// or
lua_rawgeti(L, LUA_REGISTRYINDEX, index); //index由luaL_ref生成

第二个过程

BeginClassRegister 
RegisterFunc
EndClassRegister

BeginClassRegister
生成4个table,依次压栈

classTable = {
    UnderlyingSystemType = ...
}
metaTable = {
    __call  = creator
} 
static_getter_table = {}
static_setter_table = {} 

static_getter_table和static_setter_table由后面的RegisterFunc填充, creator 在Wrap文件中有定义

BeginClassRegister
中调用 SetCSTable 进行 CS["xLuaWrapTest"] = classTable的操作,即把xLuaWrapTest这个类的相关信息注册到了lua环境,这就是CS.xLuaWrapTest索引过程得以实现的原理

最后执行 setmetatable(classTable, metaTable)
即xluaWrapTest() 对应调用的函数为creator

RegisterFunc 和之前的同理,不过这里是类方法

EndClassRegister
做的操作和EndObjectRegister类似,在metaTable的基础上添加

metaTable.__index = cls_indexer
metaTable.__newindex = cls_newindexer

只不过这里对应的是类不是对象

总结下,__Register执行完毕,最后我们的这个类xluaWrapTest,在lua生成了这么个结构

--在注册表中
registerTable["xluaWrapTest"] = objmetatable
objmetatable = {
    __index = obj_indexer
    __newindex = obj_newindexer
}
--在CS中
CS["xluaWrapTest"] = classtable
classtable = {
   UnderlyingSystemType = ...
}
classtable.__metatable =  {
    __index = cls_indexer
    __newindex = cls_newindexer
    __call = creator
}

xLuaWrapTest这个类在lua所需的数据注册完毕,xluaWrapTest的classtable放进了CS,函数逐级返回,我们就拿到了对应xluaWrapTest这个类的classtable,接着xluaWrapTest() 自然触发classtable 元表中的 __call元方法, 构造并返回一个xluaWrapTest对象

a:Function()

剩下就是 a:Function()了
先看a:Function
前面说了xluaWrapTest()时会调用一个函数creator,对应Wrap文件中的__CreateInstance,提取出其中的主要部分

var gen_ret = new xLuaWrapTest();
translator.Push(L, gen_ret);

translator.Push(RealStatePtr L, object o) 之前也遇到过,这个函数把o添加到一个objects的列表里,即为每个对象设立了全局唯一的一个index,后面就可以拿着这个index快速找到o。最后调用xlua_pushcsobj,把o压入栈中。
我们可以看看这个函数的实现。

LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) { // gettop = * pointer 
    int* pointer = (int*)lua_newuserdata(L, sizeof(int)); 
    *pointer = key;
    // *pointer.__metatable = lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
    if (need_cache) cacheud(L, key, cache_ref);

    lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);

    lua_setmetatable(L, -2);
}

在xlua的实现里,o在lua环境是一个设置了元表的userdata,这个userdata实际上是一块存着o的index的内存块。
设置的元表就是我们之前提过的objMetaTable
所以在调用o的方法时,会触发__index元方法的obj_indexer

再回过来看a:Function这个过程,obj_indexer接受“Function”字符串作为参数,返回Funtion的引用到lua。

最后Function()的调用过程就没什么可说的了,对应的就是C#层普通的调用。

标签:__,xLuaWrapTest,obj,xlua,lua,CS,Wrap,type,浅析
From: https://www.cnblogs.com/micro-universe/p/18278627

相关文章

  • ThreadLocal 源码浅析
    前言多线程在访问同一个共享变量时很可能会出现并发问题,特别是在多线程对共享变量进行写入时,那么除了加锁还有其他方法避免并发问题吗?本文将详细讲解ThreadLocal的使用及其源码。一、什么是ThreadLocal?ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是说,如果你创建......
  • ThreadLocal 源码浅析
    前言多线程在访问同一个共享变量时很可能会出现并发问题,特别是在多线程对共享变量进入写入时,那么除了加锁还有其他方法避免并发问题吗?本文将详细讲解ThreadLocal的使用及其源码。一、什么是ThreadLocal?ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是说,如果你......
  • Unity 小游戏转换(一)—— WebGL+XLua导出
    转载或者引用本文内容请注明来源及原作者一、前言小游戏的红海赛道,给游戏市场带来了新的活力。小游戏依托微信、抖音等第三方平台,因为买量成本较低、开箱既玩的特性,使得许多开发厂商开始布局小游戏平台。同时Unity引擎也花费了大量的精力(团结引擎),慢慢更改开发者对于Unity庞大......
  • 栈帧浅析,堆栈漏洞概述——【太原理工大学软件安全期末补充】
    在上一篇文章中我说实验一不重要,确实没必要完全按照实验内容逐字逐句理解,但是这里我们补充一个知识点栈帧(StackFrame)是计算机程序执行过程中,调用栈(CallStack)中的一个单元,它包含了函数调用时的上下文信息。每当一个函数被调用时,一个新的栈帧就会被创建并被推入调用栈。栈帧......
  • MySQL bit类型增加索引后查询结果不正确案例浅析
    昨天同事遇到的一个案例,这里简单描述一下:一个表里面有一个bit类型的字段,同事在优化相关SQL的过程中,给这个表的bit类型的字段新增了一个索引,然后测试验证时,居然发现SQL语句执行结果跟不加索引不一样。加了索引后,SQL语句没有查询出一条记录,删除索引后,SQL语句就能查询出几十条记录。......
  • 浅析Vite本地构建原理
    前言随着Vue3的逐渐普及以及Vite的逐渐成熟,我们有必要来了解一下关于vite的本地构建原理。对于webpack打包的核心流程是通过分析JS文件中引用关系,通过递归得到整个项目的依赖关系,并且对于非JS类型的资源,通过调用对应的loader将其打包编译生成JS代码,最后再启动开发服务器。了解......
  • 浅析Mybatis拦截器
    一、背景最近针对项目中出现的慢sql,我们使用自定义Mybatis拦截器,结合DUCC动态配置慢sql阈值,来监控慢sql并报警,提前发现风险点。借着这个契机,浅析下Mybatis拦截器原理,个人理解,不足之处请指正。二、Mybatis拦截器Mybatis使用plugin来拦截方法调用,所以MyBatisplugin也称为:Mybatis......
  • SSH配置、跨主机上传下载、Wrapper访问控制实验操作步骤
    目录终端OpenSSH服务器SSH(SecureShell)协议OpenSSH服务监听选项SSH配置修改端口号用户登录控制指定用户登录1.2.严格模式最大会话数量公钥验证使用公钥认证让客户端登录系统域名解析跨主机下载、上传文件下载指定端口下载上传指定端口上传 ​编辑sftp功......
  • CV_WRAP和CV_EXPORTS_W
    CV_EXPORTS_Wisdefinedinmodules/core/include/opencv2/core/types_c.hasaliasforCV_EXPORTS,CV_EXPORTSisdefinedas:#if(definedWIN32||defined_WIN32||definedWINCE)&&definedCVAPI_EXPORTS#defineCV_EXPORTS__declspec(dllexport)#el......
  • 数据库的读现象浅析
    “读现象”是多个事务并发执行时,在读取数据方面可能碰到的状况。先了解它们有助于理解各隔离级别的含义。其中包括脏读、不可重复读和幻读。脏读脏读又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2......