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

C#编译后IL代码后置处理及客户端/服务端代码裁剪的正确方案

如何通过MSBuild任务实现C#游戏客户端/服务端的IL代码按需裁剪?

需求概述

开发一款包含客户端和服务端的游戏,大部分代码双方共享,部分类型、字段、方法带有Side属性标记所属端。需要通过客户端/服务端两种构建配置,在C#编译完成后、MSBuild输出前、IDE运行前,对IL代码进行后置处理,移除不属于当前配置的代码元素。

尝试过程

创建了两个项目:

  • DynamicCodeTest:待处理的目标游戏项目
  • Task:包含自定义MSBuild任务类Class1的项目,依赖Microsoft.Build

遇到两个问题:

  1. 在DynamicCodeTest.csproj中使用<UsingTask AssemblyName="Task">引用任务程序集时,MSBuild无法找到该程序集
  2. 改用<UsingTask AssemblyFile="路径/Task.dll">指定路径后,出现Task.dll被其他进程占用,无法从obj目录复制到构建目录的问题

解决方案

1. 用AssemblyName引用MSBuild任务的正确配置方式

要让MSBuild通过AssemblyName识别任务程序集,推荐以下两种方式:

  • NuGet包方式(最优):将Task项目打包为NuGet包,在DynamicCodeTest项目中安装该包。NuGet会自动将任务程序集加入MSBuild搜索路径,此时使用<UsingTask AssemblyName="Task" TaskName="Task.Class1" />即可正常引用。
  • 手动添加搜索路径:若不使用NuGet,可在DynamicCodeTest.csproj中添加Task项目输出目录到MSBuild的程序集搜索路径:
<PropertyGroup>
  <AssemblySearchPaths>$(AssemblySearchPaths);$(SolutionDir)Task\bin\$(Configuration)\netstandard2.0</AssemblySearchPaths>
</PropertyGroup>

注意替换为实际的Task项目输出路径,同时需确保Task项目在DynamicCodeTest之前完成构建。

2. 解决Task.dll文件占用问题

文件锁定通常由IDE或MSBuild进程导致,可通过以下方式规避:

  • 分离Task项目与DynamicCodeTest的输出目录,直接引用Task的输出DLL路径,避免复制操作
  • 在Task项目的csproj中禁用可能导致文件锁定的特性:
<PropertyGroup>
  <EnableHotReload>false</EnableHotReload>
  <UseSharedCompilation>false</UseSharedCompilation>
</PropertyGroup>
  • 自定义任务代码中避免持有自身DLL的引用,确保任务执行完成后释放所有资源

3. IL代码裁剪的核心实现

借助Mono.Cecil库(专门用于操作IL代码)实现裁剪逻辑,示例任务代码片段:

public class TrimIlTask : Task
{
    [Required]
    public string TargetAssemblyPath { get; set; }

    [Required]
    public string BuildSide { get; set; } // 传入Client/Server标识

    public override bool Execute()
    {
        using var assembly = AssemblyDefinition.ReadAssembly(TargetAssemblyPath);
        // 遍历程序集中所有类型、字段、方法
        foreach (var module in assembly.Modules)
        {
            foreach (var type in module.Types.ToList())
            {
                // 检查Side属性,移除不属于当前构建端的类型
                if (HasInvalidSideAttribute(type, BuildSide))
                {
                    module.Types.Remove(type);
                    continue;
                }
                // 处理字段和方法的裁剪逻辑
                TrimTypeMembers(type, BuildSide);
            }
        }
        // 保存修改后的程序集
        assembly.Write(TargetAssemblyPath);
        return true;
    }

    // 辅助方法:检查元素的Side属性是否匹配当前构建端
    private bool HasInvalidSideAttribute(ICustomAttributeProvider provider, string targetSide)
    {
        var sideAttr = provider.CustomAttributes.FirstOrDefault(a => a.AttributeType.Name == "SideAttribute");
        if (sideAttr == null) return false;
        var attrValue = sideAttr.ConstructorArguments[0].Value.ToString();
        return attrValue != targetSide;
    }

    // 辅助方法:裁剪类型内的字段和方法
    private void TrimTypeMembers(TypeDefinition type, string targetSide)
    {
        // 移除不符合条件的字段
        foreach (var field in type.Fields.ToList())
        {
            if (HasInvalidSideAttribute(field, targetSide))
                type.Fields.Remove(field);
        }
        // 移除不符合条件的方法
        foreach (var method in type.Methods.ToList())
        {
            if (HasInvalidSideAttribute(method, targetSide))
                type.Methods.Remove(method);
        }
    }
}

4. 任务触发时机配置

在DynamicCodeTest.csproj中,将任务绑定到编译完成后、文件复制到输出目录前的阶段:

<Target Name="TrimIlCode" AfterTargets="CoreCompile" BeforeTargets="CopyFilesToOutputDirectory">
  <TrimIlTask TargetAssemblyPath="$(TargetPath)" BuildSide="$(Configuration)" />
</Target>

这里假设构建配置名称直接对应Client/Server,可根据实际情况调整BuildSide的传入值。

内容的提问来源于stack exchange,提问作者128_bit_guy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 10:50:24