Xlua原理分析 四

news2024/9/17 8:43:29

前面已经介绍了Xlua的通信原理,这篇主要记录Xlua如何做到Hotfix的。

我们项目就用到Xlua的Hotfix特性,周更用Lua去修改代码。版本内用C#开发。这点我觉得是Xlua比toLua强大的重要特性之一。

如何使用Hotfix本篇不介绍了,看Xlua教程懂得都懂,着重于原理部分。

一、如何进行Hotfix

先上测试代码:

    void Update()
        {
            if (++tick % 50 == 0)
            {
                Debug.Log(">>>>>>>>Update in C#, tick = " + tick);
                TestHotFixLog("C#");
            }
        }

        public void TestHotFixLog(string str)
        {
            Debug.Log("TestHotFixLog:" + str);
        }
        void OnGUI()
        {
            if (GUI.Button(new Rect(10, 10, 300, 80), "Hotfix"))
            {
                luaenv.DoString(@"
                xlua.hotfix(CS.XLuaTest.HotfixTest, 'Update', function(self)
                    self.tick = self.tick + 1
                    if (self.tick % 50) == 0 then
                        print('<<<<<<<<Update in lua, tick = ' .. self.tick)
                        self:TestHotFixLog('lua')
                    end
                end)
            ");
            }
        }

使用反编译编译Library\ScriptAssemblies\Assembly-CSharp.dll。可以看到这段

可以清晰的看到,反编译后是生成了一些委托,如果委托函数有值就不走原函数。

看看DelegateBridge 的结构:

对应上面的Update的这个函数,又看到了熟悉的压栈操作,通过这样的方式可实现热修

C#端的基础原理搞清后。看看xlua.hotfix都干了什么事。

xlua.hotfix = function(cs, field, func)
                if func == nil then func = false end
                local tbl = (type(field) == 'table') and field or {[field] = func}
                for k, v in pairs(tbl) do
                    local cflag = ''
                    if k == '.ctor' then
                        cflag = '_c'
                        k = 'ctor'
                    end
                    local f = type(v) == 'function' and v or nil
                    xlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least one
                    pcall(function()
                        for i = 1, 99 do
                            xlua.access(cs, cflag .. '__Hotfix'..i..'_'..k, f)
                        end
                    end)
                end
                xlua.private_accessible(cs)
            end

cs对应改的C#类,跟上面的反编译脚本一致。

field一个字符串

func方法。

可以看到他这里拼接了字符串,然后去向C#的委托去传递这个方法。

xlua.access(cs, cflag .. '__Hotfix0_'..k, f) -- at least one 
这里对应上述修改的__Hotfix0_Update

后面1-99 是修改了重载函数,造成了一定的性能损失。

PS:我们项目不允许C#代码使用同名重载函数,会出现很多意外的问题,可能就跟这里有关

再刨个根吧,看看xlua.access的实现:

[MonoPInvokeCallback(typeof(LuaCSFunction))]
        public static int XLuaAccess(RealStatePtr L)
        {
            try
            {
                ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
                Type type = getType(L, translator, 1);
                object obj = null;
                if (type == null && LuaAPI.lua_type(L, 1) == LuaTypes.LUA_TUSERDATA)
                {
                    obj = translator.SafeGetCSObj(L, 1);
                    if (obj == null)
                    {
                        return LuaAPI.luaL_error(L, "xlua.access, #1 parameter must a type/c# object/string");
                    }
                    type = obj.GetType();
                }

                if (type == null)
                {
                    return LuaAPI.luaL_error(L, "xlua.access, can not find c# type");
                }

                string fieldName = LuaAPI.lua_tostring(L, 2);

                BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;

                if (LuaAPI.lua_gettop(L) > 2) // set
                {
                    var field = type.GetField(fieldName, bindingFlags);
                    if (field != null)
                    {
                        field.SetValue(obj, translator.GetObject(L, 3, field.FieldType));
                        return 0;
                    }
                    var prop = type.GetProperty(fieldName, bindingFlags);
                    if (prop != null)
                    {
                        prop.SetValue(obj, translator.GetObject(L, 3, prop.PropertyType), null);
                        return 0;
                    }
                }
                else
                {
                    var field = type.GetField(fieldName, bindingFlags);
                    if (field != null)
                    {
                        translator.PushAny(L, field.GetValue(obj));
                        return 1;
                    }
                    var prop = type.GetProperty(fieldName, bindingFlags);
                    if (prop != null)
                    {
                        translator.PushAny(L, prop.GetValue(obj, null));
                        return 1;
                    }
                }
                return LuaAPI.luaL_error(L, "xlua.access, no field " + fieldName);
            }
            catch (Exception e)
            {
                return LuaAPI.luaL_error(L, "c# exception in xlua.access: " + e);
            }
        }

这里是使用了type获取元数据进行调用。

至此,xlua的hotfix原理已经清晰了。

util.hotfix就是先执行一遍lua的函数体,然后再执行一遍hotfix。所以可以执行原函数

--和xlua.hotfix的区别是:这个可以调用原来的函数
local function hotfix_ex(cs, field, func)
    assert(type(field) == 'string' and type(func) == 'function', 'invalid argument: #2 string needed, #3 function needed!')
    local function func_after(...)
        xlua.hotfix(cs, field, nil)
        local ret = {func(...)}
        xlua.hotfix(cs, field, func_after)
        return unpack(ret)
    end
    xlua.hotfix(cs, field, func_after)
end

二、如何生成程序集

  1. Generate Code
    这一步主要根据是根据C#类中需要支持热更的方法生成其对应的委托方法,但是并不是每个方法对应一个委托,而是根据调用参数和返回参数公用委托。这块之前有详细介绍代码,就不复述了。
  2. Hotfix Inject
    这一步主要是对Unity编译出的Dll中的C#类添加判断条件,以此来选择调用Lua中的修复方法还是直接执行C#代码

这一步是在Unity为C#代码生成完对应dll之后,由XLua再来对dll注入一些判断条件式来完成是否进行Lua调用的行为。
判断方法很简单,检查对应类静态字段是否有DelegateBridge对象。
实现如下:

bool injectMethod(MethodDefinition method, HotfixFlagInTool hotfixType)
        {
            var type = method.DeclaringType;
            
            bool isFinalize = (method.Name == "Finalize" && method.IsSpecialName);
            //__Gen_Delegate_Imp 方法引用
            MethodReference invoke = null;

            int param_count = method.Parameters.Count + (method.IsStatic ? 0 : 1);
			//根据返回值和参数个数类型和方法全名找对应的C#方法
            if (!findHotfixDelegate(method, out invoke, hotfixType))
            {
                Error("can not find delegate for " + method.DeclaringType + "." + method.Name + "! try re-genertate code.");
                return false;
            }

            if (invoke == null)
            {
                throw new Exception("unknow exception!");
            }

#if XLUA_GENERAL
            invoke = injectAssembly.MainModule.ImportReference(invoke);
#else
            invoke = injectAssembly.MainModule.Import(invoke);
#endif
			//插入的类静态字段,用来标记对应的方法是否有对应的Lua注入
            FieldReference fieldReference = null;
			//方法中的变量定义
            VariableDefinition injection = null;
			//IntKey前面InjectType设置过,没有泛型参数并且是同一个程序集
            bool isIntKey = hotfixType.HasFlag(HotfixFlagInTool.IntKey) && !type.HasGenericParameters && isTheSameAssembly;
            //isIntKey = !type.HasGenericParameters;
			
            if (!isIntKey)
            {
				//新建变量,看起来跟重载函数有关系
                injection = new VariableDefinition(invoke.DeclaringType);
                method.Body.Variables.Add(injection);
				//luaDelegateName 是个string方法名称
				//获取这个方法对应的委托名,因为有重载方法存在,所以之前已经注入的过的方法会在这边获取时候计数加1,
                //比如第一个重载获取的是__Hotfix0,那么下一个重载会是__Hotfix1,判断是否注入就是是否设置对应FieldReference。
                var luaDelegateName = getDelegateName(method);
				//一般不error,除非超过 MAX_OVERLOAD 100个。
                if (luaDelegateName == null)
                {
                    Error("too many overload!");
                    return false;
                }
				//创建对应的静态Field名字就是上面取到的luaDelegateName
                FieldDefinition fieldDefinition = new FieldDefinition(luaDelegateName, Mono.Cecil.FieldAttributes.Static | Mono.Cecil.FieldAttributes.Private,
                    invoke.DeclaringType);
                type.Fields.Add(fieldDefinition);
                fieldReference = fieldDefinition.GetGeneric();
            }

            bool ignoreValueType = hotfixType.HasFlag(HotfixFlagInTool.ValueTypeBoxing);
			 //IL插入位置,现在定位的是方法体的第一行
            var insertPoint = method.Body.Instructions[0];
			//获取IL处理器
            var processor = method.Body.GetILProcessor();
			//构造函数换个位置插。先不管了
            if (method.IsConstructor)
            {
                insertPoint = findNextRet(method.Body.Instructions, insertPoint);
            }

            Dictionary<Instruction, Instruction> originToNewTarget = new Dictionary<Instruction, Instruction>();
            HashSet<Instruction> noCheck = new HashSet<Instruction>();

            while (insertPoint != null)
            {
                Instruction firstInstruction;
				//isIntKey这边用到的是Xlua中的AutoIdMap,这边只对最基础的功能做分析,这边就分析基础的注入了。
                if (isIntKey)
                {
                    firstInstruction = processor.Create(OpCodes.Ldc_I4, bridgeIndexByKey.Count);
                    processor.InsertBefore(insertPoint, firstInstruction);
					//调用方法
                    processor.InsertBefore(insertPoint, processor.Create(OpCodes.Call, hotfixFlagGetter));
                }
                else
                {
					//创建第一条IL语句,获取类的静态Field压入方法栈中,其实就是之前luaDelegateName获取的字段(换句话说这里就是创建诸如 __Hotfix0_Start;)
                    firstInstruction = processor.Create(OpCodes.Ldsfld, fieldReference);
                    //插入insertPoint之前
                    processor.InsertBefore(insertPoint, firstInstruction);
                    //创建并插入IL,获取栈顶的值并压入到对应的变量中,injection就是我们之前创建的新建变量
                    processor.InsertBefore(insertPoint, processor.Create(OpCodes.Stloc, injection));
                    //创建并插入IL,压入变量体中的值到栈
                    processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldloc, injection));
                }
				//创建跳转语句,为false时候直接跳转insertPoint,
                //这边OpCodes.Brfalse看起来是布尔值判断,其实也会判断是否为null
                var jmpInstruction = processor.Create(OpCodes.Brfalse, insertPoint);
                processor.InsertBefore(insertPoint, jmpInstruction);

                if (isIntKey)
                {
					//创建当前指令参数数据源并后续调用
                    processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldc_I4, bridgeIndexByKey.Count));
					//创建委托函数对象
                    processor.InsertBefore(insertPoint, processor.Create(OpCodes.Call, delegateBridgeGetter));
                }
                else
                {
					//创建并插入IL,再次压入变量的值,因为上面做完判断后,栈顶的值就会被弹出,所以这边要再次压入
                    processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldloc, injection));
                }
				//成员函数比静态函数多了个参数,即自身,这步是压栈参数个数
                for (int i = 0; i < param_count; i++)
                {
                    if (i < ldargs.Length)
                    {
                        processor.InsertBefore(insertPoint, processor.Create(ldargs[i]));
                    }
                    else if (i < 256)
                    {
                        processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldarg_S, (byte)i));
                    }
                    else
                    {
                        processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldarg, (short)i));
                    }
                    if (i == 0 && !method.IsStatic && type.IsValueType)
                    {
                        processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ldobj, type));
                        
                    }
					 //对值类型进行Box
                    if (ignoreValueType)
                    {
                        TypeReference paramType;
                        if (method.IsStatic)
                        {
                            paramType = method.Parameters[i].ParameterType;
                        }
                        else
                        {
                            paramType = (i == 0) ? type : method.Parameters[i - 1].ParameterType;
                        }
                        if (paramType.IsValueType)
                        {
                            processor.InsertBefore(insertPoint, processor.Create(OpCodes.Box, paramType));
                        }
                    }
                }
				//创建并插入IL,调用invoke方法,因为之前已经压入injection的值,DelegateBridge的对象
                processor.InsertBefore(insertPoint, processor.Create(OpCodes.Call, invoke));
				//如果不是结构体,或者isFinalize,从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。
                if (!method.IsConstructor && !isFinalize)
                {
                    processor.InsertBefore(insertPoint, processor.Create(OpCodes.Ret));
                }
				
                if (!method.IsConstructor)
                {
                    break;
                }
                else
                {
					//普通方法,加入返回操作
                    originToNewTarget[insertPoint] = firstInstruction;
                    noCheck.Add(jmpInstruction);
                }
				//寻找下一个插入位置
                insertPoint = findNextRet(method.Body.Instructions, insertPoint);
            }
			//结构体的处理
            if (method.IsConstructor)
            {
                fixBranch(processor, method.Body.Instructions, originToNewTarget, noCheck);
            }
			//isFinalize的处理
            if (isFinalize)
            {
                if (method.Body.ExceptionHandlers.Count == 0)
                {
                    throw new InvalidProgramException("Finalize has not try-catch? Type :" + method.DeclaringType);
                }
                method.Body.ExceptionHandlers[0].TryStart = method.Body.Instructions[0];
            }
            if (isIntKey)
            {
                bridgeIndexByKey.Add(method);
            }
            return true;
        }

     

