AdaskoTheBeAsT.Interop.Unmanaged 3.0.0

dotnet add package AdaskoTheBeAsT.Interop.Unmanaged --version 3.0.0
                    
NuGet\Install-Package AdaskoTheBeAsT.Interop.Unmanaged -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AdaskoTheBeAsT.Interop.Unmanaged" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AdaskoTheBeAsT.Interop.Unmanaged" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="AdaskoTheBeAsT.Interop.Unmanaged" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AdaskoTheBeAsT.Interop.Unmanaged --version 3.0.0
                    
#r "nuget: AdaskoTheBeAsT.Interop.Unmanaged, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AdaskoTheBeAsT.Interop.Unmanaged@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AdaskoTheBeAsT.Interop.Unmanaged&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=AdaskoTheBeAsT.Interop.Unmanaged&version=3.0.0
                    
Install as a Cake Tool

AdaskoTheBeAsT.Interop.Unmanaged

πŸš€ Typed, safe, lifetime-managed dynamic native library loading for .NET β€” on Windows, Linux, and macOS.

NuGet NuGet Downloads License: MIT TFMs Platforms Warnings Deterministic

πŸ”¬ Code quality β€” SonarCloud

Quality Gate Status Coverage Maintainability Rating Reliability Rating Security Rating Bugs Vulnerabilities Code Smells Duplicated Lines (%) Technical Debt Lines of Code


πŸ‘‹ Hello, native-loving friend

You've got a native library. The static DllImport attribute is fine, right up until it isn't:

  • 🧬 the DLL name depends on runtime config or an installer location
  • 🎯 some exports only exist on certain versions of the SDK
  • 🏒 you need LOAD_LIBRARY_SEARCH_SYSTEM32 and friends to survive DLL-hijacking audits
  • 🐧 you want the same loader story on Linux and macOS
  • πŸͺ native code wants a function pointer to a managed callback
  • ♻️ you want SafeHandle lifetime β€” not a Marshal.FreeLibrary landmine

AdaskoTheBeAsT.Interop.Unmanaged is the tiny, focused library that parks all that plumbing somewhere safe so your code stays readable. πŸ“¦


✨ Why you'll love this

  • πŸ”’ SafeHandle-backed lifetime. SafeLibraryHandle cleans up on its own β€” no more "did I remember to FreeLibrary?" moments.
  • 🎯 Strongly typed delegates. Resolve exports into real delegate types, not IntPtr soup.
  • πŸͺ Managed-to-native callbacks. GetFunctionPointerForDelegate gives you a stable pointer plus a binder root so the GC can't rip it out from under native code.
  • πŸ”€ Open-generic delegate support. Callbacks typed as Delegate<T>? The library IL-emits a concrete proxy on the fly and copies your [UnmanagedFunctionPointer] attribute onto it so the calling convention matches.
  • πŸ§ͺ Runtime export probing. Missing function? GetUnmanagedFunction<T> returns null, TryGetExport returns false. No exceptions, no try/catch dances.
  • 🧭 Modern delegate* unmanaged friendly. TryGetExport hands you the raw IntPtr so you can cast to delegate* unmanaged[Stdcall]<int, int> on net5+.
  • πŸͺŸπŸ§πŸŽ Windows + Linux + macOS. Uses LoadLibraryEx on Windows; delegates to NativeLibrary on modern .NET and raw dlopen on .NET Framework via Mono.
  • βš™οΈ Full LoadLibraryFlags control. LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_WITH_ALTERED_SEARCH_PATH, LOAD_LIBRARY_AS_DATAFILE β€” all there, faithfully honored on Windows.
  • 🧬 9 TFMs, all green. net10.0, net9.0, net8.0, net481, net48, net472, net471, net47, net462 β€” the full matrix on every build.
  • πŸ›‘οΈ Quality-first. TreatWarningsAsErrors=true, deterministic builds, nullable annotations, generated XML docs, and a SonarCloud quality gate on every commit.
  • ✏️ Source Link + snupkg. Step into the library from your debugger without guessing.

πŸ“¦ Installation

dotnet add package AdaskoTheBeAsT.Interop.Unmanaged

Or via Package Manager:

Install-Package AdaskoTheBeAsT.Interop.Unmanaged

Symbols ship as .snupkg with Source Link and embedded untracked sources. Step in. Look around. It's fine. πŸ”


πŸ—ΊοΈ Target framework matrix

TFM Status Notes
net10.0 βœ… Uses System.Runtime.InteropServices.NativeLibrary under the hood on non-Windows.
net9.0 βœ… Same.
net8.0 βœ… Same.
net481 βœ… Windows desktop; other platforms go through hand-rolled dlopen/dlsym P/Invokes under Mono.
net48 βœ… Same.
net472 βœ… Same.
net471 βœ… Same.
net47 βœ… Same.
net462 βœ… Same.

