看懂Xlua实现原理——从宏观到微观(1)传递c#对象到Lua

本文主要探讨了在Unity开发中,如何利用XLua框架将C#对象传递到Lua,以实现热更新功能。文章详细讲解了LowLevelAPI和HighLevelAPI,包括传递基元类型、对象、函数等,特别强调了对象的生命周期管理、元表和缓存机制。XLua通过优化的API和元表构造器,提供了高效、灵活的C#与Lua交互方式。
摘要由CSDN通过智能技术生成


《知乎专栏》

我们要解决什么问题?

为了使基于unity开发的应用在移动平台能够热更新,我们嵌入了Lua虚拟机,将需要热更新的逻辑用lua实现。c#通过P/Invoke和lua交互 (lua由ANSI C实现)。在这个过程中,由于数据的交换需要使用lua提供的虚拟栈,不够简单高效,为了解决这个问题,我们引入了*lua框架(xluasluaulua)来达到类似RPC式的函数调用类原生对象式的对象访问以及高效的对象传递

业务中,有以下几种场景:

  1. c#对Lua方法的调用
  2. Lua对c#方法的调用
  3. Lua持有一个c#对象
  4. c#持有一个Lua对象

通过对场景的归纳,我们发现,最终其实是两个需求:

  1. 传递一个C#对象给Lua
  2. 传递一个lua对象给c#

这里我们把函数调用归纳为“传递”函数对象,因为只要我们能够把函数“传递”过去,就能完成对函数的调用。

传递是双向的(pull/push),但同时我们又可以把get一个对象理解为对方push一个返回值给我们。

c#对象传递到lua

首先我们要知道的是,lua本身提供了C_API,让我们push一个值到lua虚拟栈上。lua可以通过访问lua虚拟栈,来访问这个对象。

lua_pushnil、lua_pushnumber、lua_pushinteger、lua_pushstring、lua_pushcclosure、lua_pushboolean、lua_pushlightuserdata、lua_pushthread等等。

Lua虚拟栈是lua和其他语言交换数据的中介。

xlua对以上接口进行了封装,并同样提供了一系列的push方法,让我们可以把一个c#对象push到lua的虚拟栈上。

可以把xlua的push API归为两类:一类是针对某种特定类型的push,暂且叫做LowLevelAPI;还有一类是基于LowLevelAPI封装的更上层的HighLevelAPI

门面模式

使用HighLevelAPI时你只要简单的传入你想push的对象,HighLevelAPI会帮你找到最适合的LowLevelAPI调用,因为就算同一种类型的push方法,也可能有用户自定义的优化版本。而对于LowLevelAPI最终是需要调用xlua.dll中提供的C API来协调完成最终的工作。

#LowLevelAPI#

//using RealStatePtr = System.IntPtr;
//using LuaCSFunction = XLua.LuaDLL.lua_CSFunction;
//typedef int (*lua_CFunction) (lua_State *L);

//ObjectTranslator.cs
void pushPrimitive(RealStatePtr L, object o)
public void Push(RealStatePtr L, object o)
public void PushObject(RealStatePtr L, object o, int type_id)
public void Push(RealStatePtr L, LuaCSFunction o)
internal void PushFixCSFunction(RealStatePtr L, LuaCSFunction func)
public void Push(RealStatePtr L, LuaBase o)
public void PushDecimal(RealStatePtr L, decimal val)

传递基元类型

void pushPrimitive(RealStatePtr L, object o)

基元类型为 Boolean、Byte、SByte、Int16、UInt16、Int32、UInt32、Int64、UInt64、UIntPtr、Char、Double、Single和IntPtr (对应void*)*。

对于C#中的基元类型,大部分可以直接对应的lua中的类型,并使用对应的luaAPI进行push:

//push一个int
LUA_API void xlua_pushinteger (lua_State *L, int n)
//push一个double
#define LUA_NUMBER	double
typedef LUA_NUMBER lua_Number;
LUA_API void lua_pushnumber (lua_State *L, lua_Number n)
//push一个IntPtr
LUA_API void lua_pushlightuserdata (lua_State *L, void *p)