参考:

xlua hotfix分析

https://zhuanlan.zhihu.com/p/68907610/

OpCodes指令

https://www.cnblogs.com/chenxiaoran/archive/2012/11/19/2776807.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1961698.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网页上空格

&#xA0; no-break space(普通的英文半角空格但不换行) 中文全角空格 (一个中文宽度) &ensp; en空格(半个中文宽度) &emsp; em空格 (一个中文宽度) 四分之一em空格 (四分之一中文宽度) 相比平时的空格()&#xff0c;&nbsp拥有不间断(non-breaking)特性。即连续…

postgresql 您要的日期查询都在这

1、获取当前日期 select now();select current_timestamp;返回值均是当前年月日、时分秒&#xff0c;且秒保留6位小数&#xff0c;两种方式等价 select current_time;返回值&#xff1a;时分秒&#xff0c;秒最高精确到6位 select current_date;返回值&#xff1a;年月日 2…

HarmonyOS 鸿蒙DFX能力简介

DFX简介&#xff1a; Development and Feedback eXchange&#xff08;‌DFX&#xff09;‌&#xff0c;‌用于开发、‌测试和维护鸿蒙应用&#xff0c;提供一系列的工具和功能&#xff0c;‌帮助开发者在开发过程中进行性能分析、‌故障检测、‌异常处理。比如异常处理、性能分…

