草庐IT

lua(tolua)与C#交互以及泄漏的整理与总结

脱发怪 2023-04-03 原文

前言

lua与C#交互通信原理

lua调用C#

调用无返回值函数(lua访问image的SetNativeSize)

调用返回C#对象的函数(lua访问image的mainTexture)

参考一个调用场景

C#调用lua

通过Require\Dofile调用lua以及通过DoString执行DoString

通过lua虚拟机对象获取对应的对象实例完成调用

Tolua中泄漏

1.table作为key。

2.C#持有lua对象使用完毕不执行释放接口。

简单了解一下lua的GC

总结



前言

最近在看lua泄漏的问题,接着就暴露出自己的一些问题,对于lua的认识更多的是停留在语法使用上,而对于lua如何产生泄漏,如何检查,如何优化,一直都没有一个清晰的认识,甚至对于lua与其他语言的交互也是没有什么认识。问题的本质是没有系统的了解过lua,也没有一定的知识储备。那本着一劳永逸的犯懒精神,集中时间和精力,去了解这门语言,之后再把自己的理解写成博客,似乎是一个性价比超高的选择。

lua与C#交互通信原理

Lua和C语言通信的主要方法是一个无所不在的虚拟栈。几乎所有的API的调用都会操作这个栈上的值。所有的数据交换,无论是Lua到C语言或C语言到Lua都通过这个栈来完成。此外,还可以用这个栈来保存一些中间结果。站可以解决Lua和C语言之间的两大差异,第一种差异是Lua使用垃圾收集,而C语言要求显示地释放内存;第二种是Lua使用动态类型,而C语言使用静态类型,同时,虚拟栈可以很好的处理静态类型与动态类型的相互转换。

如下图,unity C#先是生成tolua C#(warp文件),然后通过这些自动生成的tolua C#和tolua C交互,tolua C再借助lua栈与lua交互。

图二 C#与lua调用简图

lua调用C#

其主要逻辑就是两点,一个是lua内部通过持有translatorID,来确定是哪个C#对象,另一个是通过调用实例table所设置的metatable的接口来确定是哪个C#接口。通过warp.cs接口向lua注册相关逻辑,注册后的结构如下图所示。

当调用发生时,实例对象将其所持有的translatorId通过其metatable调用,通过lua栈,最终调用C#端注册的warp文件。

调用无返回值函数(lua访问image的SetNativeSize)

以下图中的对Image进行注册的warp文件为例,当lua端调用实例img(table)的SetNativeSize接口时,按照[方法索引,参数,参数数量]的顺序将数据压进lua栈(栈底是方法索引,栈顶是参数数量),此时的参数就是实例img(table)中的translatorId,当调用到C# Warp中的SetNativeSize方法时,会先检查参数数量(CheckArgsCount),接着通过进栈的translatorId,去ObjectTranslator中查找对应的image对象。

 

调用返回C#对象的函数(lua访问image的mainTexture)

再来看一看带返回值的情况,以下图中的对Image进行注册的warp文件为例,当lua端访问实例img(table)的mainTexture属性时,按照[方法索引,参数,参数数量]的顺序将数据压进lua栈(栈底是方法索引,栈顶是参数数量),此时的参数就是实例img(table)中的translatorId,当调用到C# Warp中的访问属性的方法时,先获得Image实例,然后获得mainTexture属性,然后进行压栈处理。

通过下图的调用顺序和代码可以很清楚的看到,压栈的操作顺序:先获取lua内存中的类型索引reference,然后将mainTexture对象加进ObjectTranslator中获取对应索引index。最后,调用tolua_pushnewudata接口,reference作为lua实例的原表的索引key,index用作lua实例的translatorId。

参考一个调用场景

接下来,以常见的写法gameobj.transform.position = pos进行分析,看lua层写下这一行代码,发生了什么。因为短短一行代码,却发生了非常非常多的事情,为了更直观一点,我们把这行代码调用过的关键luaApi以及ToLua相关的关键步骤列出来(以ToLua+cstolua导出为准,gameobj是GameObject类型,pos是Vector3):

第一步:.transform.position

UnityEngine_GameObjectWrap.get_transform    lua想从gameobj拿到transform,对应gameobj.transform
LuaDLL.luanet_rawnetobj         把lua中的gameobj变成c#可以辨认的id
ObjectTranslator.TryGetValue    用这个id,从ObjectTranslator中获取c#的gameobject对象
gameobject.transform            准备这么多,这里终于真正执行c#获取gameobject.transform了

