代码注入补丁热更新大法流程 上面说了很多废话。接下直奔主题,要怎样做到:class Fucker {void Fucking() {if (PatchScript.HasPatchScript("Fucker.Fucking")) { // do patch fuck PatchScript.CallPatchScript("Fucker.Fucking"); return; } // do origin fuck Log.Info("I am a original fuck"); } } 我们要针对Fucker类的Fucking方法进行更新,则新建Lua脚本Fucker.Fucking.lua-- 文件名Fucker.Fucking.lua function Func()print("I am a patch fuck!") end return Func 一个补丁脚本就此完成,当程序运行到Fucking函数时,实际上它执行的是Lua脚本,变相的实现了热更新的功能——改变代码的执行行为。 STEP 1:执行环境 本文针对Unity游戏开发,那么原语言,当然是C#了;而打补丁的语言,当时Lua了; 在这里我们使用SLua插件,它的高质量代码和强大的反射功能,非常适合代码注入补丁热更新。class PatchScript { public boolHasPatchScript(string path) {return File.Exists("Script/" + path + ".lua"); } public void CallScript(string path) {string scriptCode = File.ReadAllString(path);var luaFunc = this.luaState.doScript(scriptCode) as LuaFunction; luaFunc.call(); } } STEP 2:代码注入 嗯,执行环境,非常的简单,不就是简单的if判断吗? 估计最令人迷惑的部分就是,如何往所有的C#函数体前部分插入代码了。 我们要做的,遍历所有的c#文件,取得class类名,然后再分析函数名,定位函数在代码中的起始位置、获取函数的参数列表、参数类型……等等。看起来很复杂,是不是要对c#做语法分析、词法分析了?感觉工作量很大啊。 幸好,轮子已经做好了。这里要用到一个重要的库——NRefactory。包括IDE MonoDevelop中的语法智能提示、重构都是基于这个库进行的。有了它,语法分析词法分析仅仅是API的调用而已。 我们要做的,就是使用NRefactory找出C#的方法体,并插入代码: using (var script = new DocumentScript(document, formattingOptions, options)) { CSharpParser parser = new CSharpParser(); SyntaxTree syntaxTree = parser.Parse(code, srcFilePath); foreach (var classDec in syntaxTree.Descendants.OfType<TypeDeclaration>()) { if (classDec.ClassType == ClassType.Class || classDec.ClassType == ClassType.Struct) { var className = classDec.Name; foreach (var method in classDec.Children.OfType<MethodDeclaration>()) { var returnType = method.ReturnType.ToString(); if (returnType.Contains("IEnumerator") || returnType.Contains("IEnumerable")) // 暂不支持yield! continue; // 。。。。这里找到了方法体! 开始进行插入! } } } } 我把使用NRefactory对C#方法体注入的代码,抽象成一个单独的类MethodInjector(C#),看文章底部。 STEP 3:编写Lua补丁 补丁的方法,在上文“代码注入补丁热更新大法流程”中已有提及: 对需要打补丁的函数,创建Lua脚本 如上文中要改变Fucker.Fucking函数的执行行为,则创建Fucker.Fucking.lua脚本文件,脚本末端返回一个Lua函数。 最后 本文着重提供了一种思路,而不提供完整的源代码,毕竟涉及到部分人的商业利益,遗憾点到即止。 使用下面的MethodInjector类,会把函数的参数值也进行解析、预编译指令引入、并且可以在Lua补丁中控制是否在执行补丁后,继续执行原C#代码,基本能达到大部分的需求了。 这里举例一个更好的方案:注入DLL的IL代码,而不是注入c#代码,来确保我们的c#代码不会被改动。 MethodInjector类#region Copyright (c) 2015 KEngine / Kelly <http://github.com/mr-kelly>, All rights reserved.// KEngine - Toolset and framework for Unity3D// ===================================// // Date: 2015/12/03// Author: Kelly// Email: 23110388@qq.com// Github: https://github.com/mr-kelly/KEngine// // This library is free software; you can redistribute it and/or// modify it under the terms of the GNU Lesser General Public// License as published by the Free Software Foundation; either// version 3.0 of the License, or (at your option) any later version.//// This library is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU// Lesser General Public License for more details.// // You should have received a copy of the GNU Lesser General Public// License along with this library.#endregionusing System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Text;using ICSharpCode.NRefactory.CSharp;using ICSharpCode.NRefactory.CSharp.Refactoring;using ICSharpCode.NRefactory.Editor; namespace CSharpMethodInjector{ /// <summary> /// 向C#代码块插入文字 /// </summary> public class MethodInjector { public delegate string CSharpMethodInjectorDelegate( string className, string methodName, string returnType, string[] parameters, string[] outParams); private CSharpMethodInjectorDelegate _afterInsert; private CSharpMethodInjectorDelegate _beforeInsert; private string[] _defineSymbols; private MethodInjector() {} public MethodInjector(CSharpMethodInjectorDelegate afterInsert, CSharpMethodInjectorDelegate beforeInsert = null, string[] defineSymbols = null) { _afterInsert = afterInsert; _beforeInsert = beforeInsert; _defineSymbols = defineSymbols; } /// <summary> /// 代码注入! GOGOGO /// </summary> /// <param name="srcFilePath"></param> /// <param name="outputFilePath"></param> public void Inject(string srcFilePath, string outputFilePath) { UTF8Encoding utf8 = new UTF8Encoding(false); var predefineSymbolsSb = new StringBuilder(); if (_defineSymbols != null) { foreach (var symbol in _defineSymbols) { predefineSymbolsSb.AppendFormat("#define {0}\n", symbol); }
var document = new StringBuilderDocument(code); var formattingOptions =FormattingOptionsFactory.CreateAllman(); var options = new TextEditorOptions(); using (var script = new DocumentScript(document, formattingOptions, options)) { CSharpParser parser = new CSharpParser(); SyntaxTree syntaxTree = parser.Parse(code, srcFilePath); foreach (var classDec in syntaxTree.Descendants.OfType<TypeDeclaration>()) { if (classDec.ClassType == ClassType.Class || classDec.ClassType == ClassType.Struct) { var className = classDec.Name; foreach (var method in classDec.Children.OfType<MethodDeclaration>()) { var returnType = method.ReturnType.ToString(); if (returnType.Contains("IEnumerator") || returnType.Contains("IEnumerable"))// 暂不支持yield!continue; var methodSegment = script.GetSegment(method); var methodOffset = methodSegment.Offset; // 方法偏移 var paramsTypes = method.Parameters;//method.Children.OfType<ParameterDeclaration>();// typeName var paramsTypesStrs = new List<string>(); // 参数 if (!method.HasModifier(Modifiers.Static)) paramsTypesStrs.Add("this"); // 非静态方法,加this
var paramsOutStrs = new List<string>(); // out 的参数 foreach (var paramsType in paramsTypes) { paramsTypesStrs.Add(paramsType.Name); if (paramsType.ParameterModifier == ParameterModifier.Out) { // out 的参数 paramsOutStrs.Add(string.Format("{0} = default({1});", paramsType.Name, paramsType.Type)); } } if (_beforeInsert != null) { var insertBeforeText = _beforeInsert(className, method.Name, returnType, paramsTypesStrs.ToArray(), paramsOutStrs.ToArray()); if (!string.IsNullOrEmpty(insertBeforeText)) script.InsertText(methodOffset, insertBeforeText); } foreach (var blockStatement in method.Descendants.OfType<BlockStatement>()) { int insertOffset; if (blockStatement.Statements.Count == 0) // 空函数 { var segment = script.GetSegment(blockStatement); insertOffset = segment.Offset + 1; // 越过"/" } else { var firstChildStatement = blockStatement.Statements.First(); var segment = script.GetSegment(firstChildStatement); insertOffset = segment.Offset; } script.InsertText(insertOffset, _afterInsert(className, method.Name, returnType, paramsTypesStrs.ToArray(), paramsOutStrs.ToArray())); break; // 仅对第一个方法包体(BlockStatement), 其它不是方法进行处理 } } } }