Every cell is built with TreatWarningsAsErrors=true, ContinuousIntegrationBuild=true, Deterministic=true, and exercised by the test suite.

πŸͺŸπŸ§πŸŽ Platform behavior

Platform Loader path LoadLibraryFlags honored?
πŸͺŸ Windows (all TFMs) LoadLibraryEx / GetProcAddress / FreeLibrary from kernel32.dll βœ… fully
🐧 Linux on net8+ NativeLibrary.Load ❌ flags silently ignored (RTLD_NOW semantics)
🍎 macOS on net8+ NativeLibrary.Load ❌ flags silently ignored (RTLD_NOW semantics)
🐧 Linux on net4.x (Mono) dlopen(..., RTLD_NOW) from libdl.so.2 ❌ flags silently ignored
🍎 macOS on net4.x (Mono) dlopen(..., RTLD_NOW) from libSystem.dylib ❌ flags silently ignored

πŸ’‘ The LoadLibraryFlags argument is accepted on every platform for call-site compatibility β€” it's only applied on Windows, which is exactly what most interop callers expect.


πŸš€ Quick start

1️⃣ Load a DLL and call an export

using System;
using System.Runtime.InteropServices;
using AdaskoTheBeAsT.Interop.Unmanaged;

[UnmanagedFunctionPointer(CallingConvention.Winapi)]
delegate uint GetCurrentProcessIdDelegate();

using var library = new UnmanagedLibrary("kernel32.dll");
var getCurrentProcessId = library.GetUnmanagedFunction<GetCurrentProcessIdDelegate>("GetCurrentProcessId");

if (getCurrentProcessId is not null)
{
    Console.WriteLine($"Current PID: {getCurrentProcessId()}");
}

2️⃣ Probe for optional exports safely

GetUnmanagedFunction<TDelegate> returns null when the export is missing, which makes feature-probing trivial β€” no exceptions, no Marshal.GetLastWin32Error rituals.

[UnmanagedFunctionPointer(CallingConvention.Winapi)]
delegate IntPtr OptionalExportDelegate();

using var library = new UnmanagedLibrary("SomeNativeSdk.dll");
var optionalExport = library.GetUnmanagedFunction<OptionalExportDelegate>("OptionalExport");

if (optionalExport is null)
{
    Console.WriteLine("This version of the native SDK does not expose OptionalExport.");
}

3️⃣ Load from an explicit path with explicit flags

Use a fully qualified path when you want deterministic loading behavior for a specific DLL. Combine flags with |.

var flags =
    LoadLibraryFlags.LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR |
    LoadLibraryFlags.LOAD_LIBRARY_SEARCH_SYSTEM32;

using var library = new UnmanagedLibrary(@"C:\Native\MyLibrary.dll", flags);

4️⃣ Static, handle-based API

Prefer to manage the handle yourself? The static helpers are there.

using System;
using System.Runtime.InteropServices;
using AdaskoTheBeAsT.Interop.Unmanaged;

[UnmanagedFunctionPointer(CallingConvention.Winapi)]
delegate uint GetTickCountDelegate();

using var handle = UnmanagedLibrary.LoadLibrary("kernel32.dll");
var getTickCount = UnmanagedLibrary.GetUnmanagedFunction<GetTickCountDelegate>(handle, "GetTickCount");

if (getTickCount is not null)
{
    Console.WriteLine($"Tick count: {getTickCount()}");
}

5️⃣ Pass a managed callback to native code

When you hand a managed delegate to unmanaged code, keep the returned binder alive for as long as native code may store or invoke the pointer.

using System;
using AdaskoTheBeAsT.Interop.Unmanaged;

Func<int, int, int> callback = (a, b) => a + b;

var callbackPointer = UnmanagedLibrary.GetFunctionPointerForDelegate(callback, out var binder);

// Pass callbackPointer into native code here.
// ...

GC.KeepAlive(binder);

πŸ’‘ For open-generic delegate types (e.g. Action<T>), the library IL-emits a non-generic proxy delegate at runtime and copies your [UnmanagedFunctionPointer] attribute onto the proxy so the generated function pointer uses the correct unmanaged calling convention.

6️⃣ Modern delegate* unmanaged via TryGetExport

On net5+ you can skip the Marshal layer entirely and use a function-pointer type (delegate* unmanaged[Stdcall]<...>). TryGetExport gives you the raw IntPtr.

using System;
using AdaskoTheBeAsT.Interop.Unmanaged;

using var library = new UnmanagedLibrary("kernel32.dll");