ObjectTranslator.AddObject      给transform分配一个id,这个id会在lua中用来代表这个transform,
                                transform要保存到ObjectTranslator供未来查找
LuaDLL.luanet_newudata          在lua分配一个userdata,把id存进去,用来表示即将返回给lua的transform
LuaDLL.lua_setmetatable         给这个userdata附上metatable,让你可以transform.position这样使用它
LuaDLL.lua_pushvalue            返回transform,后面做些收尾
LuaDLL.lua_rawseti
LuaDLL.lua_remove

第二步:= pos

TransformWrap.set_position              lua想把pos设置到transform.position
LuaDLL.luanet_rawnetobj                 把lua中的transform变成c#可以辨认的id
ObjectTranslator.TryGetValue            用这个id,从ObjectTranslator中获取c#的transform对象
LuaDLL.tolua_getfloat3                  从lua中拿到Vector3的3个float值返回给c#
lua_getfield + lua_tonumber 3次         拿xyz的值,退栈
lua_pop
transform.position = new Vector3(x,y,z) 准备了这么多,终于执行transform.position = pos赋值了

C#调用lua

C#调用lua基本上就是3种:通过Require、DoFile;通过DoString执行DoString;通过lua虚拟机对象获取对应的对象实例,对对象实例进行操作。

通过Require\Dofile调用lua以及通过DoString执行DoString

通过lua虚拟机对象获取对应的对象实例完成调用

简单描述一下获取LuaFunction实例的步骤流程,核心逻辑是先将”testFunction”转换到fullPath,然后调用lua_getglobal接口来获取对象,接着就是通过toluaL_ref来获取reference索引,reference和luaState构成了LuaFunction对象的逻辑。

LuaFunction的Call逻辑主要是三步:BeginPCall、Pcall、EndPCall。先通过tolua_beginpcall传递refrence索引,然后通过lua_pcall传递参数和起始栈的id,这样就可以在lua层执行lua的方法,最后再根据起始栈id恢复当前lua栈到执行前的层级。

Tolua中泄漏

先明确在lua中的泄漏是什么,这种GC语言的泄漏大都是其引用关系没有正常释放,存在一个或者多个地方持有但不使用对象,这种情况下的对象不能被正常释放,且这种对象的数量不断的上升,我理解的泄漏是这种情景。

根据上述章节的内容去寻找会发生泄漏的场景,一般是2个:

1.table作为key。

重复性执行的逻辑中需要一直创建table,而这些新创建的table又在存储table中作为索引key,当这个创建的table不再使用时,存储table还持有新建的table,泄漏就发生了。

这种场景下的泄漏处理的方法其实也很简单,将存储table设置为弱引用table,这样lua在GC的时候就不会判定存储table还在持有已经不用的新建table。

具体细节可以参考这篇博文:Step By Step(Lua弱引用table)

2.C#持有lua对象使用完毕不执行释放接口。

在做业务逻辑开发的时候,大都会有这种场景:C#需要持有lua对象。比如tolua的框架下会定制一些UI工具类,通过设置luaFunction来完成对lua的事件回调。

在组件的使用周期完毕之后如果不对luaFunction进行释放,是有可能引起内存泄漏的。比如不断的注册lua的匿名回调函数,注册的函数会让C#持有refrence,但因为一直没有释放所以也就不会执行luaDll.toluaL_unref接口,那么lua就认为该lua函数对象是被使用的,又因为匿名函数在执行的时候,每注册一次都会生成一个新的函数对象,然后随着注册次数的增加,函数对象的数量也就一直增加,泄漏就这样发生了。

因此,要是想要规避这种情景下的泄漏,从两个地方着手:

1.C#工具使用周期结束执行luaFunction or luaTable的Dispose接口。

2.注册函数尽可能少的使用匿名函数

3.数学运算(Vector3/Quaternion)

严格的来说,这个并不算泄漏,因为最后那些临时对象会被GC掉,放到这里是因为,不加处理的数学运算,且调用频繁的话,会大大增加内存的增长率,使得GC操作变得相对频繁起来,所以,在某种程度上,它已经算是一种泄漏了。具体细节和优化,可以参考下面这篇文章

