C
C#•11h ago
peppy

``Type.IsAssignableFrom`` returning false unexpectedly in plugin-loading scenario

This is a plugin framework. The "base" DLL with a plugin class that is subclassed by plugin DLLs is located in {ROOT}/bin/fhcore.dll, while plugins are located in {ROOT}/modules/{PLUGIN_NAME}/{PLUGIN_NAME.dll}. This plugin framework is loaded into the application process by .NET hosting, if that is somehow meaningful. All plugins have project references with <Private>false</Private> and <ExcludeAssets>runtime</ExcludeAssets>, ex.
<ProjectReference Include="..\core\Fahrenheit.Core.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
<ProjectReference Include="..\core\Fahrenheit.Core.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
I first ensure bin/fhcore.dll is loaded into AssemblyLoadContext.Default. Then for each plugin, I instantiate an AssemblyLoadContext as such:
public class FhLoadContext(string dll_path) : AssemblyLoadContext {
private readonly AssemblyDependencyResolver _resolver = new AssemblyDependencyResolver(dll_path);

protected override Assembly? Load(AssemblyName assembly_name) {
string? assembly_path = _resolver.ResolveAssemblyToPath(assembly_name);
return assembly_path != null ? LoadFromAssemblyPath(assembly_path) : null;
}

protected override nint LoadUnmanagedDll(string dll_name) {
string? dll_path = _resolver.ResolveUnmanagedDllToPath(dll_name);
return dll_path != null ? LoadUnmanagedDllFromPath(dll_path) : nint.Zero;
}
}
public class FhLoadContext(string dll_path) : AssemblyLoadContext {
private readonly AssemblyDependencyResolver _resolver = new AssemblyDependencyResolver(dll_path);

protected override Assembly? Load(AssemblyName assembly_name) {
string? assembly_path = _resolver.ResolveAssemblyToPath(assembly_name);
return assembly_path != null ? LoadFromAssemblyPath(assembly_path) : null;
}

protected override nint LoadUnmanagedDll(string dll_name) {
string? dll_path = _resolver.ResolveUnmanagedDllToPath(dll_name);
return dll_path != null ? LoadUnmanagedDllFromPath(dll_path) : nint.Zero;
}
}
I then LoadFromAssemblyPath the plugin into the context, and afterwards: - I clearly see that only the plugin itself (and any non-plugin-core dependencies thereof) are in the FhLoadContext - A duplicate reference to the core did not sneak in. Yet, typeof(BaseModule).IsAssignableFrom(plugin_type) still returns false for a given public class PluginModule : BaseModule in the plugin DLL. Why could that be?
36 Replies
peppy
peppyOP•11h ago
This exact scenario worked just fine when I just loaded every single plugin into AssemblyLoadContext.Default by force. This issue only arises when I give each a separate AssemblyLoadContext. But documentation states that in such cases, the shared dependencies can be loaded into AssemblyLoadContext.Default and things should still bind.
reflectronic
reflectronic•11h ago
is plugin_type.BaseType == typeof(BaseModule)
peppy
peppyOP•11h ago
:HmmNoted: I tried the following:
foreach (Type type in fh_dll.GetExportedTypes()) {
FhLog.Warning(type.BaseType!.AssemblyQualifiedName!);
FhLog.Warning(typeof(FhModule).AssemblyQualifiedName!);

if (type.BaseType != typeof(FhModule)) {
FhLog.Warning($"type mismatch");
continue;
}
foreach (Type type in fh_dll.GetExportedTypes()) {
FhLog.Warning(type.BaseType!.AssemblyQualifiedName!);
FhLog.Warning(typeof(FhModule).AssemblyQualifiedName!);

if (type.BaseType != typeof(FhModule)) {
FhLog.Warning($"type mismatch");
continue;
}
and got:
05/01/2025 01:14:19 | [Warning] loader.cs:37 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:14:19 | [Warning] loader.cs:38 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:14:19 | [Warning] loader.cs:41 (load_mod): type mismatch
05/01/2025 01:14:19 | [Warning] loader.cs:37 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:14:19 | [Warning] loader.cs:38 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:14:19 | [Warning] loader.cs:41 (load_mod): type mismatch
reflectronic
reflectronic•11h ago
what is AssemblyLoadContext.GetLoadContext(type.Assembly) for both of them
peppy
peppyOP•11h ago
one moment, please
05/01/2025 01:18:11 | [Warning] loader.cs:40 (load_mod): "" Fahrenheit.Core.FhLoadContext #2
05/01/2025 01:18:11 | [Warning] loader.cs:41 (load_mod): "IsolatedComponentLoadContext(C:\opt\games\ffx\fahrenheit\bin\fhcore.dll)" Internal.Runtime.InteropServices.IsolatedComponentLoadContext #0
05/01/2025 01:18:11 | [Warning] loader.cs:40 (load_mod): "" Fahrenheit.Core.FhLoadContext #2
05/01/2025 01:18:11 | [Warning] loader.cs:41 (load_mod): "IsolatedComponentLoadContext(C:\opt\games\ffx\fahrenheit\bin\fhcore.dll)" Internal.Runtime.InteropServices.IsolatedComponentLoadContext #0
oh, right, that'll explain it if I loaded the shared dep into AssemblyLoadContext.Default, the instance of the shared dep I loaded into it is where I must get the type from ...am I guessing right?
reflectronic
reflectronic•11h ago
are you using the hosting API?
peppy
peppyOP•11h ago
I am
reflectronic
reflectronic•11h ago
the problem is that fhcore is not in AssemblyLoadContext.Default well. i think that is the problem i am not sure how it is finding it
peppy
peppyOP•11h ago
public class FhLoader {
public FhLoader() {
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Join(FhInternal.PathFinder.Binaries.Path, "fhcore.dll"));
}

public void load_mod(string mod_name, FhManifest manifest, out List<FhModuleContext> modules) {
modules = [];

foreach (string fh_dll_name in manifest.DllList) {
FhModulePathInfo fh_dll_paths = FhInternal.PathFinder.create_module_paths(mod_name, fh_dll_name);
FhLoadContext fh_load_context = new FhLoadContext(fh_dll_paths.DllPath);
Assembly fh_dll = fh_load_context.LoadFromAssemblyPath(fh_dll_paths.DllPath);

foreach (Type type in fh_dll.GetExportedTypes()) {
// debug prints we just inserted
if (type.BaseType != typeof(FhModule)) {
FhLog.Warning($"type mismatch");
continue;
}
public class FhLoader {
public FhLoader() {
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Join(FhInternal.PathFinder.Binaries.Path, "fhcore.dll"));
}

public void load_mod(string mod_name, FhManifest manifest, out List<FhModuleContext> modules) {
modules = [];

foreach (string fh_dll_name in manifest.DllList) {
FhModulePathInfo fh_dll_paths = FhInternal.PathFinder.create_module_paths(mod_name, fh_dll_name);
FhLoadContext fh_load_context = new FhLoadContext(fh_dll_paths.DllPath);
Assembly fh_dll = fh_load_context.LoadFromAssemblyPath(fh_dll_paths.DllPath);

foreach (Type type in fh_dll.GetExportedTypes()) {
// debug prints we just inserted
if (type.BaseType != typeof(FhModule)) {
FhLog.Warning($"type mismatch");
continue;
}
I am under the impression I am loading it there? I should probably store the Assembly the method in the constructor returns
reflectronic
reflectronic•11h ago
i think what will be easier is to use load_assembly instead of load_assembly_and_get_function_pointer_fn in your host
peppy
peppyOP•11h ago
I see What's the difference? Or rather, how does that end up leading to this situation
reflectronic
reflectronic•11h ago
the load_assembly_and_get_function_pointer_fn creates a new ALC for each component that you load. since, you are supposed to be able to load multiple of them this is why you are seeing typeof(FhModule) inside that IsolatedComponentLoadContext thing. that is the one created by the host API
peppy
peppyOP•11h ago
Hm To tell the truth, I more or less verbatim used the .NET sample for hosting, so I don't know how I'd go about changing that There some example I can refer to?
reflectronic
reflectronic•10h ago
there are the unit tests i guess https://github.com/dotnet/runtime/blob/f6b93319242155a72801a7bb4bc92faac2ada1a3/src/native/corehost/test/nativehost/host_context_test.cpp#L425 you acquire load_assembly and get_function_pointer in the same way that you acquire load_assembly_and_get_function_pointer_fn and then you just use them there is documentation for what the methods all take here https://github.com/dotnet/runtime/blob/main/docs/design/features/native-hosting.md
peppy
peppyOP•10h ago
:catsweat: I'll look into it
reflectronic
reflectronic•10h ago
i still don't know how fhcore has sneaked into your custom ALC
peppy
peppyOP•10h ago
makes two of us; although I will say this is a rather convoluted use case to put it most mildly... I'm more or less "tacking on" .NET plugin loading into a process not my own in a way essentially identical to https://github.com/citronneur/detours.net
reflectronic
reflectronic•10h ago
can you log every asssembly you load in the override of Load is one of them fhcore
peppy
peppyOP•10h ago
absolutely, one moment please
05/01/2025 01:38:58 | [Warning] loader.cs:13 (Load): fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:38:58 | [Warning] loader.cs:13 (Load): System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
05/01/2025 01:38:58 | [Warning] loader.cs:38 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:38:58 | [Warning] loader.cs:39 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:38:58 | [Warning] loader.cs:41 (load_mod): "" Fahrenheit.Core.FhLoadContext #2
05/01/2025 01:38:58 | [Warning] loader.cs:42 (load_mod): "IsolatedComponentLoadContext(C:\opt\games\ffx\fahrenheit\bin\fhcore.dll)" Internal.Runtime.InteropServices.IsolatedComponentLoadContext #0
05/01/2025 01:38:58 | [Warning] loader.cs:45 (load_mod): type mismatch
05/01/2025 01:38:58 | [Warning] loader.cs:13 (Load): fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:38:58 | [Warning] loader.cs:13 (Load): System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
05/01/2025 01:38:58 | [Warning] loader.cs:38 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:38:58 | [Warning] loader.cs:39 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:38:58 | [Warning] loader.cs:41 (load_mod): "" Fahrenheit.Core.FhLoadContext #2
05/01/2025 01:38:58 | [Warning] loader.cs:42 (load_mod): "IsolatedComponentLoadContext(C:\opt\games\ffx\fahrenheit\bin\fhcore.dll)" Internal.Runtime.InteropServices.IsolatedComponentLoadContext #0
05/01/2025 01:38:58 | [Warning] loader.cs:45 (load_mod): type mismatch
it sure is
reflectronic
reflectronic•10h ago
when it loads fhcore does you override return null or is assembly_path non-null
peppy
peppyOP•10h ago
05/01/2025 01:41:11 | [Warning] loader.cs:13 (Load): fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:41:11 | [Warning] loader.cs:15 (Load): assembly_path is null: True
05/01/2025 01:41:11 | [Warning] loader.cs:13 (Load): System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
05/01/2025 01:41:11 | [Warning] loader.cs:15 (Load): assembly_path is null: True
05/01/2025 01:41:11 | [Warning] loader.cs:13 (Load): fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:41:11 | [Warning] loader.cs:15 (Load): assembly_path is null: True
05/01/2025 01:41:11 | [Warning] loader.cs:13 (Load): System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
05/01/2025 01:41:11 | [Warning] loader.cs:15 (Load): assembly_path is null: True
(the rest of the output is as above) just to make sure I'm not logging something entirely stupid:
foreach (Type type in fh_dll.GetExportedTypes()) {
FhLog.Warning(type.BaseType!.AssemblyQualifiedName!);
FhLog.Warning(typeof(FhModule).AssemblyQualifiedName!);

FhLog.Warning(AssemblyLoadContext.GetLoadContext(type.Assembly)!.ToString());
FhLog.Warning(AssemblyLoadContext.GetLoadContext(typeof(FhModule).Assembly)!.ToString());
foreach (Type type in fh_dll.GetExportedTypes()) {
FhLog.Warning(type.BaseType!.AssemblyQualifiedName!);
FhLog.Warning(typeof(FhModule).AssemblyQualifiedName!);

FhLog.Warning(AssemblyLoadContext.GetLoadContext(type.Assembly)!.ToString());
FhLog.Warning(AssemblyLoadContext.GetLoadContext(typeof(FhModule).Assembly)!.ToString());
I hope I did understand your instruction correctly
reflectronic
reflectronic•10h ago
uh type.BaseType.Assembly
peppy
peppyOP•10h ago
oh, right, I'm a Grade-A idiot, one moment ...in my defense, it is 3:40AM
05/01/2025 01:43:38 | [Warning] loader.cs:13 (Load): fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:43:38 | [Warning] loader.cs:15 (Load): assembly_path is null: True
05/01/2025 01:43:38 | [Warning] loader.cs:13 (Load): System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
05/01/2025 01:43:38 | [Warning] loader.cs:15 (Load): assembly_path is null: True
05/01/2025 01:43:38 | [Warning] loader.cs:39 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:43:38 | [Warning] loader.cs:40 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:43:38 | [Warning] loader.cs:42 (load_mod): "Default" System.Runtime.Loader.DefaultAssemblyLoadContext #1
05/01/2025 01:43:38 | [Warning] loader.cs:43 (load_mod): "IsolatedComponentLoadContext(C:\opt\games\ffx\fahrenheit\bin\fhcore.dll)" Internal.Runtime.InteropServices.IsolatedComponentLoadContext #0
05/01/2025 01:43:38 | [Warning] loader.cs:13 (Load): fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:43:38 | [Warning] loader.cs:15 (Load): assembly_path is null: True
05/01/2025 01:43:38 | [Warning] loader.cs:13 (Load): System.Runtime, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
05/01/2025 01:43:38 | [Warning] loader.cs:15 (Load): assembly_path is null: True
05/01/2025 01:43:38 | [Warning] loader.cs:39 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:43:38 | [Warning] loader.cs:40 (load_mod): Fahrenheit.Core.FhModule, fhcore, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05/01/2025 01:43:38 | [Warning] loader.cs:42 (load_mod): "Default" System.Runtime.Loader.DefaultAssemblyLoadContext #1
05/01/2025 01:43:38 | [Warning] loader.cs:43 (load_mod): "IsolatedComponentLoadContext(C:\opt\games\ffx\fahrenheit\bin\fhcore.dll)" Internal.Runtime.InteropServices.IsolatedComponentLoadContext #0
here we go.
reflectronic
reflectronic•10h ago
ok, this is what i expect to see
peppy
peppyOP•10h ago
sorry!
reflectronic
reflectronic•10h ago
yes, this is back to where it was before. fhcore is loaded into both the component ALC and the default ALC
peppy
peppyOP•10h ago
right, and it is loaded into the component ALC due to the host (because the host jumps to a static method in fhcore to bootstrap in the first place, I assume?)
reflectronic
reflectronic•10h ago
right, because that is the behavior of load_assembly_and_get_function_pointer_fn
peppy
peppyOP•10h ago
a-ha so it is the same exact blunder as getting core snuck into the plugin's ALC, only marginally more subtle 😅
reflectronic
reflectronic•10h ago
indeed
peppy
peppyOP•10h ago
I had no clue So, then, I have to try and figure it out from the unit tests
reflectronic
reflectronic•10h ago
based on the sample i assume you copied it from, you probably have something like
rc = get_delegate_fptr(
cxt,
hdt_load_assembly_and_get_function_pointer,
&load_assembly_and_get_function_pointer);
rc = get_delegate_fptr(
cxt,
hdt_load_assembly_and_get_function_pointer,
&load_assembly_and_get_function_pointer);
and you need to instead have something like
get_delegate_fptr(
cxt,
hdt_load_assembly,
&load_assembly);

get_delegate_fptr(
cxt,
hdt_get_function_pointer,
&get_function_pointer);
get_delegate_fptr(
cxt,
hdt_load_assembly,
&load_assembly);

get_delegate_fptr(
cxt,
hdt_get_function_pointer,
&get_function_pointer);
peppy
peppyOP•10h ago
that does look awfully familiar, yes- here's the actual source https://github.com/peppy-enterprises/fahrenheit/blob/main/src/stage1/src/main.cpp ...best not look too close into it, though 😄
reflectronic
reflectronic•10h ago
and once you have those function pointer, you should replace
int rc = load_assembly_and_get_function_pointer(
clrhost_lib_path.c_str(),
clrhost_type,
clrhost_init_method,
clrhost_delegate, /* Delegate type name, if using non-standard delegate */
nullptr,
(void**)&fh_init);
int rc = load_assembly_and_get_function_pointer(
clrhost_lib_path.c_str(),
clrhost_type,
clrhost_init_method,
clrhost_delegate, /* Delegate type name, if using non-standard delegate */
nullptr,
(void**)&fh_init);
with something like
int rc = load_assembly(
clrhost_lib_path.c_str(),
nullptr,
nullptr);

// error checking
int rc = get_function_pointer(
clrhost_type,
clrhost_init_method,
clrhost_delegate, /* Delegate type name, if using non-standard delegate */
nullptr,
nullptr,
(void**)&fh_init);

// error checking
int rc = load_assembly(
clrhost_lib_path.c_str(),
nullptr,
nullptr);

// error checking
int rc = get_function_pointer(
clrhost_type,
clrhost_init_method,
clrhost_delegate, /* Delegate type name, if using non-standard delegate */
nullptr,
nullptr,
(void**)&fh_init);

// error checking
peppy
peppyOP•10h ago
right thank you so much for taking the time done and dusted! :catlove: learned quite a few new things today, thanks again!
reflectronic
reflectronic•10h ago
:)

Did you find this page helpful?