Table of Contents

Class LibraryMapper

Namespace
Utils.Reflection
Assembly
Utils.Reflection.dll

A class to dynamically map unmanaged DLL functions to .NET properties or fields. This class also handles platform differences (Windows vs. Unix-based systems).

public abstract class LibraryMapper : IDisposable
Inheritance
LibraryMapper
Implements
Inherited Members
Extension Methods

Properties

IsDisposed

Indicates whether this mapper instance has been disposed.

public bool IsDisposed { get; }

Property Value

bool

Methods

Create<T>(string)

Creates an instance of a derived LibraryMapper class and maps the specified DLL functions to the instance's properties and fields.

public static T Create<T>(string dllPath) where T : LibraryMapper, new()

Parameters

dllPath string

The path to the DLL to load.

Returns

T

An instance of the derived class.

Type Parameters

T

A class derived from LibraryMapper.

Dispose()

Releases the resources used by the LibraryMapper class.

public void Dispose()

Dispose(bool)

protected virtual void Dispose(bool disposing)

Parameters

disposing bool

EmitInProcess<TInterface>(string, CallingConvention)

Dynamically emits a class in the current process and maps DLL functions to its members based on an interface, without any process isolation.

[Experimental("UTILSREFL001")]
public static TInterface EmitInProcess<TInterface>(string dllPath, CallingConvention callingConvention) where TInterface : class, IDisposable

Parameters

dllPath string

The path to the DLL.

callingConvention CallingConvention

The calling convention of the functions.

Returns

TInterface

An instance of the emitted class implementing the interface TInterface.

Type Parameters

TInterface

The interface that defines the functions to map.

Remarks

Security warning: the code generator (EmitDllMappableClass) builds C# source by concatenating type, method and parameter names obtained through reflection on TInterface. CLR metadata names are far less constrained than C# lexical identifiers, so an interface sourced from an untrusted or dynamically generated assembly could inject arbitrary C# — including a static constructor that runs with the full trust of this process as soon as the emitted type is loaded. Only call this method with interfaces you fully trust (typically ones you compiled yourself). Prefer Emit<TInterface>(string, CallingConvention, TimeSpan?, TimeSpan?), which runs the same code generation inside an isolated worker process, for anything else.

EmitRoundRobin<TInterface>(string, CallingConvention, int, TimeSpan?, TimeSpan?)

Like Emit<TInterface>(string, CallingConvention, TimeSpan?, TimeSpan?), but spreads calls across workerCount independent isolated worker processes instead of one, picking the next one in round-robin order for every call.

public static TInterface EmitRoundRobin<TInterface>(string dllPath, CallingConvention callingConvention, int workerCount, TimeSpan? loadTimeout = null, TimeSpan? callTimeout = null) where TInterface : class, IDisposable

Parameters

dllPath string

The path to the DLL.

callingConvention CallingConvention

The calling convention of the functions.

workerCount int

Number of independent worker processes to round-robin across. Must be at least 1.

loadTimeout TimeSpan?

Maximum time to wait for each worker's response to its load request. Defaults to 30 seconds when null.

callTimeout TimeSpan?

Maximum time to wait for a worker's response to each forwarded call. Defaults to 30 seconds when null.

Returns

TInterface

A proxy implementing TInterface that forwards each call to the next worker in round-robin order.

Type Parameters

TInterface

The interface that defines the functions to map.

Remarks

Emit<TInterface>(string, CallingConvention, TimeSpan?, TimeSpan?)'s single worker already accepts several calls in flight at once (see InvokeMethod(int, MethodInfo, object[])) — but every one of them still ends up executing inside the very same worker process, so the native library backing TInterface must itself be safe to call concurrently for that to be safe. Round-robining across workerCount separate processes instead avoids that requirement entirely: each process has its own independent load of the native DLL, so concurrent calls can never race inside the same one. The trade-off is cost: this starts workerCount full sandboxed worker processes up front (each paying the same per-process startup cost as a single Emit<TInterface>(string, CallingConvention, TimeSpan?, TimeSpan?) call), not one.