好Lua+Unity,让性能飞起来——Lua与C#交互篇_我动了谁的奶酪-CSDN博客_unity 纯lua

简单了解一下lua的GC

了解lua所采用的GC原理,对处理lua内存泄漏的问题,以及检查有很大的帮助。

Lua采用的是Mark-sweep算法:每次GC的时候,对所有对象进行一次扫描,如果该对象不存在引用,则被回收,反之则保存。在Lua5.0及其更早的版本中,Lua的GC是一次性不可被打断的过程,使用的Mark算法是双色标记算法(Two color mark),这样系统中对象的非黑即白,要么被引用,要么不被引用,这会带来一个问题:在GC的过程中如果新加入对象,这时候新加入的对象无论怎么设置都会带来问题,如果设置为白色,则如果处于回收阶段,则该对象会在没有遍历其关联对象的情况下被回收;如果标记为黑色,那么没有被扫描就被标记为不可回收,是不正确的。在Lua5.1后,Lua都采用分布回收以及三色增量标记清除算法(Tri-color incremental mark and sweep)

其基本的原理伪代码,参考书中原文为:

每个新创建的对象颜色设置为白色

//初始化阶段

遍历root节点中引用的对象,从白色置为灰色,并且放入到灰色节点列表中

//标记阶段

while(灰色链表中还有未扫描的元素):

从中取出一个对象,将其置为黑色

遍历这个对象关联的其他所有对象:

if 为白色

标记为灰色,加入到灰色链表中(insert to the head)

//回收阶段

遍历所有对象:

if 为白色,

没有被引用的对象,执行回收

else

重新塞入到对象链表中,等待下一轮GC

这是一些关于一些GC的文章:

深入探究Lua的GC算法

常见的GC回收算法及其含义c

总结

Lua与C#的交互主要是依托于lua栈,在相互调用的过程中,双方各自对到对方一个id,一个能索引到对应对象的id,然后对方根据这个id通过lua栈向对方传递操作行为,有真实对象所在的领域去执行具体的操作。大致流程如下图:

引用以及参考:

lua程序设计(第二版)[巴西]莱鲁 [2008-1]

Unity项目常见Lua解决方案性能比较_UWA—简单优化,优化简单!-CSDN博客

lua内存泄漏检测工具原理及设计 - 知乎

云风的 BLOG: 一个 Lua 内存泄露检查工具

Lua内存泄漏应对方法_xocoder's coding life-CSDN博客_lua内存泄漏

Lua 弱引用table - 天龙久传说 - 博客园

弱引用是什么?

【ToLua】C#和Lua的交互细节 - 知乎

如何编译各平台使用的库-以编译tolua为例_linxinfa的专栏-CSDN博客

【Unity游戏开发】tolua之wrap文件的原理与使用 - 马三小伙儿 - 博客园

用好Lua+Unity,让性能飞起来——Lua与C#交互篇_我动了谁的奶酪-CSDN博客_unity 纯lua

【Lua知识整理】——Lua栈_suhuaiqiang_janlay的专栏-CSDN博客_lua 栈

……

有关lua(tolua)与C#交互以及泄漏的整理与总结的更多相关文章

  1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  2. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  3. ruby - 难道Lua没有和Ruby的method_missing相媲美的东西吗? - 2

    我好像记得Lua有类似Ruby的method_missing的东西。还是我记错了? 最佳答案 表的metatable的__index和__newindex可以用于与Ruby的method_missing相同的效果。 关于ruby-难道Lua没有和Ruby的method_missing相媲美的东西吗?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/7732154/

  4. ruby-on-rails - 如何在 ruby​​ 交互式 shell 中有多行? - 2

    这可能是个愚蠢的问题。但是,我是一个新手......你怎么能在交互式ruby​​shell中有多行代码?好像你只能有一条长线。按回车键运行代码。无论如何我可以在不运行代码的情况下跳到下一行吗?再次抱歉,如果这是一个愚蠢的问题。谢谢。 最佳答案 这是一个例子:2.1.2:053>a=1=>12.1.2:054>b=2=>22.1.2:055>a+b=>32.1.2:056>ifa>b#Thecode‘if..."startsthedefinitionoftheconditionalstatement.2.1.2:057?>puts"f

  5. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  6. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  7. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  8. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

随机推荐