【qt小系统】传感器云平台3D散点图(附源码)

摘要&#xff1a;本文主要使用QT5&#xff0c;实现了一个传感器云平台的小示例&#xff0c;模拟的是各类传感器的添加&#xff0c;例如&#xff1a;热成像传感器、温度传感器、超声波传感器&#xff0c;模拟添加完成后&#xff0c;会自动将此传感器的三维坐标增加到3D散点图上&…

【每日一篇】UrbanGPT:时空大语言模型 【方便自己看】

摘要 时空预测旨在预测和洞察城市环境在时间和空间上不断变化的动态。它的目的是预测未来的模式&#xff0c;趋势和城市生活的各个方面的事件&#xff0c;包括交通&#xff0c;人口流动和犯罪率。虽然已经有许多努力致力于开发神经网络技术来准确预测时空数据&#xff0c;但重…

【C 语言】深入理解冒泡排序算法

0. 前言 冒泡排序是一种经典且基础的排序算法。它虽然在效率上并非最优&#xff0c;但对于初学者理解排序的基本概念和逻辑有着重要的意义。 1. 冒泡排序的基本思想 冒泡排序的基本思想是通过反复比较相邻的元素并交换它们&#xff08;如果顺序错误&#xff09;&#xff0c;…

基于GEC6818开发板+Linux+Qt设计的智能养老院出入管理系统(195)

