Zurück zur Startseite

Integration von nativem Code in .NET: P/Invoke, DllImport, plattformübergreifende Kompatibilität

Der Artikel ist ein detaillierter Leitfaden zur Integration von nativem Code in .NET-Anwendungen. Er behandelt P/Invoke- und DllImport-Mechanismen, die Erstellung plattformübergreifender Lösungen, Ressourcenmanagement und Verpackung in NuGet-Pakete. Das Material enthält praktische Beispiele und Empfehlungen für Entwickler auf Mittel- und Senior-Niveau.

So integrieren Sie nativen Code in .NET: praktische Beispiele und Lösungen
Advertisement 728x90

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:

Google AdInline article slot
  • 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

Google AdInline article slot

- macOS: CoreMIDI.framework

- Linux: ALSA via libasound

  • Unterschiedliche API-Funktionen:

- Windows: midiInGetNumDevs()

Google AdInline article slot

- 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

Advertisement 728x90

Weiterlesen