如何在不访问应用程序目录的情况下将程序集从字节数组加载到非默认AppDomain中?

时间:2019-05-07 13:32:46

标签: c# .net .net-assembly appdomain assembly-loading

我有一个COFF格式的内存中程序集,该程序集在我的主程序集MyApp.exe中使用:

byte[] assemblyData = GetAssemblyDataFromSomewhere();

(为了进行测试,GetAssemblyDataFromSomewhere方法只能对现有的程序集文件执行File.ReadAllBytes,但在我的实际应用中没有文件。)

让我们将此程序集称为MyAssembly。它仅具有.NET Framework引用,并且不依赖于任何其他用户代码。

我可以将此程序集加载到当前(默认)AppDomain中:

Assembly.Load(assemblyData);

// this works
var obj = Activator.CreateInstance("MyAssembly", "MyNamespace.MyType").Unwrap();

现在,我想将此程序集加载到另一个AppDomain中并在那里实例化该类。 MyNamespace.MyType源自MarshalByRefObject,因此我可以在应用程序域之间共享实例。

var newAppDomain = AppDomain.CreateDomain("DifferentAppDomain");

// this doesn't really work...
newAppDomain.Load(assemblyData);

// ...because this throws a FileNotFoundException
var obj = newAppDomain.CreateInstanceAndUnwrap("MyAssembly", "MyNamespace.MyType");

是的,我知道AppDomain.Load docs中有一个音符:

  

此方法仅应用于将程序集加载到当前应用程序域中。

是的,应该 用于该操作,但是...

  

如果当前AppDomain对象代表应用程序域 A ,并且从应用程序域 B 调用了Load方法,则程序集将被加载进入两个应用程序域。

我可以忍受。对于我来说,将程序集同时加载到两个应用程序域中都没有问题(因为无论如何我实际上都将其加载到默认应用程序域中)。

我可以看到该程序集已加载到新的应用程序域中。种类。

var assemblies = newAppDomain.GetAssemblies().Select(a => a.GetName().Name);
Console.WriteLine(string.Join("\r\n", assemblies));

这给了我

mscorlib
MyAssembly

但是尝试实例化该类总是会导致FileNotFoundException,因为CLR试图从文件加载程序集(尽管已经加载了程序集,至少根据AppDomain.GetAssemblies而言)。

我可以在MyApp.exe中这样做:

newAppDomain.AssemblyResolve += CustomResolver;

private static Assembly CustomResolver(object sender, ResolveEventArgs e)
{
    byte[] assemblyData = GetAssemblyDataFromSomewhere();
    return Assembly.Load(assemblyData);
}

这可行,但这会导致第二个应用程序域从文件中加载调用程序集(MyApp.exe)。发生这种情况是因为该应用程序域现在需要来自调用程序集的代码(CustomResolver方法)。

我可以将应用程序域创建逻辑和事件处理程序移至其他程序集中,例如MyAppServices.dll,因此新的应用程序域将加载该程序集,而不是MyApp.exe

但是,我想不惜一切代价避免文件系统访问我的应用程序目录:新的应用程序域不得从文件中加载任何用户程序集。

我也尝试过AppDomain.DefineDynamicAssembly,但这也不起作用,因为返回值的类型System.Reflection.Emit.AssemblyBuilder既不是MarshalByRefObject也不是带有[Serializable]的标记。

有什么方法可以将字节数组中的程序集加载到非默认AppDomain中,而无需将调用程序集从文件中加载到该应用程序域中?其实,没有任何文件系统可以访问我的应用程序目录吗?

2 个答案:

答案 0 :(得分:0)

我不确定您要实现什么目标,但是我会尝试以下方法。

通常,您的方法似乎还可以。您必须确保正确设置了辅助appdomain的探测路径(尤其是appbase路径)。否则,.NET Fusion将探测这些位置的依赖关系,并且您将尝试避免进行不需要的文件系统访问。 (嗯,至少要确保将这些路径配置为某些临时文件夹,且未设置任何实际权限)。

建议的解决方案

在任何情况下,您都可以尝试将 dynamic (这是我应该如何称呼它)程序集添加一个入口点(例如,某些{{1}中的Main方法) }类),以及 将程序集加载到辅助AppDomain后,请尝试调用AppDomain.ExecuteAssemblyByName

我将Bootstrap方法添加到Bootstrap类中,在CustomResolver方法中,我将订阅Main

