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

5bbce207b120832b6a24c2717858ccb2.png

CSDN

我们要解决什么问题?

为了使基于unity开发的应用在移动平台能够热更新,我们嵌入了Lua虚拟机,将需要热更新的逻辑用lua实现。c#通过P/Invoke和lua交互(lua由ANSI C实现)。在这个过程中,由于数据的交换需要使用lua提供的虚拟栈,不够简单高效,为了解决这个问题,我们引入了*lua框架(xlua、slua、ulua)来达到类似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);

    //对于要push到lua的c#对象,进行缓存,并获得索引key
    index = addObject(o, is_valuetype, is_enum);
    
    //xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref)
    LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef);
}

int addObject(object obj, bool is_valuetype, bool is_enum)
{
	//objects是所有push进lua的对象的缓存池
    int index = objects.Add(obj);
    //对于引用(和enum)类型,我们可以反查到id(方便我们快速判断,这个对象是不是已经push到lua)
    if (is_enum)
    {
        enumMap[obj] = index;
    }
    else if (!is_valuetype)
    {
        reverseMap[obj] = index;
    }
	return index;
}

gc

对于引用类型,它的生命周期管理会略微复杂。mono和lua虚拟机有各自的gc系统,并且相互无法感知。当lua和c#同时引用一个对象时,我们需要能够保证对象生命周期的正确,不能一边还在引用,另一边却把它释放掉了。

这个过程是由lua的gc驱动的。我们把对象push到lua时,会缓存在c#的对象池中,所以是不会被mono的gc所释放掉,这样就保证了lua能够安全的持有c#对象。同时我们也会把这个对象的代理缓存到lua中,而lua中对象的缓存表是一个弱表,也就是说,当没有其他的lua引用这个对象时,lua的gc会把这个对象从lua的缓存中回收,而对象被gc回收的过程会触发这个对象的的__gc元方法。

而这个__gc元方法就会通知到c#这端,来告诉我们lua不再使用这个对象,我们可以把它从对象缓存池中移除。当没有其他c#对其的引用时,mono的gc就会正常的回收这个对象。

//__gc元方法:
public static int LuaGC(RealStatePtr L)
{
    try
    {
        int udata = LuaAPI.xlua_tocsobj_safe(L, 1);
        if (udata != -1)
        {
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
            if ( translator != null )
            {
                translator.collectObject(udata);
            }
        }
        return 0;
    }
    catch (Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in LuaGC:" + e);
    }
}

//从缓存池中删除
internal void collectObject(int obj_index_to_collect)
{
	object o;
	
	if (objects.TryGetValue(obj_index_to_collect, out o))
	{
		objects.Remove(obj_index_to_collect);
        
        if (o != null)
        {
            int obj_index;
            //lua gc是先把weak table移除后再调用__gc,这期间同一个对象可能再次push到lua,关联到新的index
            bool is_enum = o.GetType().IsEnum();
            if ((is_enum ? enumMap.TryGetValue(o, out obj_index) : reverseMap.TryGetValue(o, out obj_index))
                && obj_index == obj_index_to_collect)
            {
                if (is_enum)
                {
                    enumMap.Remove(o);
                }
                else
                {
                    reverseMap.Remove(o);
                }
            }
        }
	}
}

元表

对于业务来说,我们只是单纯的把对象的索引传递过去,是远远不够的,我们还需要提供直接使用和操作对象的方法。前面我们提到,在我们把一个对象push到lua之前,我们会把对象类型所对应的元表提前注册到lua之中。这样在我们真正push一个对象时,就会用这个元表来设置操作这个对象的方法。

首先第一个问题就是,如何表示c#对象的类型?回过头来看看我们的Push函数,其中最重要的就是getTypeId

首先会尝试从c#的类型缓存typeIdMap中检查是否已经注册过这种类型,如果没有的话,我们就需要为其生成一个type_id

再从lua的类型缓存中用类型名来检索是否已经注册过这种类型,如果没有的话,意味着我们还没有为这种类型在lua中注册一个元表,继而通过TryDelayWrapLoader来生成这个类型的元表。

//
public void Push(RealStatePtr L, object o)
{
	//...
	Type type = o.GetType();
	bool is_first;
	int type_id = getTypeId(L, type, out is_first);
	//...
}