而有些需要在lua中定义对应的类型,比如对于long,xlua中定义了一个Integer64与之对应,以及相应的操作接口:

//i64lib.c
//在lua中表示c#中的long
typedef struct {
   
	int fake_id;
	int8_t type;
    union {
   
		int64_t i64;
		uint64_t u64;
	} data;
} Integer64;

注意pushPrimitive会产生装箱拆箱的GC,所以不推荐使用。事实上xlua也针对基元类型做了优化,真实环境中不会用到这个方法。

传递 object

public void Push(RealStatePtr L, object o)
public void PushObject(RealStatePtr L, object o, int type_id)
索引

不管object是什么类型,最终的push都是使用:

//xlua.c
/*
key:传递的对象
meta_ref:对象所属类型的元表的索引
need_cache:此对象是否需要在lua缓存
cache_ref:缓存表的伪索引
*/
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
   
	int* pointer = (int*)lua_newuserdata(L, sizeof(int));
	*pointer = key;
	if (need_cache) cacheud(L, key, cache_ref);//R.cache_ref[Key] = pointer
    lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
	lua_setmetatable(L, -2);//setmetatable(Key,R[meta_ref])
}

为什么我们传给lua的对象是一个int类型(这里的key)?其实我们这里的key是我们要传递的c#对象的一个索引,我们可以通过这个索引找到这个c#对象。

当传递一个c#对象的时候,我们创建一个userdate,并把这个索引值赋给这个userdata。然后,lua在全局注册表中,有一张专门的表用来存放c#各种类型所对应的元表,而meta_ref就是当前这个对象所对应类型的元表的索引id,我们通过他找到对应的元表,就可以通过setmetatable来绑定操作这个对象的方法。最终lua就可以愉快的使用这个对象。

每种类型所对应的元表,是我们在push一种类型的对象之前,提前注册进来的,后面详述。

但是对于引用类型的对象,其生命周期是有可能超出当前的调用栈的 (比如lua用一个变量引用了这个对象) 。这时,我们就不仅要能够通过这个key找到c#原始对象,还要通过这个key能够找到对应的lua代理对象。因此,对于引用类型,我们在lua中同样也要建立一套索引机制,这就是need_cachecache_ref的作用:

static void cacheud(lua_State *L, int key, int cache_ref) {
   
	lua_rawgeti(L, LUA_REGISTRYINDEX, cache_ref);
	lua_pushvalue(L, -2);
	lua_rawseti(L, -2, key);
	lua_pop(L, 1);
}
缓存

再回过头来看看c#中的索引和缓存机制:
在调用xlua_pushcsobj之前,所有object都会被放入一个对象的缓存池中ObjectTranslator.objects。而我们得到的key就是这个对象在缓存池中的下标。

//以下是经过删减的伪代码,只保留我们现在需要关注的流程
public void Push(RealStatePtr L, object o)
{
   
    if (o == null)
    {
   
        LuaAPI.lua_pushnil(L);
        return;
    }
    int index = -1;
    Type type = o.GetType();
	bool is_enum = type.IsEnum;
    bool is_valuetype = type.IsValueType;
    bool needcache = !is_valuetype || is_enum;
    //如果是引用类型(或者是enum),可能已经缓存在lua,所以先看看是不是在lua缓存中
    if (needcache && (is_enum ? enumMap.TryGetValue(o, out index) : reverseMap.TryGetValue(o, out index)))
    {
   
        //如果是已经push到lua的对象,从lua的c#对象缓存中获取这个对象
        if (LuaAPI.xlua_tryget_cachedud(L, index, cacheRef) == 1)
        {
   
       		//==1表示获取lua缓存成功(并且已经在栈顶,所以我们直接退出)
            return;
        }
    }
    bool is_first;
    //getTypeId这个函数的设计有点丑。职责有点多还和外部调用者耦合。吐槽下。(后面详述)
    int type_id = getTypeId(L, type, out is_first);

    
  • 11
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值