mcpField
D
dotnet-native-interop

dotnet-native-interop

github.com/wshaddix開発8682 回インストール35 週間前に更新
リポジトリ

dotnet-native-interop について

.NET からネイティブ C/C++ ライブラリを呼び出す開発者向けの P/Invoke ガイドです。相互運用宣言、データのマーシャリング、クロスプラットフォームでのライブラリ解決、コールバックを扱います。

主な機能

  1. 01LibraryImport と DllImport の比較
  2. 02構造体と文字列のマーシャリング
  3. 03複数プラットフォームでのライブラリ解決
  4. 04関数ポインターとコールバックの利用
  5. 05ネイティブリソースのライフタイム管理

活用シーン

  1. 01.NET からネイティブ C/C++ API を呼び出す
  2. 02複数プラットフォーム向けのネイティブライブラリ読み込み設定
  3. 03ネイティブコールバックとリソース管理の設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-native-interop

インストール後、Claude Code で/dotnet-native-interopを入力して読み込まれたか確認します。

詳細

dotnet-native-interop

.NET からネイティブの C/C++ ライブラリを呼び出すための Platform Invoke(P/Invoke)パターンです。[LibraryImport](推奨、.NET 7 以降)と [DllImport](レガシー)の比較、構造体のマーシャリング、文字列のマーシャリング、関数ポインターのコールバック、クロスプラットフォームでのライブラリ解決に使用する NativeLibrary.SetDllImportResolver、Windows、macOS、Linux、iOS、Android に固有の考慮事項を扱います。

バージョンの前提: [LibraryImport] のベースラインは .NET 7.0 以降です。[DllImport] はすべての .NET バージョンで使用できます。NativeLibrary API は .NET Core 3.0 以降で使用できます。

対象範囲: このスキルでは、ネイティブメソッドのシグネチャの宣言、データ型のマーシャリング、プラットフォーム間でのライブラリパスの解決、コールバックパターンなど、P/Invoke の一般的なガイダンスを扱います。AOT 固有の P/Invoke に関する考慮事項(direct pinvoke、AOT 公開用のコンパイル時マーシャリング)は [skill:dotnet-native-aot] で扱います。Windows COM 相互運用と CsWin32 ソースジェネレーターの使用方法は [skill:dotnet-winui] で扱います。WASM は従来型の P/Invoke をサポートしていません。[JSImport]/[JSExport] を使った JavaScript 相互運用については、[skill:dotnet-aot-wasm] を参照してください。

対象外: COM 相互運用(Windows のレガシー機能)。CsWin32 ソースジェネレーターについては [skill:dotnet-winui] を参照してください。Android の Java 相互運用用 JNI ブリッジ(P/Invoke とは異なる仕組み)。WASM 用の [JSImport]/[JSExport] については [skill:dotnet-aot-wasm] を参照してください。

相互参照:[skill:dotnet-native-aot] では、AOT 固有の P/Invoke と公開シナリオでの [LibraryImport] を、[skill:dotnet-aot-architecture] では、ソース生成による相互運用を含む AOT ファーストの設計パターンを、[skill:dotnet-winui] では、CsWin32 ソースジェネレーターと COM 相互運用を、[skill:dotnet-aot-wasm] では、WASM の JavaScript 相互運用(ネイティブ P/Invoke ではありません)を扱います。


LibraryImport と DllImport の比較

[LibraryImport](.NET 7 以降)は、新しい P/Invoke 宣言に推奨される属性です。ソース生成を使ってコンパイル時にマーシャリングコードを生成するため、完全に AOT と互換性があり、実行時のコード生成によるオーバーヘッドもありません。

[DllImport] はレガシー属性です。実行時のマーシャリングに依存するため、AOT シナリオでは利用できないコード生成が必要になる場合があります。.NET 6 以前を対象とする場合、または SYSLIB1054 アナライザーが特定のシグネチャを [LibraryImport] で処理できないと示す場合に限り、[DllImport] を使用してください。

