将 lambda 作为 IL 流传递到辅助 AppDomain 并使用 DynamicMethod 将其组装回来

发布于 2024-08-03 23:55:36 字数 4318 浏览 10 评论 0原文

是否可以将 lambda 表达式作为 IL 字节流传递到辅助 AppDomain,然后使用 DynamicMethod 将其组装回那里,以便可以调用它?

我不太确定这首先是正确的方法,所以这是我问这个问题的(详细)原因......

在我的应用程序中,有很多情况我需要加载几个用于反射的程序集,以便我可以确定下一步如何处理它们。问题是我需要能够在完成反射后卸载程序集。这意味着我需要使用另一个 AppDomain 来加载它们。

现在,我的大多数案例都有点相似,但不完全相同。例如,有时我需要返回一个简单的确认,有时我需要从程序集中序列化资源流,有时我需要进行一两个回调。

因此,我最终一遍又一遍地编写相同的半复杂的临时 AppDomain 创建代码,并实现自定义 MarshalByRefObject 代理以在新域和原始域之间进行通信。

由于这不再是真正可接受的,我决定编写一个可以这样使用的 AssemblyReflector 类:

using (var reflector = new AssemblyReflector(@"C:\MyAssembly.dll"))
{
    bool isMyAssembly = reflector.Execute(assembly =>
    {
        return assembly.GetType("MyAssembly.MyType") != null;
    });
}

AssemblyReflector 将自动卸载 AppDomain凭借 IDisposable,并允许我透明地执行在另一个 AppDomain 中保存反射代码的 Func 类型 lambda。

问题是,lambda 不能如此简单地传递到其他域。因此,在四处搜索之后,我发现了一种看起来可以做到这一点的方法:将 lambda 作为 IL 流传递到新的 AppDomain - 这让我想到了最初的问题。

这是我尝试过的方法,但没有成功(问题是在尝试调用新委托时抛出 BadImageFormatException):

public delegate object AssemblyReflectorDelegate(Assembly reflectedAssembly);

public class AssemblyReflector : IDisposable
{
    private AppDomain _domain;
    private string _assemblyFile;
    public AssemblyReflector(string fileName) { ... }
    public void Dispose() { ... }

    public object Execute(AssemblyReflectorDelegate reflector)
    {
        var body = reflector.Method.GetMethodBody();
        _domain.SetData("IL", body.GetILAsByteArray());
        _domain.SetData("MaxStackSize", body.MaxStackSize);
        _domain.SetData("FileName", _assemblyFile);

        _domain.DoCallBack(() =>
        {
            var il = (byte[])AppDomain.CurrentDomain.GetData("IL");
            var stack = (int)AppDomain.CurrentDomain.GetData("MaxStackSize");
            var fileName = (string)AppDomain.CurrentDomain.GetData("FileName");
            var args = Assembly.ReflectionOnlyLoadFrom(fileName);
            var pars = new Type[] { typeof(Assembly) };

            var dm = new DynamicMethod("", typeof(object), pars,
                typeof(string).Module);
            dm.GetDynamicILInfo().SetCode(il, stack);

            var clone = (AssemblyReflectorDelegate)dm.CreateDelegate(
                typeof(AssemblyReflectorDelegate));
            var result = clone(args); // <-- BadImageFormatException thrown.

            AppDomain.CurrentDomain.SetData("Result", result);
        });

        // Result obviously needs to be serializable for this to work.
        return _domain.GetData("Result");
    }
}

我是否已经接近(缺少什么?),或者这是一个毫无意义的练习总共?

注意:我意识到,如果这有效,我仍然需要小心放入 lambda 中的引用内容。不过,这不是问题。

更新:我设法取得了进一步的进展。似乎简单地调用 SetCode(...) 不足以重构该方法。这就是所需要的:

// Build a method signature. Since we know which delegate this is, this simply
// means adding its argument types together.
var builder = SignatureHelper.GetLocalVarSigHelper();
builder.AddArgument(typeof(Assembly), false);
var signature = builder.GetSignature();

// This is the tricky part... See explanation below.
di.SetCode(ILTokenResolver.Resolve(il, di, module), stack);
dm.InitLocals = initLocals; // Value gotten from original method's MethodInfo.
di.SetLocalSignature(signature);