//这里再次吐槽getTypeId函数的设计和实现,为了保持清楚,我只保留能大体说明逻辑的的代码
internal int getTypeId(RealStatePtr L, Type type, out bool is_first, LOGLEVEL log_level = LOGLEVEL.WARN)
{
	//尝试获取c#中检索
	if (typeIdMap.TryGetValue(type, out type_id)){return;}
	//尝试从lua中检索
	LuaAPI.luaL_getmetatable(L,type.FullName);
	if (LuaAPI.lua_isnil(L, -1)) 
	{
	    LuaAPI.lua_pop(L, 1);
	    //获取类型的元表
	    if (TryDelayWrapLoader(L,  type))
	    {
	        LuaAPI.luaL_getmetatable(L, type.FullName);
	    }
	    else
	    {
	        throw new Exception("Fatal: can not load metatable of type:" + type);
	    }
	}
	//生成新的type_id
	type_id = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);
	//注册到lua
    LuaAPI.lua_pushnumber(L, type_id);
    LuaAPI.xlua_rawseti(L, -2, 1);
    LuaAPI.lua_pop(L, 1);
    
    if (type.IsValueType())
    {
    	typeMap.Add(type_id, type);
    }

    typeIdMap.Add(type, type_id);
}
再次吐槽,表面上getTypeId只是获取一个类型的type_id,但其实上,注册(甚至生成)类型元表和元方法也是在这里完成的!!可能是为了解决循环依赖的问题而破坏了代码的结构?

这其中最重要的就是元表的生成:

用过xlua的应该都知道,xlua是可以通过配置的方式,在编译期帮我们生成优化的元表元方法的 (无gc)。这属于用户自定义的针对某种类型的元表,是高度优化的,所以也是优先级最高的。因此这里首先尝试从delayWrap中查找有没有用户事先注册的自定义的类型元表生成器 (大多数情况下就是通过xlua的Gen工具生成的)。

这里获取到的loader并不是元表,而是元表的构造器。虽然我们提前定义了很多元表构造器,但只有在这个类型第一次用到的时候,才会去构造元表。也就是说,这个过程是惰性的,这也是为什么函数名里有一个Delay的原因吧。

如果没有用户提前注册的自定义元表生成器。接下来是一个很抖机灵的方式,居然内嵌了一个代码生成器,帮助用户在运行时动态生成针对类型优化的元表生成器。

听上去有点绕,简单来说:元表的构建是由构建函数来完成的,而构建函数是由生成函数生成的;元表的构建是在运行时,而构建函数的生成可以是编译期也可以是运行时。
当然,这个在ios下是无法使用。

最后,如果没有内嵌构建函数生成器。我们嗨可以使用最万能的反射方式,为任意的类型构建元表。当然,一般来说,这种方式构建出来的元表也是性能最差的。

//这个函数也可能被lua调用,所以再加一层类型缓存,防止同一类型被多次调用。
Dictionary<Type, bool> loaded_types = new Dictionary<Type, bool>();
//构造类型元表
public bool TryDelayWrapLoader(RealStatePtr L, Type type)
{
    if (loaded_types.ContainsKey(type)) return true;
    loaded_types.Add(type, true);

    LuaAPI.luaL_newmetatable(L, type.FullName); //先建一个metatable,因为加载过程可能会需要用到
    LuaAPI.lua_pop(L, 1);

    Action<RealStatePtr> loader;//这个loader就是wrap中的_Regster方法,用来生成这种类型的元表
    int top = LuaAPI.lua_gettop(L);
    
    //首先检索是否有用户预定义的元表生成器
    if (delayWrap.TryGetValue(type, out loader))
    {
        delayWrap.Remove(type);
        //构造元表
        loader(L);
    }
    else
    {
#if !GEN_CODE_MINIMIZE && !ENABLE_IL2CPP && (UNITY_EDITOR || XLUA_GENERAL) && !FORCE_REFLECTION && !NET_STANDARD_2_0
//如果内嵌了代码生成器,则动态生成这个类型的Warp,并使用动态生成的warp来生成元表
    	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 });
    	}
    	else
    	{
        	Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
    	}
#else
//否则的话使用反射
    	Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
#endif
	}
	
	if (top != LuaAPI.lua_gettop(L))
	{
	    throw new Exception("top change, before:" + top + ", after:" + LuaAPI.lua_gettop(L));
	}

	foreach (var nested_type in type.GetNestedTypes(BindingFlags.Public))
	{
	    if (nested_type.IsGenericTypeDefinition())
	    {
	        continue;
	    }
	    GetTypeId(L, nested_type);
	}

	return true;
}
代码生成器、反射的生成方式,随后详解。

传递c#函数

这里主要是指LuaCSFunction,也就是可以被lua直接调用的c#函数。