選択ガイド
シナリオ使用する属性
.NET 7 以降を対象とする新しいコード[LibraryImport]
.NET 6 以前を対象とする[DllImport]
SYSLIB1054 アナライザーが非互換を警告する[DllImport](理由を説明するコメントを付ける)
Native AOT で公開する[LibraryImport](完全な AOT 互換性に必須)
LibraryImport の宣言
using System.Runtime.InteropServices;

public static partial class NativeApi
{
    [LibraryImport("mylib")]
    internal static partial int ProcessData(
        ReadOnlySpan<byte> input,
        int length);

    [LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
    internal static partial int OpenByName(string name);

    [LibraryImport("mylib", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static partial bool CloseResource(nint handle);
}

[LibraryImport] の主な要件:

  • メソッドは partial クラス内の static partial であること
  • 文字列がある場合は、各文字列パラメーターに StringMarshalling または [MarshalAs] を指定して、文字列のマーシャリングを明示すること(文字列がある場合にのみ必要)
  • bool の戻り値型には、明示的な [return: MarshalAs(UnmanagedType.Bool)] が必要
  • Span<T> および ReadOnlySpan<T> のパラメーターはそのままサポートされます。[DllImport] はこれらをサポートしません(代わりに配列を使用してください)
DllImport の宣言(レガシー)
using System.Runtime.InteropServices;

public static class NativeApiLegacy
{
    [DllImport("mylib", CharSet = CharSet.Unicode, SetLastError = true)]
    internal static extern int ProcessData(
        byte[] input,
        int length);

    [DllImport("mylib", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool CloseResource(IntPtr handle);
}
DllImport から LibraryImport への移行

SYSLIB1054 アナライザーは、[DllImport] を [LibraryImport] に変換するよう提案し、コード修正を提示します。主な変更点は次のとおりです。

  1. [DllImport] を [LibraryImport] に置き換える
  2. static extern を static partial に変更する
  3. 含むクラスを partial にする
  4. CharSet を StringMarshalling に置き換える
  5. 適切な場合は IntPtr を nint に置き換える
  6. bool のパラメーターと戻り値に、明示的な [MarshalAs] を追加する
// Before (DllImport)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr LoadLibrary(string lpLibFileName);

// After (LibraryImport)
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf16,
    SetLastError = true)]
internal static partial nint LoadLibrary(string lpLibFileName);

プラットフォーム固有のライブラリ名

ネイティブライブラリの名前はプラットフォームによって異なります。NativeLibrary.SetDllImportResolver または条件付きコンパイルを使用して対応してください。

Windows

Windows では .dll ファイルを使用します。ローダーはアプリケーションディレクトリ、システムディレクトリ、PATH の順に検索します。

// Windows library name includes .dll extension
[LibraryImport("sqlite3.dll")]
internal static partial int sqlite3_open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string filename,
    out nint db);

Windows では拡張子を省略することもできます。ローダーが自動的に .dll を付加します。

[LibraryImport("sqlite3")]
internal static partial int sqlite3_open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string filename,
    out nint db);
macOS と Linux

macOS では .dylib ファイル、Linux では .so ファイルを使用します。.NET ランタイムは、一般的な名前のバリエーション(lib プレフィックスの有無、プラットフォーム固有の拡張子)を自動的に探索します。

// Use the logical name without extension -- .NET probes:
// libsqlite3.dylib (macOS), libsqlite3.so (Linux), sqlite3.dll (Windows)
[LibraryImport("libsqlite3")]
internal static partial int sqlite3_open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string filename,
    out nint db);

ライブラリ名 "foo" に対する .NET の探索順序:

  1. foo(完全一致の名前)
  2. foo.dll、foo.so、foo.dylib(プラットフォームの拡張子)
  3. libfoo、libfoo.so、libfoo.dylib(lib プレフィックスと拡張子)
iOS

iOS では、実行時の動的ライブラリの読み込みは許可されていません。ネイティブコードはアプリケーションのバイナリに静的リンクする必要があります。メイン実行ファイルにリンクされた関数を呼び出すには、ライブラリ名として __Internal を使用します。

// Calls a function statically linked into the iOS app binary
[LibraryImport("__Internal")]
internal static partial int NativeFunction(int input);

