Zpět na domů

Integrace nativního kódu do .NET: P/Invoke, DllImport, křížově platformní

Článek představuje podrobný průvodce integrací nativního kódu do aplikací .NET. Jsou zde popsány mechanismy P/Invoke a DllImport, vytváření křížově platformních řešení, správa zdrojů a balení do NuGet balíčků. Materiál obsahuje praktické příklady a doporučení pro vývojáře střední a vysoké úrovně.

Jak integrovat nativní kód do .NET: praktické příklady a řešení
Advertisement 728x90

Integrace nativního kódu v .NET: praktický průvodce pro vývojáře

Používání nativního kódu v aplikacích .NET umožňuje rozšířit funkcionalitu nad rámec standardní knihovny. Tento přístup je obzvláště užitečný při práci s nízkourovňovými API operačních systémů nebo hardwarovými rozhraními, jako je MIDI. V článku jsou probírány klíčové aspekty integrace: od základního použití DllImport po vytváření multiplatformních řešení a balení do NuGet balíčků.

Základy P/Invoke a DllImport

Mechanismus P/Invoke (Platform Invoke) zajišťuje volání nativních funkcí ze spravovaného kódu .NET. Základní příklad použití atributu DllImport pro práci s Windows API:

[DllImport("winmm.dll")]
static extern uint midiInGetNumDevs();

Klíčové vlastnosti DllImport:

Google AdInline article slot
  • Určení názvu knihovny a vstupního bodu (EntryPoint)
  • Automatický marshaling datových typů
  • Podpora různých konvencí volání

Marshaling – proces převodu datových typů mezi spravovanou a nespravovanou pamětí. To zahrnuje:

  • Převod primitivních typů (int, float)
  • Práci s řetězci (string na LPWSTR)
  • Správu struktur a ukazatelů
  • Přidělování a uvolňování nespravované paměti

Multiplatformní řešení

Při vývoji pro více operačních systémů vznikají specifické požadavky:

  • Různé systémové knihovny:

- Windows: winmm.dll

Google AdInline article slot

- macOS: CoreMIDI.framework

- Linux: ALSA přes libasound

  • Různé API funkce:

- Windows: midiInGetNumDevs()

Google AdInline article slot

- macOS: MIDIGetNumberOfSources()

- Linux: snd_seq_* funkce

  • Různé cesty ke knihovnám:

- Windows: jednoduchý název DLL

- macOS: úplná cesta k frameworku

Pro určení operačního systému použijte RuntimeInformation:

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    count = midiInGetNumDevs();
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
    count = MIDIGetNumberOfSources();

Vytvoření nativního backendu

Unifikovaný přístup předpokládá vytvoření vlastní nativní vrstvy, která skrývá rozdíly mezi platformami.

Struktura projektu:

  • Native-Windows.cpp → Native.dll
  • Native-macOS.cpp → Native.dylib
  • Native-Linux.cpp → Native.so

Příklad implementace pro Windows:

extern "C" __declspec(dllexport) int GetInputDevicesCount()
{
    return midiInGetNumDevs();
}

Příklad implementace pro macOS:

extern "C" int GetInputDevicesCount()
{
    return MIDIGetNumberOfSources();
}

Výhody přístupu:

  • Jednotné API pro všechny platformy
  • Izolace kódu specifického pro platformu
  • Zjednodušení C# kódu
  • Možnost přidání další logiky

Sestavení a správa binárních souborů

Proces sestavování nativních knihoven závisí na cílové platformě:

Pro Windows (MSVC):

cl /LD Native-Windows.cpp /link /OUT:Native.dll

Pro macOS (Clang):

clang++ -dynamiclib -o Native.dylib Native-macOS.cpp -framework CoreMIDI

Pro Linux (GCC):

g++ -shared -fPIC -o Native.so Native-Linux.cpp -lasound

Umístění souborů v projektu:

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řístupy k P/Invoke

Atribut LibraryImport

V .NET 7+ se objevil nový atribut LibraryImport, který generuje kód pro marshaling během kompilace:

[LibraryImport("Native")]
internal static partial int GetInputDevicesCount();

Výhody LibraryImport:

  • Generování kódu během kompilace
  • Zlepšený výkon
  • Přísnější kontrola typů
  • Podpora generátorů zdrojového kódu