这样,当调用AssemblyResolve方法(并希望它能按预期工作)时,对AppDomain的Main的订阅将不会触发融合。

我没有测试此解决方案,可能会花很长时间,但是尝试得更糟。

PS: 我确实看到有关此方法的文档确实声明了,运行时将首先尝试加载程序集(可能使用常规的探测逻辑),但是它并没有说明程序集已预加载到AppDomain中的情况。在拨打电话之前。

备注

ExecuteAssemblyByName方法提供了与ExecuteAssembly方法类似的功能,但是通过显示名称或AssemblyName而不是文件位置指定了程序集。因此,ExecuteAssemblyByName使用Load方法而不是LoadFile方法加载程序集。

程序集在.NET Framework标头中指定的入口点开始执行。

此方法不会创建新的进程或应用程序域,并且不会在新线程上执行入口点方法。

Load方法文档也没有提供明确的答案。

P.P.S: 调用Unwrap方法可能会在您的主AppDomain中触发融合,因为已为您的类创建了代理。我想,这时您的主AppDomain将尝试找到该动态加载的程序集。您确定引发异常的是辅助AppDomain吗?

答案 1 :(得分:0)

第一个问题是将程序集加载到第二个AppDomain中的方式。

您需要在两个AppDomains之间加载/共享一些类型。如果程序集尚未加载到第一个AppDomain中,则无法将程序集从第一个AppDomain加载到第二个AppDomain中(如果您将程序集字节加载到第一个AppDomain中,则该程序集也将不起作用uisng .Load(...))。
这应该是一个很好的起点:
可以说我有一个名为 Models 的类库,具有单个类Person,如下所示:

namespace Models
{
    public class Person : MarshalByRefObject
    {
        public void SayHelloFromAppDomain()
        {
            Console.WriteLine($"Hello from {AppDomain.CurrentDomain.FriendlyName}");
        }
    }
}

和控制台应用程序如下( Models 类库不是项目的引用)

namespace ConsoleApp
{
    internal class Program
    {
        [LoaderOptimizationAttribute(LoaderOptimization.MultiDomain)]
        public static void Main(String[] args)
        {
            CrossAppDomain();
        }

        private static Byte[] ReadAssemblyRaw()
        {
            // read Models class library raw bytes
        }

        private static void CrossAppDomain()
        {
            var bytes = ReadAssemblyRaw();
            var isolationDomain = AppDomain.CreateDomain("Isolation App Domain");

            var isolationDomainLoadContext = (AppDomainBridge)isolationDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, "ConsoleApp.AppDomainBridge");
            // person is MarshalByRefObject type for the current AppDomain
            var person = isolationDomainLoadContext.ExecuteFromAssembly(bytes);
        }
    }


    public class AppDomainBridge : MarshalByRefObject
    {
        public Object ExecuteFromAssembly(Byte[] raw)
        {
            var assembly = AppDomain.CurrentDomain.Load(rawAssembly: raw);
            dynamic person = assembly.CreateInstance("Models.Person");
            person.SayHelloFromAppDomain();
            return person;
        }
    }
}

它的工作方式是从 ConsoleApp 项目创建AppDomainBridge的实例,该实例同时加载到AppDomains中。现在,该实例位于第二个AppDomain中。然后,您可以使用AppDomainBridge实例将程序集实际加载到第二个AppDomain中,并跳过与第一个AppDomain有关的任何操作。
这是我执行代码(.NET Framework 4.7.2)时控制台的输出,因此Person实例位于第二个AppDomain中:

enter image description here


您的第二个问题是AppDomain之间共享实例。

AppDomains共享相同代码之间的主要问题是需要共享相同的JIT编译代码(方法表,类型信息等)。
来自docs.microsoft

JIT编译的代码无法共享给加载到 使用Assembly类的LoadFrom方法加载源上下文,或者 使用指定方法的Load方法的重载从图像加载 字节数组。

因此,当您从字节组装加载时,您将无法完全共享类型信息,这意味着对于第一个MarshalByRefObject,此时的对象仅为AppDomain。这意味着您只能从MarshalByRefObject类型执行和访问方法/属性(尝试使用动态/反射并不重要-第一个AppDomain没有实例的类型信息)。

您所要做的不是从ExecuteFromAssembly返回对象,而是将AppDomainBridge类扩展为所创建的Person实例的简单包装,并使用它委派任何方法如果您确实需要将它们从第一个AppDomain执行到第二个。