c# getresponsestream返回byte[]_看懂Xlua实现原理——从宏观到微观(1)传递c#对象到Lua...

v2-f2591ed83ac6a370699e2e888911a4f1_1440w.jpg?source=172ae18b

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;

传递基元类型

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

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

//push一个int

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

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

传递 object

public 

索引

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

//xlua.c

为什么我们传给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 

缓存

再回过头来看看c#中的索引和缓存机制:

在调用xlua_pushcsobj之前,所有object都会被放入一个对象的缓存池中ObjectTranslator.objects。而我们得到的key就是这个对象在缓存池中的下标。

//以下是经过删减的伪代码,只保留我们现在需要关注的流程

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元方法:

元表

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

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

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

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

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

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

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

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

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

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

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

//这个函数也可能被lua调用,所以再加一层类型缓存,防止同一类型被多次调用。
代码生成器、反射的生成方式,随后详解。

传递c#函数

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

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

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

//LUADLL.cs

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

AOP面向切面编程
//xlua.c

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

internal 

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

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

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

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

//PushFixCSFunction使用的的包装函数

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

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

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

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

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

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

其他push

//push一个lua在c#中的代理对象

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

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

//重载push一个decimal,避免gc

#HighLevelAPI#

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

可以看作类似是编译器的函数重载功能
public 

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

实现也是简单明了:

public 

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

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

//针对基元类型的push函数表
(2)传递lua对象到c#敬请期待
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值