Передача лямбды второму AppDomain в виде потока IL и его сборка с использованием DynamicMethod

Можно ли передать лямбда-выражение второму AppDomain как поток байтов IL, а затем собрать его там, используя 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 и позволит мне выполнить a Func<Assembly,object> -тип лямбда, сохраняя код отражения в другом AppDomain прозрачно.

Проблема заключается в том, что lambdas нельзя просто передать другим доменам. Поэтому, после поиска, я нашел, что похоже на способ сделать именно это: передать лямбду в новый AppDomain как поток IL - и это подводит меня к исходному вопросу.

Вот то, что я пробовал, но не работал (проблема была вызвана 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");
    }
}

Я даже закрываю (что не хватает?), или это вообще бессмысленное упражнение?

ПРИМЕЧАНИЕ. Я понимаю, что если это сработает, мне все равно нужно быть осторожным в отношении того, что я вкладываю в лямбда в отношении ссылок. Но это не проблема.

ОБНОВЛЕНИЕ: мне удалось получить немного дальше. Похоже, что просто вызвать 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 MethodInfo.
di.SetLocalSignature(signature);

Трюк заключается в следующем. Оригинальный IL содержит определенные маркеры метаданных, которые действительны только в контексте исходного метода. Мне нужно было разобрать IL и заменить эти жетоны теми, которые действительны в новом контексте. Я сделал это, используя специальный класс ILTokenResolver, который я адаптировал из этих двух источников: Дрю Уилсон и Haibo Luo.

Есть еще небольшая проблема с этим - новый IL, похоже, не совсем верен. В зависимости от точного содержимого лямбда он может или не может вызывать InvalidProgramException во время выполнения.

В качестве простого примера это работает:

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

пока это не так:

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

Существуют также более сложные примеры, которые либо работают, либо нет, в зависимости от некоторых различий, которые еще не определены. Возможно, я пропустил некоторые небольшие, но важные детали. Но я уверен, что найду его после более детального сравнения результатов ildasm. Я опубликую свои результаты здесь, когда я это сделаю.

EDIT: О, мужик. Я полностью забыл, что этот вопрос все еще открыт. Но, поскольку это, вероятно, стало очевидным само по себе, я отказался от решения этого. Я не доволен этим, это точно. Это действительно позор, но я думаю, что я буду ждать улучшения поддержки из фреймворка и/или CLR, прежде чем повторю попытку. Для выполнения этой работы нужно просто много хаков, и даже тогда это не является надежным. Извиняюсь перед всеми заинтересованными.

Ответ 1

Вероятно, нет, потому что лямбда больше, чем просто выражение в исходном коде. лямбда-выражения также создают замыкания, которые захватывают/перемещают переменные в свои собственные скрытые классы. Программа модифицируется компилятором, поэтому везде вы используете те переменные, которые вы фактически говорите с классом. Таким образом, вам придется не только передавать код для лямбды, но также и любые изменения переменных закрытия во времени.

Ответ 2

Я не понял, в чем проблема, которую вы пытаетесь решить, но я сделал компонент в прошлом, который может его решить.

В основном, его целью было создание выражения Lambda из string. Он использует отдельный AppDomain для запуска компилятора CodeDOM. Ил скомпилированного метода сериализуется в исходное AppDomain, а затем перестраивается в делегат с DynamicMethod. Затем вызывается делегат и возвращается лямбда-выражение.

Я опубликовал полное объяснение этого на blog. Естественно, это с открытым исходным кодом. Итак, если вы его используете, пришлите мне любую обратную связь, которую вы считаете разумной.