public delegate int lua_CSFunction(IntPtr L);
普通的c#函数也可以传递,属于前面的基元类型,只是简单的传递一个IntPtr指针,虽然不能直接被lua调用,但是可以被lua传递(函数式编程,比如作为回调和返回值)。

xlua通过lua_pushstdcallcfunction来push一个LuaCSFunction,其调用的时xlua.dll提供的xlua_push_csharp_function

//LUADLL.cs
public static void lua_pushstdcallcfunction(IntPtr L, lua_CSFunction function, int n = 0)//[-0, +1, m]
{
	IntPtr fn = Marshal.GetFunctionPointerForDelegate(function);
	xlua_push_csharp_function(L, fn, n);
}

当我们push一个LuaCSFunction函数到lua中后,这个函数和栈上的参数会被当作另一个包装函数csharp_function_wrap的upvalue,生成一个闭包,最终把这个闭包push到lua虚拟栈上。这样的话,我们在调用这个函数时就可以做一些额外的事情,比如错误检测、钩子函数的回调。

AOP面向切面编程
 //xlua.c
 //push一个LuaCSFunction
 LUA_API void xlua_push_csharp_function(lua_State* L, lua_CFunction fn, int n)
{ 
    lua_pushcfunction(L, fn);
	if (n > 0) {
		lua_insert(L, -1 - n);
	}
	lua_pushboolean(L, 0);
	if (n > 0) {
		lua_insert(L, -1 - n);
	}
	//把原函数、参数作为包装函数的upvalue
    lua_pushcclosure(L, csharp_function_wrap, 2 + (n > 0 ? n : 0));
}

//包装函数
static int csharp_function_wrap(lua_State *L) {
	lua_CFunction fn = (lua_CFunction)lua_tocfunction(L, lua_upvalueindex(1));
	//真正调用的地方
    int ret = fn(L);    
    //错误检测
    if (lua_toboolean(L, lua_upvalueindex(2)))
    {
        lua_pushboolean(L, 0);
        lua_replace(L, lua_upvalueindex(2));
        return lua_error(L);
    }
    //钩子函数
	if (lua_gethook(L)) {
		call_ret_hook(L);
	}
	
    return ret;
}

最终提供给用户的是这两个接口:

internal void PushFixCSFunction(RealStatePtr L, LuaCSFunction func)
public void Push(RealStatePtr L, LuaCSFunction o)

这两个函数都做了一件事情,就是在LuaCSFunction函数push到lua之前,用另一个LuaCSFunction来包装了一层,用来做异常捕获。

和gc一样,mono和lua有自己的异常

不同的是,包装函数中索引原函数的方式不同:

PushFixCSFunction()使用FixCSFunction()来包装原函数。为了能够调回到原函数,用一个List<LuaCSFunction> fix_cs_functions来建立了下标到原函数的映射,最终push到lua的只是这个下标。调用lua_pushstdcallcfunction()时,这个下标作为upvalue一起传递。FixCSFunction()被调到时,通过upvalue取到下标,进而取到原函数,最终完成调用。

//PushFixCSFunction使用的的包装函数
static int FixCSFunction(RealStatePtr L)
{
    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        int idx = LuaAPI.xlua_tointeger(L, LuaAPI.xlua_upvalueindex(1));
        LuaCSFunction func = (LuaCSFunction)translator.GetFixCSFunction(idx);
        return func(L);
    }
    catch (Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in FixCSFunction:" + e);
    }
}

Push()使用StaticCSFunction()来包装原函数。原函数通过之前push一个objec的t函数Push(RealStatePtr L, object o)被push到lua(因此其实push的也是一个objkect的索引),同样也是作为StaticCSFunction()的upvalue。包装函数被调到时,通过upvalue取到索引,再通过FastGetCSObj()(下一篇介绍)取到原函数,最终完成调用。

//Push(RealStatePtr L, LuaCSFunction o)使用的包装函数
static int StaticCSFunction(RealStatePtr L)
{

    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);

        //获取被包装的func,是在这个包装方法入栈之前被压入的
        LuaCSFunction func = (LuaCSFunction)translator.FastGetCSObj(L, LuaAPI.xlua_upvalueindex(1));
        return func(L);
    }
    catch (Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in StaticCSFunction:" + e);
    }
}

两种索引方式的不同,使用在了不同的场景。

PushFixCSFunction()大量被用在我们静态生成的元表构造器中,做为默认需要支持的类型的元表,注册进lua,并永久存在。而Push()被大量使用在反射生成的元表之中,在使用完之后,可能就会被释放。