技巧如下。原始 IL 包含某些元数据标记,这些标记仅在原始方法的上下文中有效。我需要解析 IL 并将这些标记替换为在新上下文中有效的标记。我通过使用一个特殊的类 ILTokenResolver 来完成此操作,该类是根据以下两个来源改编的:德鲁·威尔逊罗海波

这仍然存在一个小问题 - 新的 IL 似乎并不完全有效。根据 lambda 的具体内容,它可能会也可能不会在运行时抛出 InvalidProgramException。

作为一个简单的例子,这是可行的:

reflector.Execute(a => { return 5; });

虽然这不行:

reflector.Execute(a => { int a = 5; return a; });

还有更复杂的例子,它们要么有效,要么无效,具体取决于一些尚未确定的差异。可能是我错过了一些小但重要的细节。但我相当有信心在对 ildasm 输出进行更详细的比较后我会找到它。当我这样做时,我会在这里发布我的发现。

编辑:哦,伙计。我完全忘记了这个问题仍然悬而未决。但由于它本身可能变得显而易见,所以我放弃了解决这个问题。我对此并不高兴,这是肯定的。这确实很遗憾,但我想在再次尝试之前我会等待框架和/或 CLR 提供更好的支持。要使这项工作发挥作用,需要采取很多技巧,但即便如此,它也是不可靠的。向所有感兴趣的人致歉。

Is it possible to pass a lambda expression to a secondary AppDomain as a stream of IL bytes and then assemble it back there using DynamicMethod so it can be called?

I'm not too sure this is the right way to go in the first place, so here's the (detailed) reason I ask this question...

In my applications, there are a lot of cases when I need to load a couple of assemblies for reflection, so I can determine what to do with them next. The problem part is I need to be able to unload the assemblies after I'm finished reflecting over them. This means I need to load them using another AppDomain.

Now, most of my cases are sort of similar, except not quite. For example, sometimes I need to return a simple confirmation, other times I need to serialize a resource stream from the assembly, and again other times I need to make a callback or two.

So I end up writing the same semi-complicated temporary AppDomain creation code over and over again and implementing custom MarshalByRefObject proxies to communicate between the new domain and the original one.

As this is not really acceptable anymore, I decided to code me an AssemblyReflector class that could be used this way:

using (var reflector = new AssemblyReflector(@"C:\MyAssembly.dll"))
{
    bool isMyAssembly = reflector.Execute(assembly =>
    {
        return assembly.GetType("MyAssembly.MyType") != null;
    });
}

AssemblyReflector would automize the AppDomain unloading by virtue of IDisposable, and allow me to execute a Func<Assembly,object>-type lambda holding the reflection code in another AppDomain transparently.

The problem is, lambdas cannot be passed to other domains so simply. So after searching around, I found what looks like a way to do just that: pass the lambda to the new AppDomain as an IL stream - and that brings me to the original question.

Here's what I tried, but didn't work (the problem was BadImageFormatException being thrown when trying to call the new delegate):

public delegate object AssemblyReflectorDelegate(Assembly reflectedAssembly);

public class AssemblyReflector : IDisposable
{
    private AppDomain _domain;
    private string _assemblyFile;
    public AssemblyReflector(string fileName) { ... }
    public void Dispose() { ... }

    public object Execute(AssemblyReflectorDelegate reflector)
    {
        var body = reflector.Method.GetMethodBody();
        _domain.SetData("IL", body.GetILAsByteArray());
        _domain.SetData("MaxStackSize", body.MaxStackSize);
        _domain.SetData("FileName", _assemblyFile);

        _domain.DoCallBack(() =>
        {
            var il = (byte[])AppDomain.CurrentDomain.GetData("IL");
            var stack = (int)AppDomain.CurrentDomain.GetData("MaxStackSize");
            var fileName = (string)AppDomain.CurrentDomain.GetData("FileName");
            var args = Assembly.ReflectionOnlyLoadFrom(fileName);
            var pars = new Type[] { typeof(Assembly) };

            var dm = new DynamicMethod("", typeof(object), pars,
                typeof(string).Module);
            dm.GetDynamicILInfo().SetCode(il, stack);

            var clone = (AssemblyReflectorDelegate)dm.CreateDelegate(
                typeof(AssemblyReflectorDelegate));
            var result = clone(args); // <-- BadImageFormatException thrown.

            AppDomain.CurrentDomain.SetData("Result", result);
        });

