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
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
dllPathstringThe path to the DLL to load.
Returns
- T
An instance of the derived class.
Type Parameters
TA 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
disposingbool
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
dllPathstringThe path to the DLL.
callingConventionCallingConventionThe calling convention of the functions.
Returns
- TInterface
An instance of the emitted class implementing the interface
TInterface.
Type Parameters
TInterfaceThe 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
dllPathstringThe path to the DLL.
callingConventionCallingConventionThe calling convention of the functions.
workerCountintNumber of independent worker processes to round-robin across. Must be at least 1.
loadTimeoutTimeSpan?Maximum time to wait for each worker's response to its load request. Defaults to 30 seconds when null.
callTimeoutTimeSpan?Maximum time to wait for a worker's response to each forwarded call. Defaults to 30 seconds when null.
Returns
- TInterface
A proxy implementing
TInterfacethat forwards each call to the next worker in round-robin order.
Type Parameters
TInterfaceThe 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
workerCountis less than 1.- NotSupportedException
Thrown when
TInterfaceuses 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
dllPathstringThe path to the DLL.
callingConventionCallingConventionThe calling convention of the functions.
loadTimeoutTimeSpan?Maximum time to wait for the worker to load
dllPathand emit the mapping class. Defaults to 30 seconds when null.callTimeoutTimeSpan?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
TInterfacethat forwards every call to the isolated worker.
Type Parameters
TInterfaceThe 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
TInterfaceuses 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
argsstring[]The current process's command-line arguments, unmodified.
Returns
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.