iOS では、ネイティブライブラリを静的ライブラリ(.a)としてコンパイルし、Xcode のビルドフェーズでリンクする必要があります。MAUI と Xamarin では、プロジェクトファイル内のネイティブ参照を通じてこれを処理します。

<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
  <NativeReference Include="libs/libmynative.a">
    <Kind>Static</Kind>
    <ForceLoad>true</ForceLoad>
  </NativeReference>
</ItemGroup>
Android

Android では、アプリのネイティブライブラリディレクトリから読み込まれる .so ファイルを使用します。P/Invoke 宣言では、通常、ライブラリ名から lib プレフィックスと .so 拡張子を省略します。

// Android loads libmynative.so from the APK's lib/<abi>/ directory
[LibraryImport("mynative")]
internal static partial int NativeFunction(int input);

プロジェクトには、ターゲット ABI ごとにプラットフォーム固有の .so ファイルを含めてください。

<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
  <AndroidNativeLibrary Include="libs/arm64-v8a/libmynative.so" Abi="arm64-v8a" />
  <AndroidNativeLibrary Include="libs/x86_64/libmynative.so" Abi="x86_64" />
</ItemGroup>
WASM

WebAssembly は従来の P/Invoke をサポートしていません。ブラウザーの WASM では、ネイティブの C/C++ コードを [LibraryImport] または [DllImport] 経由で呼び出すことはできません。JavaScript 相互運用については、[skill:dotnet-aot-wasm] を参照してください。


NativeLibrary.SetDllImportResolver

NativeLibrary.SetDllImportResolver(.NET Core 3.0 以降)を使用すると、ライブラリの解決を実行時に制御できます。静的な名前の探索では不十分な場合に、クロスプラットフォームでライブラリを読み込むための推奨される方法です。

using System.Reflection;
using System.Runtime.InteropServices;

// Register once at startup (per assembly)
NativeLibrary.SetDllImportResolver(
    Assembly.GetExecutingAssembly(),
    DllImportResolver);

static nint DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
    if (libraryName == "mynativelib")
    {
        if (OperatingSystem.IsWindows())
            return NativeLibrary.Load("mynative.dll", assembly, searchPath);

        if (OperatingSystem.IsMacOS())
            return NativeLibrary.Load("libmynative.dylib", assembly, searchPath);

        if (OperatingSystem.IsLinux())
            return NativeLibrary.Load("libmynative.so.1", assembly, searchPath);
    }

    // Fall back to default resolution
    return nint.Zero;
}
DllImportResolver の一般的な使用例
シナリオリゾルバーが必要な理由
Linux 上のバージョン付き .so(例:libfoo.so.2)既定の探索ではバージョン付きの名前が確認されない
標準以外のパスにあるライブラリ実行時にカスタムディレクトリから読み込む
RID ごとにバンドルされたネイティブライブラリruntimes/<rid>/native/ パスに解決する
読み込み時の機能検出複数のライブラリ名を試し、適切にフォールバックする
NativeLibrary API

NativeLibrary クラスは、低レベルのライブラリ管理機能を提供します。

// Load a library explicitly
nint handle = NativeLibrary.Load("mylib");

// Try to load without throwing
if (NativeLibrary.TryLoad("mylib", out nint h))
{
    // Get a function pointer by name
    nint funcPtr = NativeLibrary.GetExport(h, "my_function");

    // Or try without throwing
    if (NativeLibrary.TryGetExport(h, "my_function", out nint fp))
    {
        // Use function pointer
    }

    NativeLibrary.Free(h);
}

マーシャリングのパターン

構造体のマーシャリング

ネイティブコードに渡す構造体には、明確に定義されたメモリレイアウトが必要です。レイアウトとアラインメントを制御するには、[StructLayout] を使用します。

using System.Runtime.InteropServices;

// Sequential layout -- fields laid out in declaration order
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
    public int X;
    public int Y;
}

// Explicit layout -- fields at specific byte offsets (for unions)
[StructLayout(LayoutKind.Explicit)]
public struct ValueUnion
{
    [FieldOffset(0)] public int IntValue;
    [FieldOffset(0)] public float FloatValue;
    [FieldOffset(0)] public double DoubleValue;
}