The set of workers is fixed for the lifetime of the returned proxy — there is no dynamic scaling, health-checking, or replacement of a worker that dies mid-flight; a dead worker's share of subsequent round-robin turns simply starts failing with whatever exception InvokeMethod(int, MethodInfo, object[]) raises for it (typically an InvalidOperationException once its connection is detected as broken). Disposing the returned proxy disposes every worker in the set.

Exceptions

ArgumentOutOfRangeException

Thrown when workerCount is less than 1.

NotSupportedException

Thrown when TInterface uses a type that cannot cross a process boundary.

Emit<TInterface>(string, CallingConvention, TimeSpan?, TimeSpan?)

Maps DLL functions to an interface, isolating the (untrusted-input-sensitive) code generation and native calls in a separate worker process.

public static TInterface Emit<TInterface>(string dllPath, CallingConvention callingConvention, TimeSpan? loadTimeout = null, TimeSpan? callTimeout = null) where TInterface : class, IDisposable

Parameters

dllPath string

The path to the DLL.

callingConvention CallingConvention

The calling convention of the functions.

loadTimeout TimeSpan?

Maximum time to wait for the worker to load dllPath and emit the mapping class. Defaults to 30 seconds when null.

callTimeout TimeSpan?

Maximum time to wait for the worker's response to each native call forwarded through the returned proxy. Defaults to 30 seconds when null.

Returns

TInterface

A proxy implementing TInterface that forwards every call to the isolated worker.

Type Parameters

TInterface

The interface that defines the functions to map.

Remarks

This is the recommended, safe-by-default way to map an interface to a native DLL: the worker process is a copy of the current executable, re-launched with the strongest available OS sandbox (see ProcessContainerFactory), so that even a maliciously crafted TInterface (see EmitDllMappableClass) can only do as much damage as the sandbox permissions allow, rather than running with the full trust of the calling process.

Only interfaces whose members use types that can be represented as JSON (primitives, string, enums, arrays/structs made of these) can be mapped this way, since every call is forwarded to the worker and back; pointers and handles (nint, unmanaged pointers) are never supported because they are meaningless outside the process that produced them. Use EmitInProcess<TInterface>(string, CallingConvention) for interfaces that need them.

Requires the host application to call RunWorkerIfRequested(string[]) at the very start of its entry point (before any other startup logic), so the re-launched copy of the process can recognize that it should run as a worker instead of starting normally.

Exceptions

NotSupportedException

Thrown when TInterface uses a type that cannot cross a process boundary.

TimeoutException

Thrown by this method (initial load) or by the returned proxy's members (subsequent calls) when the worker does not respond within loadTimeout/callTimeout.

Important: a per-call timeout abandons only the timed-out request on the host side; the worker process is not killed and remains usable for subsequent calls. The native call inside the worker continues to execute on its own thread until it finishes, and the eventual response is silently discarded. Callers must treat the outcome of a timed-out call as indeterminate and should not assume any side effect was or was not applied.

~LibraryMapper()

protected ~LibraryMapper()

RunWorkerIfRequested(string[])

Entry point for the isolated Emit worker. Host applications that use Emit<TInterface>(string, CallingConvention, TimeSpan?, TimeSpan?) must call this as the very first statement of their own entry point (before any other startup logic), passing the raw process arguments through unchanged.

public static bool RunWorkerIfRequested(string[] args)

Parameters

args string[]

The current process's command-line arguments, unmodified.

Returns

bool

true when this process ran as an isolated Emit worker; otherwise false.

Remarks

When the process was re-launched by Emit<TInterface>(string, CallingConvention, TimeSpan?, TimeSpan?) to act as an isolated worker, this method never returns until the worker's named-pipe connection ends: it runs the request/response loop and then returns true, at which point the caller should exit immediately without running its normal startup logic. When the process is running normally (no worker marker in args), this method returns false immediately and the caller should proceed as usual.