如何用Mono.Cecil为带[NetworkVariable]属性的字段注入方法调用?
问题背景
我有如下NetworkObject类,其中包含一个protected方法LocalNetworkVariableChange:
class NetworkObject { protected void LocalNetworkVariableChange(object value) { // 执行相关逻辑 } }
我已经通过Mono.Cecil实现了为带有[NetworkVariable]属性的属性setter注入该方法调用的功能,示例代码如下:
class Foo : NetworkObject { public String Data { get { return ... } set { // 原有代码 ... // 注入的调用 this.LocalNetworkVariableChange(value); } }
对应的注入逻辑运行正常:
var propertySetMethod = property.SetMethod; ILProcessor processor = propertySetMethod.Body.GetILProcessor(); var lastInstruction = propertySetMethod.Body.Instructions.Last(); var valueOperand = (FieldDefinition)propertySetMethod.Body.Instructions .First(i => i.OpCode == OpCodes.Stfld) .Operand; var onLocalChangeMethod = assembly.MainModule .Types.Single(td => td.FullName == typeof(NetworkObject).FullName) .Methods.Single(md => md.Name == "LocalNetworkVariableChange") .Resolve(); var newInstructions = new List<Instruction> { // 加载当前实例到栈 Instruction.Create(OpCodes.Ldarg_0), // 加载网络变量的索引(原代码中的index变量) Instruction.Create(OpCodes.Ldc_I4, index), // 加载本地变量0到栈 Instruction.Create(OpCodes.Ldloc_0), // 读取字段值到栈 Instruction.Create(OpCodes.Ldfld, valueOperand), // 调用方法 Instruction.Create(OpCodes.Callvirt, onLocalChangeMethod), // 在return前插入Nop Instruction.Create(OpCodes.Nop) }; foreach (var newInstruction in newInstructions) { processor.InsertBefore(lastInstruction, newInstruction); }
目前这个功能在属性上运行正常,setter被调用时会自动触发注入的方法。现在我需要把这个功能扩展到带有[NetworkVariable]属性的字段上,请问有哪些可行方案?能不能把字段转换为属性?如果可以的话要怎么实现?
可行方案
方案一:将字段转换为自动属性(推荐)
这是最干净的方案,能直接复用你已经写好的属性注入逻辑,后续维护也更简单。核心思路是把带[NetworkVariable]的字段改成对应的属性,把原字段设为私有,再替换所有代码中对原字段的引用为属性访问。
具体实现步骤(基于Mono.Cecil)
- 筛选目标字段
遍历程序集中的所有类型,找出带有[NetworkVariable]属性的字段:
var networkVariableAttrType = assembly.MainModule.ImportReference(typeof(NetworkVariableAttribute)); foreach (var type in assembly.MainModule.Types) { foreach (var field in type.Fields.Where(f => f.CustomAttributes.Any(ca => ca.AttributeType.FullName == networkVariableAttrType.FullName))) { ConvertFieldToProperty(field, assembly.MainModule); } }
- 为字段生成对应的属性和访问器
为每个目标字段创建public属性,生成getter和setter方法,其中setter里注入LocalNetworkVariableChange调用:
void ConvertFieldToProperty(FieldDefinition field, ModuleDefinition module) { var declaringType = field.DeclaringType; // 创建属性,名称和原字段保持一致(也可以按规范改成首字母大写) var property = new PropertyDefinition(field.Name, PropertyAttributes.None, field.FieldType); declaringType.Properties.Add(property); // 生成Getter方法 var getter = new MethodDefinition( $"get_{field.Name}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, field.FieldType ); var getterIL = getter.Body.GetILProcessor(); getterIL.Append(Instruction.Create(OpCodes.Ldarg_0)); getterIL.Append(Instruction.Create(OpCodes.Ldfld, field)); getterIL.Append(Instruction.Create(OpCodes.Ret)); declaringType.Methods.Add(getter); property.GetMethod = getter; // 生成Setter方法并注入调用逻辑 var setter = new MethodDefinition( $"set_{field.Name}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, module.TypeSystem.Void ); // 添加setter的value参数 setter.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, field.FieldType)); var setterIL = setter.Body.GetILProcessor(); // 基础赋值逻辑:this.原字段 = value; setterIL.Append(Instruction.Create(OpCodes.Ldarg_0)); setterIL.Append(Instruction.Create(OpCodes.Ldarg_1)); setterIL.Append(Instruction.Create(OpCodes.Stfld, field)); // 注入LocalNetworkVariableChange调用(复用你原有的逻辑,这里简化了索引部分,按需调整) var onLocalChangeMethod = module .Types.Single(td => td.FullName == typeof(NetworkObject).FullName) .Methods.Single(md => md.Name == "LocalNetworkVariableChange") .Resolve(); var injectInstructions = new List<Instruction> { Instruction.Create(OpCodes.Ldarg_0), // 加载当前实例 Instruction.Create(OpCodes.Ldc_I4, index), // 替换成你的网络变量索引逻辑 Instruction.Create(OpCodes.Ldarg_1), // 直接用setter的value参数,不用再读字段 Instruction.Create(OpCodes.Callvirt, onLocalChangeMethod), Instruction.Create(OpCodes.Nop) }; var retInstr = Instruction.Create(OpCodes.Ret); setterIL.Append(retInstr); foreach (var instr in injectInstructions) { setterIL.InsertBefore(retInstr, instr); } declaringType.Methods.Add(setter); property.SetMethod = setter; // 把原字段设为私有,防止代码直接访问字段跳过属性setter field.IsPrivate = true; }
- 替换所有字段引用为属性访问
遍历所有方法的IL代码,把原来的ldfld(读字段)和stfld(写字段)指令替换为调用属性的getter和setter:
// 遍历所有方法 foreach (var method in assembly.MainModule.Types.SelectMany(t => t.Methods)) { if (!method.HasBody) continue; var ilProcessor = method.Body.GetILProcessor(); var instructions = method.Body.Instructions.ToList(); foreach (var instr in instructions) { // 替换读字段为调用getter if (instr.OpCode == OpCodes.Ldfld && instr.Operand == field) { ilProcessor.Replace(instr, Instruction.Create(OpCodes.Call, property.GetMethod)); } // 替换写字段为调用setter else if (instr.OpCode == OpCodes.Stfld && instr.Operand == field) { ilProcessor.Replace(instr, Instruction.Create(OpCodes.Call, property.SetMethod)); } } }
这个方案的优势很明显:统一了字段和属性的处理逻辑,后续维护只需要改属性注入的代码就行;而且不管是直接赋值还是反射访问属性,都能触发LocalNetworkVariableChange。
方案二:直接修改所有写入字段的IL代码
如果不想把字段改成属性,也可以直接找到所有对目标字段执行stfld(实例字段写入)的IL指令,在这些指令之后注入方法调用。
核心步骤
- 遍历程序集中的所有方法,找出所有包含
stfld操作符且操作数是目标字段的IL指令。 - 在每个
stfld指令之后,插入调用LocalNetworkVariableChange的IL代码:- 注意栈的状态:
stfld执行前,栈顶是要写入的值,执行后栈为空。所以需要在stfld之前先复制一份值到栈上,或者在stfld之后重新加载字段的新值。 - 示例注入逻辑:
// 在stfld之前插入Dup,复制要写入的值 ilProcessor.InsertBefore(stfldInstr, Instruction.Create(OpCodes.Dup)); // 执行stfld(原指令) // 然后加载this,调用方法 ilProcessor.InsertAfter(stfldInstr, Instruction.Create(OpCodes.Ldarg_0)); ilProcessor.InsertAfter(stfldInstr, Instruction.Create(OpCodes.Callvirt, onLocalChangeMethod));
- 注意栈的状态:
这个方案的缺点是逻辑更复杂:需要处理所有可能写入字段的场景(包括其他类的代码),容易遗漏;而且如果有反射写入字段的情况,这种方法无法触发调用。
内容的提问来源于stack exchange,提问作者Sebastian Barth
相关产品推荐
相关产品推荐

