views:

356

answers:

3

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.

+1  A: 

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.

Joel Coehoorn
I was aware of that. But I thought that wouldn't be a problem if I didn't use outside variables in lambda?How about if I used a plain old delegate instead of lambda?
aoven
Part of the point is that a lambda isn't just the code in the expression. It also transforms other code in your app at compile time. You can transfer compile-time changes to another, already compiled, assembly.
Joel Coehoorn
A: 

Wow. Let's revisit something that you said originally:

I'm not too sure this is the right way to go in the first place

Ok, given that ... let's look at a statement you made shortly after that:

The problem part is I need to be able to unload the assemblies after I'm finished reflecting over them

Why?

I genuinely would like to know the reason, because that is the premise upon which everything that follows is based. Unless you're planning to write something that rivals Serge Lidins gem ... well, YAGNI -- just use CodeDom to compile a string you pass through ... but only, if you really, really have to.

Eric Smith
"Why?"There's no reason to hold them after I'm finished with them.I explained that there are different cases where I need this. Some are linked to the plugin architecture I'm employing in my apps. Being able to unload a plugin after it had served its purpose without restarting the host is quite an important feature."Unless you're planning to write something that rivals Serge Lidins gem ... well, YAGNI --"A rather rude assumption, but opinion noted."just use CodeDom to compile a string you pass through"I'm aware of this approach. It's not what I'd want in this case, though.
aoven
+1  A: 

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.

jpbochi