// Sequential with packing -- override default alignment
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PackedHeader
{
    public byte Magic;
    public int Length;     // No padding before this field
    public short Version;
}

Blittable 構造体(シーケンシャルまたは明示的なレイアウトを持つプリミティブ値型のみを含む構造体)は、コピーされずにネイティブコードへ直接渡されます。Blittable ではない構造体にはマーシャリングが必要であり、オーバーヘッドが発生します。

Blittable なプリミティブ型:byte、sbyte、short、ushort、int、uint、long、ulong、float、double、nint、nuint。

Blittable ではない型: bool(既定では 4 バイトの BOOL としてマーシャリングされます)、char(文字セットに依存します)、string、Blittable ではない型の配列。

文字列のマーシャリング

文字列のエンコーディングを明示的に指定してください。既定のマーシャリング動作には決して依存しないでください。

// UTF-8 strings (most common for cross-platform C APIs)
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int ProcessText(string input);

// UTF-16 strings (Windows APIs)
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int ProcessTextW(string input);

// Per-parameter marshalling when methods mix encodings
[LibraryImport("mylib")]
internal static partial int MixedApi(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string utf8Param,
    [MarshalAs(UnmanagedType.LPWStr)] string utf16Param);

出力文字列バッファーには、StringBuilder の代わりに ArrayPool の char[] または byte[] を使用してください。

[LibraryImport("mylib")]
internal static partial int GetName(
    [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] char[] buffer,
    int bufferSize);

// Usage
char[] buffer = ArrayPool<char>.Shared.Rent(256);
try
{
    int result = GetName(buffer, buffer.Length);
    string name = new string(buffer, 0, result);
}
finally
{
    ArrayPool<char>.Shared.Return(buffer);
}
関数ポインターのコールバック

最新の .NET(.NET 5 以降)では、パフォーマンスと AOT 互換性を高めるため、デリゲートベースのコールバックよりもアンマネージ関数ポインターが推奨されます。

推奨:[UnmanagedCallersOnly] を使用したアンマネージ関数ポインター

using System.Runtime.InteropServices;

// Native callback signature: int (*callback)(int value, void* context)
[LibraryImport("mylib")]
internal static unsafe partial void RegisterCallback(
    delegate* unmanaged[Cdecl]<int, nint, int> callback,
    nint context);

// Callback implementation
[UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
static int MyCallback(int value, nint context)
{
    // Process value
    return 0;
}

// Registration
unsafe
{
    RegisterCallback(&MyCallback, nint.Zero);
}

代替手段:デリゲートベースのコールバック(マネージ状態が必要な場合)

// Define delegate matching native signature
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int NativeCallback(int value, nint context);

[LibraryImport("mylib")]
internal static partial void RegisterCallbackDelegate(
    NativeCallback callback,
    nint context);

// Usage -- prevent GC collection during native use
static NativeCallback? s_callback;

static void Setup()
{
    s_callback = new NativeCallback(MyManagedCallback);
    RegisterCallbackDelegate(s_callback, nint.Zero);
    // Keep s_callback alive as long as native code may call it
}

static int MyManagedCallback(int value, nint context)
{
    return value * 2;
}
リソースのライフタイムに対する SafeHandle

生の IntPtr/nint の代わりに、SafeHandle のサブクラスを使用してネイティブリソースのライフタイムを管理してください。これにより、リソースリークや解放後使用のバグを防止できます。

using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

// Custom SafeHandle for a native resource
public class NativeResourceHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    private NativeResourceHandle() : base(ownsHandle: true) { }

    protected override bool ReleaseHandle()
    {
        NativeApi.CloseResource(handle);
        return true;
    }
}

public static partial class NativeApi
{
    [LibraryImport("mylib")]
    internal static partial NativeResourceHandle OpenResource(
        [MarshalAs(UnmanagedType.LPUTF8Str)] string name);

    [LibraryImport("mylib")]
    internal static partial void CloseResource(nint handle);

    [LibraryImport("mylib")]
    internal static partial int ReadResource(NativeResourceHandle handle,
        Span<byte> buffer, int count);
}

