Loading assemblies
Load assemblies and resolve native function pointers to managed static methods.
Once you have an initialized HostContext, you can start the .NET runtime
and hand it managed code to load — resolving native function pointers to static methods, or loading
assemblies by path or from memory. HostFxrSharp exposes the four data-loading runtime delegates of
hostfxr as small, strongly-typed wrappers, so you never touch the raw native signatures, the
char_t marshalling, or the UNMANAGEDCALLERSONLY_METHOD sentinel yourself.
This is the counterpart to Running applications: instead of running an
app.dll to completion, you load a component (typically a framework-dependent class library with a
.runtimeconfig.json) and call into it. See How hosting works for the underlying model.
Requesting any loader starts the runtime. All four wrappers are obtained by asking the context
for a runtime delegate, which starts the runtime the first time. After that, runtime
properties become read-only. Set every property you need before
calling any Get…Loader / GetFunctionPointerResolver method.
The four loaders at a glance
Each accessor on HostContext returns a readonly struct wrapper (in HostFxrSharp.Loading) over one
native runtime delegate:
| Accessor | Native helper | Load context | Dependency resolution | Result | Minimum runtime |
|---|---|---|---|---|---|
GetAssemblyFunctionPointerLoader() | load_assembly_and_get_function_pointer | Isolated AssemblyLoadContext | .deps.json via AssemblyDependencyResolver | nint function pointer | .NET Core 3.0+ |
GetFunctionPointerResolver() | get_function_pointer | Default AssemblyLoadContext | none (assembly must already be loaded) | nint function pointer | .NET 5+ |
GetAssemblyLoader() | load_assembly | Default AssemblyLoadContext | .deps.json via AssemblyDependencyResolver | void (loads only) | .NET 8+ |
GetAssemblyBytesLoader() | load_assembly_bytes | Default AssemblyLoadContext | none (dependencies must be pre-loaded) | void (loads only) | .NET 8+ |
Every wrapper exposes IsValid and UnderlyingPointer and has value equality. A default-constructed
wrapper (one not obtained from a context) has IsValid == false; calling its Load / Resolve throws
InvalidOperationException.
The loaders are just typed views over GetRuntimeDelegate. GetAssemblyFunctionPointerLoader()
is exactly new AssemblyFunctionPointerLoader(GetRuntimeDelegate(HostFxrDelegateType.LoadAssemblyAndGetFunctionPointer)).
If a delegate type is unavailable on the current runtime (for example requesting load_assembly on
.NET 6), the native call fails and surfaces as a HostingException — see
Error handling. The HostFxrDelegateType enum lists availability per version.
Isolated vs. default load context
The single most important distinction between the loaders is which AssemblyLoadContext (ALC) the
managed code ends up in. There is still only one runtime per process; isolation happens at the ALC
level, not the runtime level.
-
Isolated load context —
GetAssemblyFunctionPointerLoader()loads the target assembly into a fresh, isolatedAssemblyLoadContextwith anAssemblyDependencyResolverbuilt from the sibling.deps.json. Each call resolves the component and its private dependencies independently, so two components that reference different versions of the same library do not collide. This mirrors the classic .NET Core 3.0 "component activation" model. -
Default load context —
GetFunctionPointerResolver(),GetAssemblyLoader(), andGetAssemblyBytesLoader()all operate on the process's default ALC. Assemblies loaded here share one resolution scope (the app's own dependency graph).load_assemblystill applies the component's.deps.json;get_function_pointerandload_assembly_bytesdo not resolve dependencies for you — the type must already be loadable in the default context.
The pointer has process lifetime. A function pointer returned by any loader is valid for the life of the process, and the assembly it came from cannot be unloaded. Resolve each entry point once and cache the pointer; do not re-resolve on a hot path.
Describing the method signature
GetAssemblyFunctionPointerLoader().Load(…) and GetFunctionPointerResolver().Resolve(…) both take a
MethodSignature (default MethodSignature.Default). It tells the runtime how to interpret the target
static method and controls the native delegate_type_name argument. There are three kinds.
MethodSignature | MethodSignatureKind | Native delegate_type_name | Target method shape |
|---|---|---|---|
MethodSignature.Default | ComponentEntryPoint | NULL | static int M(IntPtr arg, int argSizeInBytes) |
MethodSignature.UnmanagedCallersOnly | UnmanagedCallersOnly | UNMANAGEDCALLERSONLY_METHOD sentinel | a [UnmanagedCallersOnly] static method (.NET 5+) |
MethodSignature.FromDelegateType(name) | DelegateType | assembly-qualified delegate type name | a static method matching that delegate's signature |
default(MethodSignature) equals MethodSignature.Default. The library never exposes the raw
((const char_t*)-1) sentinel — select MethodSignature.UnmanagedCallersOnly instead.
Default — the component entry point
Passing no signature (or MethodSignature.Default) targets the classic component entry point, whose
shape is fixed as int Method(IntPtr arg, int argSizeInBytes). The runtime returns a pointer that is
invoked through a managed delegate:
// In the component assembly (Plugin.dll):
namespace Plugin;
public static class Entry
{
// Matches component_entry_point_fn: int(IntPtr, int).
public static int Run(IntPtr arg, int argSizeInBytes) => 0;
}using System.Runtime.InteropServices;
// A host-side delegate that matches the fixed component_entry_point_fn shape.
internal delegate int ComponentEntryPoint(IntPtr arg, int argSizeInBytes);
// signature omitted => MethodSignature.Default
nint pointer = loader.Load(
@"C:\components\Plugin\Plugin.dll",
"Plugin.Entry, Plugin",
"Run");
var run = Marshal.GetDelegateForFunctionPointer<ComponentEntryPoint>(pointer);
int rc = run(IntPtr.Zero, 0);UnmanagedCallersOnly
MethodSignature.UnmanagedCallersOnly (.NET 5+) targets a static method annotated with
[UnmanagedCallersOnly]. This is the preferred, Native-AOT-friendly path: the returned pointer is
a genuine unmanaged function pointer you can call directly through a delegate* unmanaged<…>, with no
managed delegate marshalling. The method's parameter and return types must be blittable.
// In the component assembly (Calculator.dll):
using System.Runtime.InteropServices;
namespace Calculator;
public static class Exports
{
[UnmanagedCallersOnly]
public static int Add(int a, int b) => a + b;
}nint pointer = loader.Load(
"Calculator.dll",
"Calculator.Exports, Calculator",
"Add",
MethodSignature.UnmanagedCallersOnly);
var add = (delegate* unmanaged<int, int, int>)pointer;
int sum = add(2, 3); // 5Match the calling convention. A plain [UnmanagedCallersOnly] method with no CallConvs uses
the platform default, which matches delegate* unmanaged<…>. If you specify calling conventions on
the attribute (for example CallConvs = [typeof(CallConvCdecl)]), cast to the matching
delegate* unmanaged[Cdecl]<…>.
FromDelegateType
MethodSignature.FromDelegateType(assemblyQualifiedDelegateTypeName) targets a static method whose
signature matches a named delegate type defined in the loaded component. Use this when the method
is not [UnmanagedCallersOnly] and does not fit the component-entry-point shape. The returned pointer
is invoked through a managed delegate, so this path (like Default) involves marshalling.
// In the component assembly (Calculator.dll):
namespace Calculator;
public delegate int MultiplyDelegate(int a, int b);
public static class Exports
{
public static int Multiply(int a, int b) => a * b;
}using System.Runtime.InteropServices;
// A host-side delegate with a matching signature, used only for marshalling.
internal delegate int MultiplyDelegate(int a, int b);
nint pointer = loader.Load(
"Calculator.dll",
"Calculator.Exports, Calculator",
"Multiply",
MethodSignature.FromDelegateType("Calculator.MultiplyDelegate, Calculator"));
var multiply = Marshal.GetDelegateForFunctionPointer<MultiplyDelegate>(pointer);
int product = multiply(6, 7); // 42MethodSignature.FromDelegateType(null) or an empty string throws ArgumentException.
Complete example: isolated load + [UnmanagedCallersOnly] call
The following program initializes a host context from a component's .runtimeconfig.json, loads the
component into an isolated load context, resolves a native pointer to an [UnmanagedCallersOnly]
method, and calls it through a delegate* unmanaged<…> — the end-to-end, AOT-friendly path.
Publish the component
The component is its own project, published to C:\components\Calculator. It exposes a blittable
[UnmanagedCallersOnly] static method:
using System.Runtime.InteropServices;
namespace Calculator;
public static class Exports
{
[UnmanagedCallersOnly]
public static int Add(int a, int b) => a + b;
}Initialize a host context
HostFxr.InitializeForRuntimeConfig creates the context from the component's .runtimeconfig.json. It
starts nothing yet, so any runtime properties you need must be set now.
Request the loader (starts the runtime)
GetAssemblyFunctionPointerLoader() obtains the load_assembly_and_get_function_pointer delegate,
which starts the runtime. Runtime properties become read-only from this point.
Load and call
loader.Load(…) isolated-loads the assembly and returns a native pointer to Add. Cast it to a
delegate* unmanaged<…> and invoke it directly — no marshalling.
The host program. Unsafe function pointers require <AllowUnsafeBlocks>true</AllowUnsafeBlocks> in the
host project:
using System;
using HostFxrSharp;
using HostFxrSharp.Contexts;
using HostFxrSharp.Exceptions;
using HostFxrSharp.Loading;
internal static class Program
{
private static unsafe void Main()
{
try
{
// Initialize a host context for the component (starts nothing yet).
using HostContext host = HostFxr.InitializeForRuntimeConfig(
@"C:\components\Calculator\Calculator.runtimeconfig.json");
// Requesting the loader starts the runtime; properties are now read-only.
AssemblyFunctionPointerLoader loader = host.GetAssemblyFunctionPointerLoader();
// Isolated-load the assembly and resolve a pointer to Add.
nint pointer = loader.Load(
assemblyPath: @"C:\components\Calculator\Calculator.dll",
typeName: "Calculator.Exports, Calculator",
methodName: "Add",
signature: MethodSignature.UnmanagedCallersOnly);
// Call directly through an unmanaged function pointer — no marshalling.
var add = (delegate* unmanaged<int, int, int>)pointer;
int sum = add(2, 3);
Console.WriteLine($"2 + 3 = {sum}"); // 2 + 3 = 5
}
catch (HostingException ex)
{
Console.Error.WriteLine($"Hosting failed: {ex.Message}");
}
}
}InitializeForRuntimeConfig may be called more than once. Unlike command-line initialization,
it can create multiple (secondary) contexts and is safe to call concurrently — but all of them attach
to the same single runtime. See Host contexts.
Loading into the default context: AssemblyLoader + FunctionPointerResolver
get_function_pointer resolves a method that is already available in the default load context — it
does not load anything. On .NET 8+, pair it with load_assembly to first bring the assembly into that
context by path (which does apply the component's .deps.json):
using HostFxrSharp;
using HostFxrSharp.Contexts;
using HostFxrSharp.Loading;
unsafe void CallInDefaultContext()
{
using HostContext host = HostFxr.InitializeForRuntimeConfig(
@"C:\components\Calculator\Calculator.runtimeconfig.json");
// .NET 8+: load the assembly (and its dependencies) into the default ALC.
AssemblyLoader assemblies = host.GetAssemblyLoader();
assemblies.Load(@"C:\components\Calculator\Calculator.dll");
// .NET 5+: resolve a method now present in the default ALC.
FunctionPointerResolver resolver = host.GetFunctionPointerResolver();
nint pointer = resolver.Resolve(
"Calculator.Exports, Calculator",
"Add",
MethodSignature.UnmanagedCallersOnly);
var add = (delegate* unmanaged<int, int, int>)pointer;
int sum = add(10, 20); // 30
}AssemblyLoader.Load returns nothing — it only makes the assembly available. A null/empty path
throws ArgumentException; a native failure throws HostingException.
Loading from memory: AssemblyBytesLoader
load_assembly_bytes (.NET 8+) loads an assembly straight from a ReadOnlySpan<byte> into the default
context, with optional PDB bytes for symbols. It performs no file-based dependency resolution, so
any dependencies must already be loadable in the default context.
using System.IO;
using HostFxrSharp.Contexts;
using HostFxrSharp.Loading;
void LoadFromMemory(HostContext host)
{
byte[] assembly = File.ReadAllBytes(@"C:\components\Calculator\Calculator.dll");
byte[] symbols = File.ReadAllBytes(@"C:\components\Calculator\Calculator.pdb");
AssemblyBytesLoader bytesLoader = host.GetAssemblyBytesLoader();
bytesLoader.Load(assembly, symbols); // symbols is optional
// Resolve entry points afterwards with GetFunctionPointerResolver(), as above.
}AssemblyBytesLoader.Load takes ReadOnlySpan<byte> for both arguments. An empty assembly span
throws ArgumentException; the symbols span may be empty (the default).
Choosing a loader
| You want to… | Use | Why |
|---|---|---|
| Load a component in isolation and call a static method | GetAssemblyFunctionPointerLoader() | One-call isolated load + pointer; resolves .deps.json; widest runtime support (.NET Core 3.0+); AOT-friendly with UnmanagedCallersOnly. |
| Call a method in an assembly already present in the default context | GetFunctionPointerResolver() | No re-load; shares the app's dependency graph (.NET 5+). |
| Add an assembly (by path) to the default context, with dependency resolution | GetAssemblyLoader() | Applies the sibling .deps.json; combine with GetFunctionPointerResolver() (.NET 8+). |
| Load an assembly you already hold in memory | GetAssemblyBytesLoader() | No file on disk needed; you own dependency resolution (.NET 8+). |
Rules of thumb:
- Prefer
GetAssemblyFunctionPointerLoader()for plugin-style hosting: it is the most portable (works back to .NET Core 3.0), gives you isolation and dependency resolution in a single call, and is the path exercised most in the wild. - Prefer
MethodSignature.UnmanagedCallersOnlyfor the method itself when you can: directdelegate* unmanaged<…>invocation avoids marshalling and keeps the call site Native-AOT compatible.DefaultandFromDelegateTypego throughMarshal.GetDelegateForFunctionPointer, which is marshalling-based. - Use the default-context loaders only when you actually want components to share one resolution
scope; otherwise the isolation of
GetAssemblyFunctionPointerLoader()is safer.
Native behavior and lifetimes
- One runtime per process. All loads share a single runtime; the isolated loader isolates
AssemblyLoadContexts, not runtimes. See How hosting works. - Pointers and loaded assemblies live forever. Returned function pointers are process-lifetime and the underlying assembly cannot be unloaded. Resolve once and cache.
- Runtime start is one-way. The first
Get…Loader/Resolve/RunAppstarts the runtime and locks runtime properties. Configure them first. char_tis marshalled for you. Paths, type names, method names, and delegate type names are nativechar_t*strings — UTF-16 on Windows, UTF-8 on Unix. You always pass ordinarystrings; the library allocates and frees the native buffers. See Cross-platform.- Threading. A
HostContextis not thread-safe: use one loader call at a time per context. The function pointers you obtain, however, are ordinary native pointers you can invoke from any thread once resolved.
Error handling
Every loader validates its arguments (ArgumentException for null/empty strings or empty assembly
bytes), guards against use of a default-constructed wrapper (InvalidOperationException), and maps
native failures to a HostingException from HostFxrSharp.Exceptions. Requesting a delegate type the
current runtime does not support surfaces through the same channel (for example
UnsupportedHostingScenarioException for scenarios the runtime removed, such as WinRT activation on
.NET 5+). Wrap loading in a single try/catch (HostingException …) as shown above.
See Error handling for the full exception hierarchy and how native status codes map to managed exceptions.
Next steps
Host contexts
Initialize and dispose the context you load from.
Runtime properties
Configure the runtime before it starts and locks.
Running applications
The RunApp counterpart to loading components.
Native AOT
Why the function-pointer path stays AOT-compatible.
Error handling
The full HostingException hierarchy.
API reference
Every member of the HostFxrSharp.Loading types.