我有一个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
中,而无需将调用程序集从文件中加载到该应用程序域中?其实,没有任何文件系统可以访问我的应用程序目录吗?
答案 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
中:
您的第二个问题是AppDomain之间共享实例。
AppDomains
共享相同代码之间的主要问题是需要共享相同的JIT编译代码(方法表,类型信息等)。
来自docs.microsoft:
JIT编译的代码无法共享给加载到 使用Assembly类的LoadFrom方法加载源上下文,或者 使用指定方法的Load方法的重载从图像加载 字节数组。
因此,当您从字节组装加载时,您将无法完全共享类型信息,这意味着对于第一个MarshalByRefObject
,此时的对象仅为AppDomain
。这意味着您只能从MarshalByRefObject
类型执行和访问方法/属性(尝试使用动态/反射并不重要-第一个AppDomain
没有实例的类型信息)。
您所要做的不是从ExecuteFromAssembly
返回对象,而是将AppDomainBridge
类扩展为所创建的Person
实例的简单包装,并使用它委派任何方法如果您确实需要将它们从第一个AppDomain
执行到第二个。