クロスプラットフォームのデータ型マッピング

C/C++ の型を .NET の型に慎重にマッピングしてください。一部の C 型は、プラットフォームによってサイズが異なります。

固定サイズ型
C/C++ Type.NET Typeサイズ
int8_t / charsbyte1 バイト
uint8_t / unsigned charbyte1 バイト
int16_t / shortshort2 バイト
uint16_t / unsigned shortushort2 バイト
int32_t / intint4 バイト
uint32_t / unsigned intuint4 バイト
int64_t / long longlong8 バイト
uint64_t / unsigned long longulong8 バイト
floatfloat4 バイト
doubledouble8 バイト
プラットフォーム依存型
C/C++ Type.NET Type注記
size_t / ptrdiff_tnint / nuintポインターサイズ
void* / ポインター型nint or void*ポインターサイズ
long (C/C++)CLong (.NET 6+)Windows では 4 バイト、Unix 64-bit では 8 バイト
unsigned longCULong (.NET 6+)long と同じプラットフォームによる差異
Windows BOOLint4 バイト(bool ではありません)
Windows BOOLEANbyte1 バイト

Unix 64-bit ではサイズが異なるため、C/C++ の long に C# の long を使用しないでください。移植性のある相互運用には CLong/CULong を使用してください。


エージェントに関する注意点

  1. 正当な理由がない限り、新しい .NET 7 以降のコードで [DllImport] を使用しないでください。 コンパイル時にマーシャリングコードを生成する [LibraryImport] を使用してください。[DllImport] にフォールバックするのは、SYSLIB1054 アナライザーが非互換性を示した場合に限ります。
  2. bool が 1 バイトとしてマーシャリングされると思い込まないでください。 .NET は既定で bool を 4 バイトの Windows BOOL としてマーシャリングします。C の _Bool/bool には [MarshalAs(UnmanagedType.U1)] を、Windows BOOL には [MarshalAs(UnmanagedType.Bool)] を明示的に使用してください。
  3. C/C++ の long との相互運用に C# の long を使用しないでください。 C の long は Windows では 4 バイトですが、64-bit Unix では 8 バイトです。クロスプラットフォームで正しく動作させるには、.NET 6 以降の CLong/CULong を使用してください。
  4. 出力文字列バッファーに StringBuilder を使用しないでください。 [LibraryImport] は StringBuilder をまったくサポートしておらず、[DllImport] と併用すると複数の中間コピーが割り当てられます。代わりに ArrayPool から char[] または byte[] を使用してください。
  5. WASM では [LibraryImport] や [DllImport] を使用しないでください。 WebAssembly は従来の P/Invoke をサポートしていません。WASM での JavaScript 相互運用については、[skill:dotnet-aot-wasm] を参照してください。
  6. iOS では動的ライブラリの読み込みを使用しないでください。 iOS では実行時の動的ライブラリ読み込みが禁止されています。静的にリンクされたネイティブコードでは、ライブラリ名として "__Internal" を使用してください。
  7. 相互運用構造体のフィールドに System.Delegate を使用しないでください。 型指定されたデリゲート、またはアンマネージド関数ポインター(delegate* unmanaged)を使用してください。型指定のないデリゲートは、マーシャリング中にランタイムを不安定にする可能性があります。
  8. ネイティブコードで使用している間、デリゲートのインスタンスを生存させておくことを忘れないでください。 ネイティブコードが引き続き参照しているデリゲートを、GC が回収する可能性があります。デリゲートを静的フィールドに格納するか、ネイティブコールバックの期間中 GCHandle を使用してください。

前提条件

  • [LibraryImport] のソース生成には .NET 7 以降の SDK
  • NativeLibrary API には .NET Core 3.0 以降
  • 各対象プラットフォーム/アーキテクチャ向けにコンパイルされたネイティブライブラリ
  • iOS の場合:NativeReference を介してネイティブ静的ライブラリをリンクした Xcode
  • Android の場合:対象 ABI(arm64-v8a、x86_64)ごとのネイティブ .so ファイル

参考資料

コメント