使用lambda expression来动态获取delegate,然后用Cecil注入代码(1)

深感一口气吃不成胖子的郁闷……要是时间能停下来让我慢慢想就好了。

之前留意到 ray_linn在这边的留言,提到AOP的需求:希望能找到一种办法来自动生成待注入的 IL,然后利用 Mono.Cecil来实现这个注入。Cecil本身功能还不错,但是在插入代码时,必须要自己来组装IL,这对不熟悉IL的开发者来说是个挑战,对熟悉IL的开发者来说也并不总是让人高兴的事吧。最好就是能用什么更高级的语言来编写待注入的代码,例如说使用C#本身。

要把高级语言翻译成IL,无非就是进行了编译的工作。在.NET Framework提供的现成的工具中,有三种办法可以实现这个编译:
1、从外部编译出IL
2、使用System.CodeDom来生成IL
3、使用C# 3.0的lambda expression,配合System.Linq.Expressions.Expression<TDelegate>来生成IL
当然咯,要是有毅力自己写一个编译器也不是不行……时间、精力、毅力,缺一不可。
所以还是把自己写编译器的选项放在一边,看看上面三种选择分别是个什么状况:

1、从外部编译出IL
可以把待注入的代码放在一个特定名字的方法里,从外部让C#编译器编译出整个assembly,然后再通过反射找到那个特定方法,并提取其中的IL。

过程类似这样:
先定义个占位用的接口:
public interface IInjectionWrapper
{
    void CodeToBeInjected( ); // make an ugly name to avoid name clashing
}

然后实现这个接口,并从外部调用csc(.NET Framework的C#编译器)或mcs(Mono的C#编译器)来编译下面的代码,得到一个assembly:
public class CodeInjectionDummy : IInjectionWrapper
{
    public void CodeToBeInjected( ) {
        // some code here
        // for now let's just print some meaningless message
        System.Console.WriteLine( "Dummy message" );
    }
}

得到了编译好的assembly之后,另外写一个程序来抽取其中的内容:
using Mono.Cecil;
using Mono.Cecil.Cil;

// ......

// get the dummy assembly
AssemblyDefinition assembly = AssemblyFactory.GetAssembly( "CodeInjectionDummy.dll" );

// get the dummy method
MethodDefinition method =
    assembly.MainModule.Types[ "CodeInjectionDummy" ]
        .GetMethod( "CodeToBeInjected", new Type[] { } );

// do whatever necessary with the instructions
// for now let's just print them out...
foreach ( Instruction ins in method.Body.Instructions ) {
    Console.WriteLine( "IL_{0}: {1}\t{2}",
        ins.Offset.ToString( "x4" ),
        ins.OpCode.Name,
        ins.Operand is string ?
            string.Format( "\"{0}\"", ins.Operand )
          : ins.Operand );
}


这种办法挺RP的。好处是我们熟悉的开发环境还是可以用,而且错误也会在编译时被捕捉到,但是要从外部调用编译器而不是在程序内部调用,可以控制的程度就降低了。
当然我们可以把待注入的代码的那个dummy类跟进行注入的类放在同一个文件里(或者广义来说,放在同一个assembly),就不用像上面描述的那样分开编译,但本质上还是一样的。

2、使用System.CodeDom来生成IL
例子请看 这里
使用System.CodeDom,我们可以轻松的把C#或者VB.NET的代码编译为assembly。相对前一种方法而言,使用System.CodeDom给予了我们更好的控制力;我们是直接在代码里调用编译器的,而且生成的assembly也不一定要以文件形式保存到磁盘上,而是可以直接在内存生成并使用。
但是问题也很明显:编译用的源代码,要么要以字符串的形式存在,要么要放在别的文件里。如果把源代码放在一个字符串里,那IDE就没办法对那字符串里的内容提供任何语法高亮之类的功能了——那就是一字符串。如果在字符串里打错了点什么,也必须到运行我们对System.CodeDom的调用时才能够发现。如果要把需要注入的源代码放在别的文件里的话,那还不如直接用前一种方法了,反正麻烦程度都差不多……

顺带一提,Sebastien Lebreton所写的 Reflexil也是用System.CodeDom来进行代码生成的。具体的代码在/Compilation/Compiler.cs里。
同时,Reflexil也用到了Cecil。下面引用/Utils/CecilHelpers.cs的一段代码(GPLv3):
#region " Method body import "
/// <summary>
/// Clone a source method body to a target method definition.
/// Field/Method/Type references are corrected
/// </summary>
/// <param name="source">Source method definition</param>
/// <param name="target">Target method definition</param>
public static void ImportMethodBody(
    MethodDefinition source,
    MethodDefinition target ) {
    // All i want is already in Mono.Cecil, but not accessible.
    // Reflection is my friend
    object context = new ImportContext(
        new DefaultImporter( target.DeclaringType.Module ) );
    Type contexttype = context.GetType( );

    Type mbodytype = typeof( Mono.Cecil.Cil.MethodBody );
    MethodInfo clonemethod = mbodytype.GetMethod(
        "Clone",
        BindingFlags.Static | BindingFlags.NonPublic,
        null,
        new Type[ ] { mbodytype, typeof( MethodDefinition ), contexttype },
        null );
    Mono.Cecil.Cil.MethodBody newBody = clonemethod.Invoke(
        null, new object[ ] { source.Body, target, context } )
            as Mono.Cecil.Cil.MethodBody;

    target.Body = newBody;

    // Then correct fields and methods references
    foreach ( Instruction ins in newBody.Instructions ) {
        if ( ins.Operand is TypeReference ) {
            TypeReference tref = ins.Operand as TypeReference;
            if ( tref.FullName == source.DeclaringType.FullName ) {
                ins.Operand = target.DeclaringType;
            }

        } else if ( ins.Operand is FieldReference ) {
            FieldReference fref = ins.Operand as FieldReference;
            if ( fref.DeclaringType.FullName == source.DeclaringType.FullName ) {
                ins.Operand = FindMatchingField(
                    target.DeclaringType as TypeDefinition, fref );
            }
        } else if ( ins.Operand is MethodReference ) {
            MethodReference mref = ins.Operand as MethodReference;
            if ( mref.DeclaringType.FullName == source.DeclaringType.FullName ) {
                ins.Operand = FindMatchingMethod(
                    target.DeclaringType as TypeDefinition, mref );
            }
        }
    }
}
#endregion

留意到它使用System.CodeDom与Cecil的方式:先利用System.CodeDom在另一个AppDomain生成了一个临时的assembly,然后利用Cecil找到那个assembly中要找的新的源方法,整个复制到目标方法里去,同时修正参数类型的不匹配。
这种方式的实现使得Reflexil只能整个方法替换,而没办法做真正的代码注入;不过跟Reflector一起用的话,这倒不是什么问题:反正Reflector能反编译出C#代码,那就把反编译出来的代码复制到新注入的代码中就是了。

但是……要是能有什么别的更自动的办法就好了。

3、使用C# 3.0的lambda expression,配合System.Linq.Expressions.Expression<TDelegate>来生成IL

C# 3.0中的expression tree相当的有趣。

当一个lambda expression被赋值给一个delegate类型,例如 Action<T>或者 Func<T, TResult>等,这个lambda expression会被编译器直接编译为
1) 当lambda expression没有使用闭包内的非局部引用也没有使用到this时,编译为一个私有静态方法;
2) 当lambda expression没有使用闭包内的非局部引用,但用到了this时,编译为一个私有成员方法;
3) 当lambda expression中引用到非局部变量,则编译为一个私有的内部类,将引用到的非局部变量提升为内部类的成员变量,将表达式的内容封装到内部类里的一个成员方法。
以前我在 这里稍微讨论过相关的一些问题,可以参考一下。

