You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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)

  1. 筛选目标字段
    遍历程序集中的所有类型,找出带有[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);
    }
}
  1. 为字段生成对应的属性和访问器
    为每个目标字段创建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;
}
  1. 替换所有字段引用为属性访问
    遍历所有方法的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指令,在这些指令之后注入方法调用。

核心步骤

  1. 遍历程序集中的所有方法,找出所有包含stfld操作符且操作数是目标字段的IL指令。
  2. 在每个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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:07:03