if (library.TryGetExport("GetCurrentProcessId", out var addr))
{
    unsafe
    {
        var fn = (delegate* unmanaged[Stdcall]<uint>)addr;
        Console.WriteLine($"PID: {fn()}");
    }
}

Static overload exists too:

using var handle = UnmanagedLibrary.LoadLibrary("kernel32.dll");
UnmanagedLibrary.TryGetExport(handle, "GetCurrentProcessId", out var addr);

🧠 API at a glance

UnmanagedLibrary

Main entry point for loading DLLs and resolving exports.

// Instance API
new UnmanagedLibrary(string fileName, LoadLibraryFlags flags = ...);
TDelegate? GetUnmanagedFunction<TDelegate>(string functionName);
bool TryGetExport(string functionName, out IntPtr functionPointer);

// Static / handle-based API
static SafeLibraryHandle LoadLibrary(string fileName, LoadLibraryFlags flags = ...);
static void FreeLibrary(SafeLibraryHandle? safeLibraryHandle);
static TDelegate? GetUnmanagedFunction<TDelegate>(SafeLibraryHandle handle, string functionName);
static bool TryGetExport(SafeLibraryHandle handle, string functionName, out IntPtr functionPointer);

// Managed-to-native callbacks
static IntPtr GetFunctionPointerForDelegate<T>(T delegateCallback, out object binder);

// IL-emit re-wrap (advanced; prefer `Marshal.GetDelegateForFunctionPointer<T>`)
static T? GetDelegateForFunctionPointer<T>(IntPtr ptr, CallingConvention callingConvention);

SafeLibraryHandle

Wraps the native module handle using the .NET SafeHandle pattern, which helps prevent leaks and double-free mistakes. using-friendly.

LoadLibraryFlags

[Flags] enum mirroring the Windows LoadLibraryEx flags β€” LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_WITH_ALTERED_SEARCH_PATH, LOAD_LIBRARY_AS_DATAFILE, LOAD_IGNORE_CODE_AUTHZ_LEVEL, and friends.

DelegatePin

Internal helper that roots generic-delegate proxies so the JIT-generated bridge stays alive. Most consumers only need to keep the returned binder rooted β€” DelegatePin is exposed for edge cases where you're doing the wrapping yourself.


πŸ›‘οΈ Safety and lifetime rules

These are the few things you actually need to remember:

  • βœ… Keep the UnmanagedLibrary / SafeLibraryHandle alive while retrieved delegates or function pointers are still in use
  • βœ… Keep the callback binder alive while native code may call the function pointer
  • βœ… Use the exact delegate signature and calling convention expected by the native export
  • βœ… Prefer explicit LOAD_LIBRARY_SEARCH_* flags when loading third-party binaries (audit-friendly)
  • ❌ Do not unload the library and continue using delegates or function pointers you obtained from it
  • ❌ Do not load untrusted DLLs 🚫

⚠️ Important behavior notes

  • Export names are case-sensitive πŸ”  (this matches native loader semantics on Linux/macOS and avoids surprises on Windows)
  • Invalid file names throw ArgumentException
  • Failed loads throw Win32Exception with Failed to load library '<name>'; the trailing message is the native loader error string on Windows and on the .NET Framework/Mono dlopen path, and the wrapped managed-exception message (DllNotFoundException / BadImageFormatException / FileLoadException) on .NET 8+ non-Windows
  • Missing exports return null (classic API) or false (TryGetExport) β€” never throw
  • FreeLibrary is safe to call with null or an already closed handle (idempotent)
  • LoadLibraryFlags are silently ignored on Linux/macOS; the library always passes RTLD_NOW on those platforms
  • The IL-emit path in GetDelegateForFunctionPointer<T> does not perform parameter marshaling β€” for string / struct marshaling use Marshal.GetDelegateForFunctionPointer<T>(ptr) instead

πŸ€” When to reach for this library

Use this when you need any of:

Scenario Why DllImport isn't enough
πŸ”€ DLL path chosen at runtime DllImport wants a compile-time string
🎯 Optional / version-specific exports DllImport throws EntryPointNotFoundException
🏒 Explicit LOAD_LIBRARY_SEARCH_* flags DllImport uses default loader search order
πŸͺ Managed callback ↔ native function pointer Marshal.GetFunctionPointerForDelegate is OK, but you have to manage the GC root yourself
🧬 delegate* unmanaged[X]<...> from a dynamically loaded DLL DllImport doesn't apply; you need dlsym/GetProcAddress
πŸ”’ SafeHandle-backed native module lifetime Marshal.FreeLibrary is a foot-gun

If the DLL is fixed at compile time and every export is always present, DllImport (or [LibraryImport] on net7+) is absolutely the right tool. πŸ‘