一、前言 1.1 项目介绍 【1】项目功能介绍 随着我国老龄化进程的加快,养老问题日益突出,如何有效保障老年人的生活质量与安全成为社会关注的重点。智能化、信息化技术的发展为解决这一问题提供了新的思路和手段。基于Linux系统的智能养老院出入管理系统应运而生,为了实现…

记录一次使用Docker部署skywalking的过程

临时一个测试系统需要追一下bug&#xff0c;所以计划单节点部署一套skywalking进行调用链分析。 网上扒拉了几篇&#xff0c;都有点问题&#xff0c;这里单独记录一个。 首先skywalking需要是用es做数据源&#xff0c;当然也有mysql等多个版本&#xff0c;这里用的es。 同时…

使用JavaFx Fxml笔记

使用JavaFx Fxml实现账号密码登录 HelloApplication.java&#xff1a;package com.example.dr295cmonth7;import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.Scene; i…

Pinely Round 4 (Div. 1 + Div. 2)

有不明白或者想交流一下的可以加一下扣扣&#xff1a;2674993642&#xff0c;博客一般写的时候才看 A. Maximize the Last Element 解析&#xff1a;题目要让一次删除两个相邻的数字&#xff0c;最后留下尽可能最大的数字&#xff0c;赛时我用笔模拟了一下&#xff0c;发现如…

