Cecil 是 Mono 的一個子項目,用於對程序集進行讀寫,並且已經用於 Mono 的調試,Reflector 也使用它作為底層庫。最近把 DbEntry 使用 Emit 生成程序集的方式,改成了使用 Cecil 的方式,就我的感受來說,Cecil 是比較優秀的,有一些地方,比 Emit 使用起來還舒服的多;不過,有一些地方也比較繁瑣。
我使用的是 Git 裡的最新版本,如果大家要測試的話,也建議使用 Git 版,所以,需要安裝一個 Git 客戶端。
這裡,用一個非常簡單的例子,說明一下 Cecil 的基本用法。
首先,我們編寫一個測試用的程序集 TestApp.exe :
using System;
namespace TestApp
{
class Program
{
static void Main()
{
Console.WriteLine("Main");
}
private static void Before()
{
Console.WriteLine("Before");
}
private static void After()
{
Console.WriteLine("After");
}
}
}
然後,編寫一個使用 Cecil 進行改寫的應用 CecilTest.exe :
using System;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
namespace CecilTest
{
class Program
{
static void Main(string[] args)
{
if(args.Length != 1)
{
Console.WriteLine("Usage: CecilTest TestApp.exe");
}
var m = ModuleDefinition.ReadModule(args[0]);
var prog = m.Types.First(p => p.Name == "Program");
var main = prog.Methods.First(p => p.Name == "Main");
var before = prog.Methods.First(p => p.Name == "Before");
var after = prog.Methods.First(p => p.Name == "After");
var il = main.Body.GetILProcessor();
il.InsertBefore(main.Body.Instructions[0], il.Create(OpCodes.Call, before));
il.InsertBefore(main.Body.Instructions.Last(), il.Create(OpCodes.Call, after));
m.Write(args[0] + ".exe");
Console.WriteLine("Done");
Console.ReadLine();
}
}
}
編譯這兩個項目,並且使用 CecilTest.exe 處理一下 TestApp.exe,生成 TestApp.exe.exe,運行 TestApp.exe,運行結果:
Main
運行 TestApp.exe.exe,運行結果:
Before
Main
After
可以看到,我們已經成功的在 Main 函數的前後,分別插入了一次函數調用。
基本使用方法就是這樣,大體和 Emit 類似,只是不止可以使用 Emit 函數,還可以直接修改程序集。當然,還有其它一些細節的不同,比如它的變量定義不是通過 ILProcessor,而是直接操作函數體的 Variables;再比如它沒有 DeclareLabel 函數,跳轉直接引用函數體的 Instruction 進行。另外,它可以只加載目標程序集,卻不加載其依賴項,所以很多東西都分定義和引用兩種。
就目前來說,我認為它的效果是令人滿意的。不過它最大的問題,在於泛型處理上。不是說不能做,而是太繁瑣,有時候甚至是不可能。在 DbEntry 中,最後被泛型擊敗,采取了 Reflection+Cecil 的方式,這種方式簡單易行,不過問題是,Reflection 需要加載程序集,除了可能出現無法加載依賴項的異常外,也無法簡單的回寫原文件。我在 Cecil 的 Git 上提交了 issue,不過作者回復,不覺得這是問題,所以我也懶得糾纏了。