πŸ’‘ Common use cases

  • πŸ”§ Loading Windows system DLLs such as kernel32.dll or user32.dll
  • 🏒 Dynamically integrating with third-party native SDKs whose install path you read at runtime
  • 🎯 Supporting optional native features across multiple versions of the same DLL
  • πŸͺ Registering managed callbacks with unmanaged code
  • 🐧🍎 Writing cross-platform interop that speaks to platform-specific shared libraries (libfoo.so, libfoo.dylib, foo.dll)
  • 🧱 Choosing DLL resolution behavior explicitly to reduce DLL-hijacking risk

πŸ§ͺ Build and test

dotnet build .\AdaskoTheBeAsT.Interop.Unmanaged.slnx
dotnet test  .\AdaskoTheBeAsT.Interop.Unmanaged.slnx --no-build

The test suite runs across the full 9-TFM matrix. Windows-specific tests (things that call kernel32) self-skip on non-Windows hosts.


πŸ§ͺ Quality notes

This project is built with quality-oriented defaults:

  • πŸ›‘οΈ Nullable reference types enabled
  • πŸ“ Generated XML documentation in every package
  • 🚨 TreatWarningsAsErrors=true across all projects
  • πŸ§ͺ Automated tests across net462–net481 and net8–net10
  • πŸ”¬ Static analysis via Roslyn analyzers + SonarCloud quality gate
  • 🧬 Deterministic, ContinuousIntegrationBuild=true packages
  • πŸ” Source Link + snupkg symbols for step-in debugging

❓ FAQ

Do I need to pass a full DLL path?

Not always. A bare module name like kernel32.dll works when the selected flags can resolve it. Use a fully qualified path when you want deterministic loading from a specific location.

What happens if the export does not exist?

GetUnmanagedFunction<TDelegate> returns null. TryGetExport returns false with IntPtr.Zero. No exceptions.

Do I need to use DelegatePin directly?

Usually no. Most consumers only need to keep the binder returned by GetFunctionPointerForDelegate rooted for the required lifetime.

Can I use this on Linux and macOS?

Yes. πŸŽ‰ On net8+ the non-Windows path delegates to System.Runtime.InteropServices.NativeLibrary; on .NET Framework under Mono it falls back to direct dlopen/dlsym P/Invokes. Note that LoadLibraryFlags are silently ignored on non-Windows platforms and RTLD_NOW is used.

Which should I use: GetUnmanagedFunction<T> or TryGetExport + delegate*?

On net5+ with unsafe code, TryGetExport + delegate* unmanaged[Stdcall]<...> gives you zero-alloc direct calls. On older TFMs, or when you want marshaling (strings, structs), stick with GetUnmanagedFunction<T>.

Can I re-wrap a raw IntPtr back into a delegate?

Yes β€” GetDelegateForFunctionPointer<T>(ptr, callingConvention) IL-emits a thunk using calli. Note: this path does not marshal parameters. For marshaling (e.g. string ↔ LPWStr), use Marshal.GetDelegateForFunctionPointer<T>(ptr) instead.

Does it support musl-based Linux (Alpine)?

On net8+ it does β€” NativeLibrary.Load handles the search. On .NET Framework under Mono, the current hard-coded soname is libdl.so.2, which works on glibc distros; Alpine/musl is not explicitly supported on the Mono path.


πŸ™‹ Contributing

Found a bug? Got an idea? Spotted a typo that's been haunting you? πŸ‘»

  1. πŸ™ Open an issue describing the problem or the proposal.
  2. πŸ› οΈ Fork + branch (feature/your-idea).
  3. βœ… Run dotnet build + dotnet test across the full matrix.
  4. ✨ Add/update tests β€” the strict-build settings will tell you if something's off.
  5. πŸš€ Open a PR β€” the CI will do the rest.

πŸ“„ License

This project is licensed under the MIT License.


<p align="center"> Because <em>dynamic</em> native DLL loading in .NET shouldn't feel like defusing a bomb. πŸ’£βž‘οΈπŸ•ŠοΈ<br/> Made with ❀️ (and a lot of coffee β˜•) by <a href="https://github.com/AdaskoTheBeAsT">AdaskoTheBeAsT</a>. </p>

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Framework net462 is compatible.  net463 was computed.  net47 is compatible.  net471 is compatible.  net472 is compatible.  net48 is compatible.  net481 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on AdaskoTheBeAsT.Interop.Unmanaged:

Package Downloads
AdaskoTheBeAsT.WkHtmlToX

AdaskoTheBeAsT.WkHtmlToX c# wrapper

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 230 4/20/2026
2.0.0 105 4/7/2026
1.0.0 204 11/23/2025