不过,当一个lambda expression被赋值给一个 System.Linq.Expressions.Expression<TDelegate>类型时,表达式的内容会被编译为一棵expression tree,而不会在编译时为其生成IL。直到真的调用那个Expression时才会进行到IL的编译。

用一段小程序来演示这个特性。
先把一个lambda expression((x, y) => x << y)赋值给一个delegate,Func<int, int, int>:
using System;
using System.Linq.Expressions;
using System.Reflection;

namespace TestLinqExpression
{
    class Program
    {
        static void Main(string[] args)
        {
            Func<int, int, int> shlFunc = (x, y) => x << y;
            MethodInfo method = shlFunc.Method; // gets a RuntimeMethodInfo instance
            Console.WriteLine(method); // Int32 <Main>b__0(Int32, Int32)
            Console.WriteLine(method.Invoke(null, new object[] { 3, 2 })); // 12
            Console.WriteLine(shlFunc(3, 2)); // 12
        }
    }
}

编译后再利用Reflector反编译,会看到多了一个私有静态方法<Main>b__0,还有一个缓存这个delegate的一个私有成员域:
internal class Program
{
    // Methods
    private static void Main(string[] args)
    {
        Func<int, int, int> shlFunc = delegate (int x, int y) {
            return x << y;
        };
        MethodInfo method = shlFunc.Method;
        Console.WriteLine(method);
        Console.WriteLine(method.Invoke(null, new object[] { 3, 2 }));
        Console.WriteLine(shlFunc(3, 2));
    }
    
    [CompilerGenerated]
    private static int <Main>b__0(int x, int y) {
        return (x << y);
    }
    
    [CompilerGenerated]
    private static Func<int, int, int> CS$<>9__CachedAnonymousMethodDelegate1;
}

留意到,上面的代码里同时使用了MethodBase.Invoke()(实际上是RuntimeMethodInfo.Invoke())与直接用括号的方式来调用那个delegate,运行的结果都是一样的。

把lambda expression改为赋值给Expression<Func<int, int, int>>的话:
using System;
using System.Linq.Expressions;
using System.Reflection;