Bezpečné správa zdrojů

IntPtr vs SafeHandle:

  • IntPtr – jednoduchý ukazatel, vyžaduje ruční správu
  • SafeHandle – bezpečná obálka s automatickým uvolňováním

Příklad použití SafeHandle:

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;
    }
}

Práce s zpětnými voláními a asynchronitou

Specifika volání ze zpětných volání:

  • Některé nativní funkce nelze volat ze zpětných volání
  • Je nutná synchronizace mezi vlákny
  • Možné deadlocky při nesprávném použití

Vzor pro práci s buffery v MIDI:

  • Předem inicializovat několik bufferů
  • Ve zpětném volání – opakované použití bufferu s daty
  • Vyhýbat se volání unprepared/close ve zpětných voláních
  • Použití vláknově bezpečných datových struktur

Příklad bezpečného zpětného volání:

private static void MidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2)
{
    // Extrakce dat z bufferu
    var data = ExtractMidiData(param1);
    
    // Asynchronní zpracování
    Task.Run(() => ProcessMidiDataAsync(data));
    
    // Opětovné přidání bufferu
    lock (bufferLock)
    {
        midiInAddBuffer(handle, param1, Marshal.SizeOf<MIDIHDR>());
    }
}

Balení do NuGet balíčku

Struktura NuGet balíčku s nativními knihovnami:

<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>

Klíčové direktivy v .csproj:

<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>

Co je důležité:

  • Správná organizace souborů podle RID (Runtime Identifier)
  • Určení cílových platforem v .csproj
  • Testování instalace balíčku na různých systémech
  • Podpora jak x64, tak ARM64 architektur

Optimalizace výkonu

Metody snížení režie:

  • Minimalizace přechodů mezi spravovaným a nespravovaným kódem
  • Použití blittable typů (nevyžadujících marshaling)
  • Ukládání deskriptorů a ukazatelů do cache
  • Dávkové zpracování volání

Blittable typy:

  • byte, sbyte, short, ushort, int, uint, long, ulong
  • float, double
  • struktury obsahující pouze blittable typy

Příklad optimalizované struktury:

[StructLayout(LayoutKind.Sequential)]
struct MidiMessage
{
    public uint Timestamp;
    public byte Status;
    public byte Data1;
    public byte Data2;
    public byte Reserved;
}

Ladění a diagnostika

Nástroje pro ladění nativního kódu:

  • WinDbg a CDB pro Windows
  • LLDB pro macOS a Linux
  • Visual Studio Mixed Mode Debugging
  • JetBrains Rider s podporou nativního ladění

Typické problémy a řešení:

  • Access Violation: nesprávný marshaling typů
  • Memory Leaks: neuvolněné zdroje
  • Deadlocks: zámky ve zpětných voláních
  • Performance Issues: častá P/Invoke volání

Logování volání:

[DllImport("Native", EntryPoint = "GetInputDevicesCount")]
private static extern int NativeGetInputDevicesCount();

public static int GetInputDevicesCount()
{
    Logger.Debug($"Volání nativní funkce GetInputDevicesCount");
    var result = NativeGetInputDevicesCount();
    Logger.Debug($"Nativní funkce vrátila: {result}");
    return result;
}

Budoucnost nativní integrace v .NET

Trendy a směry vývoje:

  • Source Generators: automatická generace P/Invoke kódu
  • NativeAOT: kompilace do nativního kódu bez runtime
  • WASI: spouštění .NET v WebAssembly s přístupem k systémovým API
  • Těsnější integrace s jazyky jako Rust a C++

Příklad použití Source Generators:

[NativeMethod("Native", "GetInputDevicesCount")]
public static partial int GetInputDevicesCount();

Co je důležité

  • P/Invoke zůstává hlavním mechanismem pro volání nativního kódu
  • Multiplatformnost vyžaduje vytvoření unifikovaného API
  • Správná správa zdrojů je kritická pro stabilitu
  • NuGet balíčky musí obsahovat binární soubory pro všechny cílové platformy
  • Moderní přístupy (LibraryImport, Source Generators) zjednodušují vývoj

— Editorial Team

Advertisement 728x90

Číst dál