c++11,左值引用和右值引用,右值引用的作用

目录 左右值引用概念 右值引用的作用 左右值引用概念 什么是左值&#xff1f;什么是左值引用&#xff1f; 左值是一个表示数据的表达式(如变量名或解引用的指针)&#xff0c;我们可以获取它的地址可以对它赋 值&#xff0c;左值可以出现赋值符号的左边&#xff0c;右值不能出…

最详细最新最简单的Jenkins安装使用

首先去jenkins官网下载最新LTS war版本https://www.jenkins.io/download/ 准备sdk包 、nginx配置文件 、已配置的maven文件 查看系统架构 cat /etc/redhat-release uname -a uname -m lscpu安装常用工具 yum install jq -y yum install git -yyum install nginx -y systemc…

深入源码:解析SpotBugs (6)jvm 字节码简介

文章目录 一、JVM字节码概述一、文件结构概述二、详细解析1. 魔数和Class文件的版本2. 常量池3. 访问标志4. 类索引、父类索引与接口索引集合5. 字段表和方法表6. 属性表 字节码Spotbugs 作为一名资深的Java开发工程师&#xff0c;对JVM及其字节码有着深入的理解。现在&#xf…

DRAM 和 NAND 闪存收入将在 2024 年显著增长 75% 和 77%

#### 市场概况 根据 TrendForce 最新发布的市场报告&#xff0c;预计 2024 年 DRAM 和 NAND 闪存的收入将分别显著增长 75% 和 77%&#xff0c;这一增长主要是由于平均价格的上涨以及高价值产品的兴起&#xff0c;例如 HBM&#xff08;高带宽内存&#xff09;和 QLC&#xff0…

卷积神经网络(六)---实现 cifar10 分类

cifar10 数据集有60000张图片&#xff0c;每张图片的大小都是 32x32 的三通道的彩色图&#xff0c;一共是10种类别、每种类别有6000张图片&#xff0c;如图4.27所示。 图 4.27 cifar数据集 使用前面讲过的残差结构来处理 cifar10 数据集&#xff0c;可以实现比较高的准确率。 …

配置本地开发服务器代理请求以及登录模块开发(二)

项目初始化完成之后&#xff0c;准备开始进行项目的开发&#xff0c;首先配置好开发环境作为整个项目的基础 一、配置代理 1、config/proxy.ts配置代理 export default {// 如果需要自定义本地开发服务器 请取消注释按需调整dev: {// localhost:8000/api/** -> https://p…

Seata 入门与实战

一、什么是 Seata Seata 是一款开源的分布式事务解决方式&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式事务解决方案。 二、Seata 组成 事务协调者&#xff08;Transacti…

什么是Shell?怎么编写和执行Shell脚本?

大家好呀&#xff01;今天来简单介绍一下Shell基础&#xff0c;Shell介于内核与用户之间&#xff0c;是一个命令解释器&#xff0c;负责命令的解释。简单理解&#xff0c;Shell既是一个程序也是一种脚本语言。 1、shell介绍 1.1 概述 shell介于内核与用户之间&#xff0c;是一个…

索引结构—B+Tree索引、Hash索引、Full-Text(全文)索引、R-Tree(空间)索引

一、概述 在数据库系统中&#xff0c;索引是一种用于加快数据检索的数据结构。不同的索引结构适用于不同的查询场景和数据特性。索引按照不同角度可以划分不同类型的索引。按照数据结构可以划分BTree索引、Hash索引、FULL TEXT&#xff08;全文&#xff09;索引、R-Tree&#…

python inf是什么意思

INF / inf&#xff1a;这个值表示“无穷大 (infinity 的缩写)”&#xff0c;即超出了计算机可以表示的浮点数的范围&#xff08;或者说超过了 double 类型的值&#xff09;。例如&#xff0c;当用 0 除一个整数时便会得到一个1.#INF / inf值&#xff1b;相应的&#xff0c;如果…