Native Code Integration in .NET: Praktischer Entwicklerleitfaden
Die Nutzung von nativem Code in .NET-Anwendungen ermöglicht es, die Grenzen der Standardbibliothek zu überschreiten. Das ist besonders nützlich für Low-Level-OS-APIs oder Hardware-Schnittstellen wie MIDI. Dieser Leitfaden deckt die Grundlagen ab: von einfachem DllImport-Einsatz bis hin zu plattformübergreifenden Lösungen und NuGet-Verpackung.
P/Invoke und DllImport Grundlagen
P/Invoke (Platform Invoke) verbindet verwalteten .NET-Code mit nativen Funktionen. Hier ein einfaches Beispiel mit dem DllImport-Attribut für die Windows-API:
[DllImport("winmm.dll")]
static extern uint midiInGetNumDevs();
Wichtige DllImport-Features:
- Angabe des Bibliotheksnamens und Einstiegspunkts
- Automatisches Marshalling von Datentypen
- Unterstützung verschiedener Aufrufkonventionen
Marshalling wandelt Datentypen zwischen verwaltetem und nicht verwaltetem Speicher um. Es behandelt:
- Primitive Typen (int, float)
- Strings (string zu LPWSTR)
- Strukturen und Zeiger
- Allokieren und Freigeben von nicht verwaltetem Speicher
Plattformübergreifende Lösungen
Die Entwicklung für mehrere Betriebssysteme bringt besondere Herausforderungen mit sich:
- Unterschiedliche Systembibliotheken:
- Windows: winmm.dll
- macOS: CoreMIDI.framework
- Linux: ALSA via libasound
- Unterschiedliche API-Funktionen:
- Windows: midiInGetNumDevs()
- macOS: MIDIGetNumberOfSources()
- Linux: snd_seq_* Funktionen
- Unterschiedliche Bibliothekspfade:
- Windows: einfacher DLL-Name
- macOS: vollständiger Framework-Pfad
Verwenden Sie RuntimeInformation, um das OS zu erkennen:
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
count = midiInGetNumDevs();
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
count = MIDIGetNumberOfSources();
Aufbau eines nativen Backends
Ein einheitlicher Ansatz schafft eine eigene native Schicht, die Plattformunterschiede abstrahiert.
Projektstruktur:
- Native-Windows.cpp → Native.dll
- Native-macOS.cpp → Native.dylib
- Native-Linux.cpp → Native.so
Windows-Implementierungsbeispiel:
extern "C" __declspec(dllexport) int GetInputDevicesCount()
{
return midiInGetNumDevs();
}
macOS-Implementierungsbeispiel:
extern "C" int GetInputDevicesCount()
{
return MIDIGetNumberOfSources();
}
Vorteile:
- Einheitliche API über Plattformen hinweg
- Isolierung von plattformspezifischem Code
- Vereinfachung des C#-Codes
- Ermöglicht zusätzliche Logik
Erstellung und Verwaltung von Binaries
Der Build nativer Bibliotheken unterscheidet sich je nach Plattform:
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
Projektstruktur:
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
Moderne P/Invoke-Ansätze
LibraryImport-Attribut
.NET 7+ führt LibraryImport ein, das Marshalling-Code zur Kompilierzeit generiert:
[LibraryImport("Native")]
internal static partial int GetInputDevicesCount();
LibraryImport-Vorteile:
- Codegenerierung zur Kompilierzeit
- Bessere Performance
- Strengere Typprüfung
- Source-Generator-Unterstützung
Sichere Ressourcenverwaltung
IntPtr vs SafeHandle:
- IntPtr: roher Zeiger, manuelle Verwaltung
- SafeHandle: sicherer Wrapper mit automatischer Entsorgung
SafeHandle-Beispiel:
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 und asynchrone Verarbeitung
Callback-Herausforderungen:
- Einige native Funktionen können nicht aus Callbacks aufgerufen werden
- Thread-Synchronisation erforderlich
- Deadlock-Risiken bei Fehlnutzung
MIDI-Buffer-Muster:
- Mehrere Buffer im Voraus allokieren
- Buffer mit Daten in Callbacks wiederverwenden
- Unvorbereitete/Schließen-Aufrufe in Callbacks vermeiden
- Thread-sichere Datenstrukturen nutzen
Sicheres Callback-Beispiel:
private static void MidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2)
{
// Daten aus Buffer extrahieren
var data = ExtractMidiData(param1);
// Asynchrone Verarbeitung
Task.Run(() => ProcessMidiDataAsync(data));
// Buffer erneut hinzufügen
lock (bufferLock)
{
midiInAddBuffer(handle, param1, Marshal.SizeOf<MIDIHDR>());
}
}
Verpackung für NuGet
NuGet-Struktur mit nativen Bibliotheken:
<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-Direktiven:
<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>
Wichtige Punkte:
- Nach RID (Runtime Identifier) organisieren
- Zielplattformen in .csproj definieren
- Über Systeme hinweg testen
- x64 und ARM64 unterstützen
Performance-Optimierung
Overhead reduzieren:
- Verwaltete/nicht verwaltete Übergänge minimieren
- Blittable Typen nutzen (kein Marshalling nötig)
- Handles und Zeiger cachen
- Aufrufe bündeln
Blittable Typen:
- byte, sbyte, short, ushort, int, uint, long, ulong
- float, double
- Strukturen nur mit Blittables
Optimierte Struktur:
[StructLayout(LayoutKind.Sequential)]
struct MidiMessage
{
public uint Timestamp;
public byte Status;
public byte Data1;
public byte Data2;
public byte Reserved;
}
Debugging und Diagnose
Native-Debugging-Tools:
- WinDbg und CDB (Windows)
- LLDB (macOS/Linux)
- Visual Studio Mixed Mode
- JetBrains Rider Native-Support
Häufige Probleme:
- Access Violation: Fehlschlagendes Typ-Marshalling
- Memory Leaks: Nicht freigegebene Ressourcen
- Deadlocks: Callback-Sperren
- Performance: Häufiges P/Invoke
Aufruf-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
Noch keine Kommentare.