最后还有一个小细节,Push()中对IsStaticPInvokeCSFunction的函数没有加包装,因为这种类型的函数是我们静态生成的,在生成时,我们已经加入了异常捕获的代码,不需要再被捕获了。

可以看到,一个函数在被调用之前,进行了多次的包装,每次包装都附带了一些额外的功能,但又对原函数没有侵入。(函数式编程,面向切片编程)

其他push

//push一个lua在c#中的代理对象
public void Push(RealStatePtr L, LuaBase o)

LuaBase是c#对lua中特有的类型的封装。比如说LuaTable对应table、LuaFunction对应luafunction(此处不是luacfunction)。C#可以通过对应的类型去创建、操作一个lua原生对象。

所以,LuaBase只是一个lua对象在c#中的代理,我们push一个LuaBase其实是找到真正的lua对象,并push。

//重载push一个decimal,避免gc
void PushDecimal(RealStatePtr L, decimal val)

#HighLevelAPI#

对于HighLevelAPI,里面不包含具体的push实现,而是通过获取对象的类型,来选择性的调用类型所对应的具体push函数。

可以看作类似是编译器的函数重载功能
public void PushAny(RealStatePtr L, object o)
public void PushByType<T>(RealStatePtr L,  T v)

顾名思义,PushAny()可以用来push所有的类型,可以被用在我们提前没法知道对象类型的地方。最典型的例子就是在反射生成元表时,我们动态的获取对象,通过PushAny()把类型未知的对象push到lua。

实现也是简单明了:

public void PushAny(RealStatePtr L, object o)
        {
            if (o == null)
            {
                LuaAPI.lua_pushnil(L);
                return;
            }

            Type type = o.GetType();
            if (type.IsPrimitive())
            {
                pushPrimitive(L, o);
            }
            else if (o is string)
            {
                LuaAPI.lua_pushstring(L, o as string);
            }
            else if (type == typeof(byte[]))
            {
                LuaAPI.lua_pushstring(L, o as byte[]);
            }
            else if (o is decimal)
            {
                PushDecimal(L, (decimal)o);
            }
            else if (o is LuaBase)
            {
                ((LuaBase)o).push(L);
            }
            else if (o is LuaCSFunction)
            {
                Push(L, o as LuaCSFunction);
            }
            else if (o is ValueType)
            {
                PushCSObject push;
                if (custom_push_funcs.TryGetValue(o.GetType(), out push))
                {
                    push(L, o);
                }
                else
                {
                    Push(L, o);
                }
            }
            else
            {
                Push(L, o);
            }
        }

PushByType()是对PushAny()的封装,唯一的不同就是做了一个优化:

对于基元类型,不再调用pushPrimitive() (会有装箱/拆箱),而是通过查表的方式直接获取针对各个基元类型的直接push的方式。

//针对基元类型的push函数表
push_func_with_type = new Dictionary<Type, Delegate>()
{
    {typeof(int),  new Action<RealStatePtr, int>(LuaAPI.xlua_pushinteger) },
    {typeof(double), new Action<RealStatePtr, double>(LuaAPI.lua_pushnumber) },
    {typeof(string), new Action<RealStatePtr, string>(LuaAPI.lua_pushstring) },
    {typeof(byte[]), new Action<RealStatePtr, byte[]>(LuaAPI.lua_pushstring) },
    {typeof(bool), new Action<RealStatePtr, bool>(LuaAPI.lua_pushboolean) },
    {typeof(long), new Action<RealStatePtr, long>(LuaAPI.lua_pushint64) },
    {typeof(ulong), new Action<RealStatePtr, ulong>(LuaAPI.lua_pushuint64) },
    {typeof(IntPtr), new Action<RealStatePtr, IntPtr>(LuaAPI.lua_pushlightuserdata) },
    {typeof(decimal), new Action<RealStatePtr, decimal>(PushDecimal) },
    {typeof(byte),  new Action<RealStatePtr, byte>((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
    {typeof(sbyte),  new Action<RealStatePtr, sbyte>((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
    {typeof(char),  new Action<RealStatePtr, char>((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
    {typeof(short),  new Action<RealStatePtr, short>((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
    {typeof(ushort),  new Action<RealStatePtr, ushort>((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
    {typeof(uint),  new Action<RealStatePtr, uint>(LuaAPI.xlua_pushuint) },
    {typeof(float),  new Action<RealStatePtr, float>((L, v) => LuaAPI.lua_pushnumber(L, v)) },
};
(2)传递lua对象到c#敬请期待
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值