namespace TestLinqExpression
{
    class Program
    {
        static void Main( string[ ] args ) {
            Expression<Func<int, int, int>> shlExp = ( x, y ) => x << y;
            Func<int, int, int> shlFunc = shlExp.Compile( );
            MethodInfo method = shlFunc.Method; // gets a DynamicMethod.RTDynamicMethod instance
            Console.WriteLine( method ); // Int32 lambda_method(System.Runtime.CompilerServices.ExecutionScope, Int32, Int32)
            // Console.WriteLine( method.Invoke( null, new object[ ] { 3, 2 } ) ); // throws ArgumentException
            Console.WriteLine( shlFunc( 3, 2 ) ); // 12
        }
    }
}

则会发现那个表达式并没有被编译为IL,而是生成了一棵expression tree:
internal class Program
{
    // Methods
    private static void Main(string[] args) {
        ParameterExpression CS$0$0000;
        ParameterExpression CS$0$0001;
        Func<int, int, int> shlFunc = Expression.Lambda<Func<int, int, int>>(
            Expression.LeftShift(
                CS$0$0000 = Expression.Parameter(typeof(int), "x"),
                CS$0$0001 = Expression.Parameter(typeof(int), "y")),
            new ParameterExpression[] { CS$0$0000, CS$0$0001 }).Compile();
        MethodInfo method = shlFunc.Method;
        Console.WriteLine(method);
        // Console.WriteLine(method.Invoke(null, new object[] { 3, 2 }));
        Console.WriteLine(shlFunc(3, 2));
    }
}

这棵“树”的表现方式不习惯的话可能会觉得有点怪。其实就是用括号来表示的树而已,没什么特别的。LINQ里的expression tree整个是静态类型的。这个问题以后写关于DLR的笔记的时候再讨论。
留意到这里得到的方法名跟前一个例子不一样了,expression tree通过Compile()方法被编译为IL,赋值给了一个Func<int, int, int>类型的delegate。不过却无法使用MethodBase.Invoke()(实际上是RTDynamicMethod.Invoke())来进行调用。
在.NET Reference Source Code, System.Reflection.Emit.DynamicMethod.cs里有这样一段代码:
public override Object Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) {
    // We want the creator of the DynamicMethod to control who has access to the 
    // DynamicMethod (just like we do for delegates). However, a user can get to
    // the corresponding RTDynamicMethod using Exception.TargetSite, StackFrame.GetMethod, etc. 
    // If we allowed use of RTDynamicMethod, the creator of the DynamicMethod would 
    // not be able to bound access to the DynamicMethod. Hence, we do not allow
    // direct use of RTDynamicMethod. 
    throw new ArgumentException(Environment.GetResourceString("Argument_MustBeRuntimeMethodInfo"), "this");
}

这段注释在这个文件里出现了好几次。反正就是不让直接用RTDynamicMethod来Invoke。直接用括号来对delegate调用倒是没问题。

说到底,我们只是要编译后的IL而已,能否直接Invoke没什么关系。
这个时候,可以通过DynamicMethod.GetMethodBody()来得到一个MethodBody,然后通过MethodBody.GetILAsByteArray()来获得IL所在的byte数组。
可是……拿到了byte数组,如何能让Cecil识别出来呢?这个就等到下次再写吧~

嗯,肯定会有人问说用这个lambda expression搭配LINQ的expression tree到底有什么好处。这个问题我一时也没想得特别清楚。我的几个出发点是:
1) lambda expression可以减少名字冲突的可能性。如果要注入的目标有好几个不同的地方,无论是外部编译还是用System.CodeDom,势必要为每个注入的位置都生成一次assembly,很麻烦,而且要想办法避免类型/方法名潜在的冲突的可能。
2) 生成时,最好能不生成完整的assembly,而是只生成方法。无论是外部编译还是System.CodeDom生成的都是完整的assembly,在这里算是overkill了。我们要的只是那个方法的内容而已。从一个delegate我们可以直接获取它的MethodInfo,进而得到MethodBody、byte[]等等,不用再花功夫跑到生成出来的assembly里去慢慢找我们要的方法……
3) lambda expression转换成expression tree之后,是被动态编译为IL的,但在编译时却能检查到语法错误。这要分开两方面看:
3a) lambda expression是C#语法的一部分,所以在编译时会被编译器检查。这样就不会像用System.CodeDom那样遇到缺乏IDE支持之类的问题了。
3b) expression tree既可以由lambda expression生成,也可以动态组装。相比之下,LINQ的expression tree比IL高级,组装起来也方便许多。假如做出一个什么InjectionCodeBuilder之类的,接受expression tree为参数的话,就既能照顾到编译时的lambda expression,也能照顾到运行时自行组装的expression tree。调用一个现成的Compile()方法就能编译为IL,何乐而不为呢。


P.S. 说到AOP,我不是不知道 PostSharp……不过它在内部到底是怎么实现的我还没看清楚。这库的代码量也不小啊(晕乎
PostSharp Core里的code model和weaver大概是值得关注的点吧。
总之自己要是能做一个不同的AOP实现的话,总也是件有趣的事 ^ ^

你可能感兴趣的:(AOP,C++,c,C#,LINQ)