Native Code Integration in .NET: A Practical Developer Guide
Leveraging native code in .NET apps lets you push beyond the standard library's limits. This is especially handy for low-level OS APIs or hardware interfaces like MIDI. This guide covers the essentials: from basic DllImport usage to cross-platform solutions and NuGet packaging.
P/Invoke and DllImport Basics
P/Invoke (Platform Invoke) bridges managed .NET code to native functions. Here's a simple example using the DllImport attribute for the Windows API:
[DllImport("winmm.dll")]
static extern uint midiInGetNumDevs();
Key DllImport features:
- Specifying the library name and entry point
- Automatic data type marshaling
- Support for various calling conventions
Marshaling converts data types between managed and unmanaged memory. It handles:
- Primitive types (int, float)
- Strings (string to LPWSTR)
- Structures and pointers
- Allocating and freeing unmanaged memory
Cross-Platform Solutions
Building for multiple OSes brings unique challenges:
- Different system libraries:
- Windows: winmm.dll
- macOS: CoreMIDI.framework
- Linux: ALSA via libasound
- Different API functions:
- Windows: midiInGetNumDevs()
- macOS: MIDIGetNumberOfSources()
- Linux: snd_seq_* functions
- Different library paths:
- Windows: simple DLL name
- macOS: full framework path
Use RuntimeInformation to detect the OS:
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
count = midiInGetNumDevs();
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
count = MIDIGetNumberOfSources();
Building a Native Backend
A unified approach creates your own native layer to abstract platform differences.
Project structure:
- Native-Windows.cpp β Native.dll
- Native-macOS.cpp β Native.dylib
- Native-Linux.cpp β Native.so
Windows implementation example:
extern "C" __declspec(dllexport) int GetInputDevicesCount()
{
return midiInGetNumDevs();
}
macOS implementation example:
extern "C" int GetInputDevicesCount()
{
return MIDIGetNumberOfSources();
}
Benefits:
- Single API across platforms
- Isolates platform-specific code
- Simplifies C# code
- Allows extra logic
Building and Managing Binaries
Native library builds vary by platform:
Windows (MSVC):
cl /LD Native-Windows.cpp /link /OUT:Native.dll
macOS (Clang):
clang++ -dynamiclib -o Native.dylib Native-macOS.cpp -framework CoreMIDI
Linux (GCC):
g++ -shared -fPIC -o Native.so Native-Linux.cpp -lasound
Project file layout:
project/
βββ src/
β βββ Native/
β β βββ Windows/
β β β βββ x64/
β β β β βββ Native.dll
β β β βββ arm64/
β β β βββ Native.dll
β β βββ macOS/
β β β βββ x64/
β β β β βββ Native.dylib
β β β βββ arm64/
β β β βββ Native.dylib
β β βββ Linux/
β β βββ x64/
β β βββ Native.so
β βββ Managed/
β βββ MidiWrapper.cs
βββ MidiWrapper.csproj
Modern P/Invoke Approaches
LibraryImport Attribute
.NET 7+ introduces LibraryImport, which generates marshaling code at compile time:
[LibraryImport("Native")]
internal static partial int GetInputDevicesCount();
LibraryImport advantages:
- Compile-time code generation
- Better performance
- Stricter type checking
- Source generator support
Safe Resource Management
IntPtr vs SafeHandle:
- IntPtr: raw pointer, manual management
- SafeHandle: safe wrapper with auto-disposal
SafeHandle example:
sealed class MidiHandle : SafeHandle
{
public MidiHandle() : base(IntPtr.Zero, true) { }
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
return midiInClose(handle) == 0;
}
}
Callbacks and Async Handling
Callback challenges:
- Can't call some native functions from callbacks
- Needs thread synchronization
- Deadlock risks with misuse
MIDI buffer pattern:
- Pre-allocate multiple buffers
- Reuse buffers with data in callbacks
- Avoid unprepared/close calls in callbacks
- Use thread-safe data structures
Safe callback example:
private static void MidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2)
{
// Extract data from buffer
var data = ExtractMidiData(param1);
// Async processing
Task.Run(() => ProcessMidiDataAsync(data));
// Re-add buffer
lock (bufferLock)
{
midiInAddBuffer(handle, param1, Marshal.SizeOf<MIDIHDR>());
}
}
Packaging for NuGet
NuGet structure with natives:
<PackageReference>
<NativeLibs>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<NativeLib>runtimes/win-x64/native/Native.dll</NativeLib>
</NativeLibs>
<NativeLibs>
<RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
<NativeLib>runtimes/osx-arm64/native/Native.dylib</NativeLib>
</NativeLibs>
</PackageReference>
.csproj directives:
<ItemGroup>
<Content Include="runtimes\win-x64\native\Native.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackagePath>runtimes/win-x64/native/</PackagePath>
</Content>
<Content Include="runtimes\osx-arm64\native\Native.dylib">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackagePath>runtimes/osx-arm64/native/</PackagePath>
</Content>
</ItemGroup>
Key points:
- Organize by RID (Runtime Identifier)
- Target platforms in .csproj
- Test across systems
- Support x64 and ARM64
Performance Optimization
Reduce overhead:
- Minimize managed/unmanaged transitions
- Use blittable types (no marshaling needed)
- Cache handles and pointers
- Batch calls
Blittable types:
- byte, sbyte, short, ushort, int, uint, long, ulong
- float, double
- Structs with only blittables
Optimized struct:
[StructLayout(LayoutKind.Sequential)]
struct MidiMessage
{
public uint Timestamp;
public byte Status;
public byte Data1;
public byte Data2;
public byte Reserved;
}
Debugging and Diagnostics
Native debugging tools:
- WinDbg and CDB (Windows)
- LLDB (macOS/Linux)
- Visual Studio Mixed Mode
- JetBrains Rider native support
Common issues:
- Access Violation: Bad type marshaling
- Memory Leaks: Unfreed resources
- Deadlocks: Callback locks
- Performance: Frequent P/Invoke
Call logging:
[DllImport("Native", EntryPoint = "GetInputDevicesCount")]
private static extern int NativeGetInputDevicesCount();
public static int GetInputDevicesCount()
{
Logger.Debug("Calling native GetInputDevicesCount");
var result = NativeGetInputDevicesCount();
Logger.Debug($
β Editorial Team
No comments yet.