        // Result obviously needs to be serializable for this to work.
        return _domain.GetData("Result");
    }
}

Am I even close (what's missing?), or is this a pointless excercise all in all?

NOTE: I realize that if this worked, I'd still have to be carefull about what I put into lambda in regard to references. That's not a problem, though.

UPDATE: I managed to get a little further. It seems that simply calling SetCode(...) is not nearly enough to reconstruct the method. Here's what's needed:

// Build a method signature. Since we know which delegate this is, this simply
// means adding its argument types together.
var builder = SignatureHelper.GetLocalVarSigHelper();
builder.AddArgument(typeof(Assembly), false);
var signature = builder.GetSignature();

// This is the tricky part... See explanation below.
di.SetCode(ILTokenResolver.Resolve(il, di, module), stack);
dm.InitLocals = initLocals; // Value gotten from original method's MethodInfo.
di.SetLocalSignature(signature);

The trick is as follows. Original IL contains certain metadata tokens which are valid only in the context of the original method. I needed to parse the IL and replace those tokens with ones that are valid in the new context. I did this by using a special class, ILTokenResolver, which I adapted from these two sources: Drew Wilson and Haibo Luo.

There is still a small problem with this - the new IL doesn't seem to be exactly valid. Depending on the exact contents of the lambda, it may or may not throw an InvalidProgramException at runtime.

As a simple example, this works:

reflector.Execute(a => { return 5; });

while this doesn't:

reflector.Execute(a => { int a = 5; return a; });

There are also more complex examples that are either working or not, depending on some yet-to-be-determined difference. It could be I missed some small but important detail. But I'm reasonably confident I'll find it after a more detailed comparison of the ildasm outputs. I'll post my findings here, when I do.

EDIT: Oh, man. I completely forgot this question was still open. But as it probably became obvious in itself, I gave up on solving this. I'm not happy about it, that's for sure. It's really a shame, but I guess I'll wait for better support from the framework and/or CLR before I attempt this again. There're just to many hacks one has to do to make this work, and even then it's not reliable. Apologies to everyone interested.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

-小熊_ 2024-08-10 23:55:36

我不知道你想要解决的问题是什么,但我过去制作了一个可以解决它的组件。

基本上,其目的是从字符串生成Lambda表达式。它使用单独的 AppDomain 来运行 CodeDOM 编译器。已编译方法的 IL 会序列化到原始的 AppDomain,然后使用 DynamicMethod 重建为委托。然后,调用委托并返回 lambda 表达式。

我在我的 博客。当然,它是开源的。因此,如果您要使用它,请向我发送您认为合理的任何反馈。

I didn't get exactly what is the problem you are trying to solve, but I made a component in the past that may solve it.

Basically, its purpose was to generate a Lambda Expression from a string. It uses a separate AppDomain to run the CodeDOM compiler. The IL of a compiled method is serialized to the original AppDomain, and then rebuild to a delegate with DynamicMethod. Then, the delegate is called and an lambda expression is returned.

I posted a full explanation of it on my blog. Naturally, it's open source. So, if you get to use it, please send me any feedback you think is reasonable.

伴梦长久 2024-08-10 23:55:36

可能不会,因为 lambda 不仅仅是源代码中的一个表达式。 lambda 表达式还创建闭包,将变量捕获/提升到它们自己的隐藏类中。该程序由编译器修改,因此无论您在哪里使用这些变量,您实际上都是在与类对话。因此,您不仅必须传递 lambda 的代码,还必须传递闭包变量随时间的任何更改。

Probably not, because a lambda is more than just an expression in source code. lambda expressions also create closures which capture/hoist variables into their own hidden classes. The program is modified by the compiler so everywhere you use those variables you're actually talking to the class. So you'd have to not only pass the code for the lambda, but also any changes to closure variables over time.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文