dotnet-pinvoke について
.NET からネイティブライブラリを呼び出す P/Invoke 宣言の作成とレビューを支援するスキルです。シグネチャの対応付け、文字列とメモリの扱いを案内し、対象フレームワークや要件に応じて `DllImport` と `LibraryImport` の選択を支援します。
主な機能
- 01`DllImport` と `LibraryImport` の選択
- 02ネイティブ型と .NET 型の対応付け
- 03文字列とメモリ所有権の処理
- 04`SafeHandle` によるネイティブハンドル管理
- 05コールバックとエラーの処理
活用シーン
- 01C ヘッダーから P/Invoke 宣言を作成
- 02型やエンコーディングに関する署名の確認
- 03ネイティブ境界でのクラッシュや破損の調査
- 04AOT やトリミング要件に合わせた宣言の移行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill dotnet-pinvokeインストール後、Claude Code で/dotnet-pinvokeを入力して読み込まれたか確認します。
同梱ファイルreferences/diagnostics.mdreferences/type-mapping.md
詳細
.NET P/Invoke
.NET からネイティブコードを呼び出すことは強力ですが、誤りが許されません。不正なシグネチャ、文字列の文字化け、メモリのリークや解放は、最もよくあるバグの原因です。これらはすべて、断続的なクラッシュ、気づきにくいデータ破損、実際の不具合箇所から離れた場所でのアクセス違反として現れることがあります。
このスキルでは、DllImport(.NET Framework 1.0 から利用可能)と LibraryImport(ソース生成方式、.NET 7 以降)の両方を扱います。.NET Framework をターゲットにする場合は、必ず DllImport を使用してください。.NET 7 以降をターゲットにする場合、新しいコードでは LibraryImport を優先してください。ネイティブ AOT が要件である場合、選択肢は LibraryImport のみです。
このスキルを使用する場面
- C/C++ ヘッダーから新しい
[DllImport]または[LibraryImport]宣言を作成する - P/Invoke シグネチャの正確さ(型のサイズ、呼び出し規約、文字列エンコーディング)をレビューする
- .NET から使用するために C ライブラリ全体をラップする
- ネイティブ境界での
AccessViolationException、DllNotFoundException、または気づきにくいデータ破損をデバッグする - AOT/トリミングとの互換性のために
DllImport宣言をLibraryImportに移行する - ネイティブハンドルやバッファーに関連するメモリリークやヒープ破損を診断する
中止の判断基準
- 単一の関数ですか? シグネチャを対応付け(ステップ 1~3)、必要な場合にのみ文字列/メモリを扱い、ツールと移行のセクションは省略します。
- ユーザーからの依頼がある場合、または AOT/トリミングが明示的な要件である場合を除き、既存の
DllImportをLibraryImportに移行しないでください。 - ターゲットが Win32 API に限定されている場合を除き、CsWin32 を推奨しないでください。
- ネイティブ API が関数ポインターを必要とする場合を除き、コールバックを生成しないでください(ステップ 8)。
- レビューの依頼ですか? 検証チェックリストを使用し、動作しているコードを書き換えないでください。
入力
| 入力 | 必須 | 説明 |
|---|---|---|
| ネイティブヘッダーまたはドキュメント | はい | C/C++ の関数シグネチャ、構造体定義、呼び出し規約 |
| ターゲットフレームワーク | はい | DllImport と LibraryImport のどちらを使用するかを決定します |
| ターゲットプラットフォーム | 推奨 | 型のサイズ(long、size_t)とライブラリ名に影響します |
| メモリ所有権の契約 | はい | 各バッファーまたはハンドルの確保と解放を誰が行うか |
エージェントの動作: ドキュメントとネイティブヘッダーの内容が異なる場合は、必ずヘッダーを信頼してください。オンラインドキュメント(公式の Win32 API ドキュメントを含む)では、正確な P/Invoke シグネチャに不可欠な型、呼び出し規約、構造体のレイアウトに関する詳細が、省略されたり簡略化されたりしていることがよくあります。
ワークフロー
ステップ 1:DllImport または LibraryImport を選択する
| 項目 | DllImport | LibraryImport(.NET 7 以降) |
|---|---|---|
| 仕組み | ランタイムマーシャリング | ソースジェネレーター(コンパイル時) |
| AOT/トリミング対応 | いいえ | はい |
| 文字列マーシャリング | CharSet enum | StringMarshalling enum |
| エラー処理 | SetLastError | SetLastError |
| 利用可能なバージョン | .NET Framework 1.0 以降 | .NET 7 以降のみ |
ステップ 2:ネイティブ型を .NET 型にマッピングする
最も危険なマッピングです。バグの大半はこれらが原因です。
| C / Win32 型 | .NET 型 | 理由 |
|---|---|---|
long | CLong | Windows では 32 ビット、64 ビット Unix では 64 ビットです。LibraryImport では [assembly: DisableRuntimeMarshalling] が必要です |
size_t | nuint / UIntPtr | ポインターサイズです。.NET 8 以降では nuint、それ以前の .NET では UIntPtr を使用します。ulong は絶対に使用しないでください |
BOOL (Win32) | int | bool ではありません。Win32 の BOOL は 4 バイトです |
bool (C99) | [MarshalAs(UnmanagedType.U1)] bool | 1 バイトのマーシャリングを指定する必要があります |
HANDLE、HWND | SafeHandle | 生の IntPtr より優先してください |
LPWSTR / wchar_t* | string | Windows では UTF-16 です(in 文字列のコストが最も低くなります)。クロスプラットフォームコードでは避けてください。wchar_t の幅はコンパイラーによって定義されるためです(Windows 以外では通常 UTF-32) |
LPSTR / char* | string | エンコーディング(ANSI または UTF-8)を指定する必要があります。in パラメーターでは常にマーシャリングのコストが発生します |
型マッピング表、構造体のレイアウト、blittable 型のルールの完全版については、references/type-mapping.md を参照してください。
❌ 絶対に C の
longにintやlongを使用しないでください。Windows では 32 ビット、Unix では 64 ビットです。必ずCLongを使用してください。 ❌ 絶対にsize_tにulongを使用しないでください。32 ビットではスタック破損の原因になります。nuintまたはUIntPtrを使用してください。 ❌ 絶対にMarshalAsなしでboolを使用しないでください。既定のマーシャリングサイズは正しくありません。
ステップ 3:宣言を記述する
次の C ヘッダーを使用します。
int32_t process_records(const Record* records, size_t count, uint32_t* out_processed);
DllImport:
[DllImport("mylib")]
private static extern int ProcessRecords(
[In] Record[] records, UIntPtr count, out uint outProcessed);
LibraryImport:
[LibraryImport("mylib")]
internal static partial int ProcessRecords(
[In] Record[] records, nuint count, out uint outProcessed);
呼び出し規約の指定が必要なのは、Cdecl と StdCall が異なる Windows x86(32 ビット)をターゲットとする場合のみです。x64、ARM、ARM64 では呼び出し規約は 1 種類だけなので、属性は不要です。
エージェントの動作: プロジェクトのプロパティ(例:<PlatformTarget>x86</PlatformTarget>)、ランタイム識別子(例:win-x86)、ビルドスクリプト、コメント、開発者の指示などから Windows x86 がターゲットであると検出した場合は、開発者に通知し、すべての P/Invoke 宣言で呼び出し規約を明示するよう推奨してください。
// DllImport (x86 targets)
[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]
// LibraryImport (x86 targets)
[LibraryImport("mylib")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
マネージドメソッド名がネイティブのエクスポート名と異なる場合は、EntryPoint を指定して EntryPointNotFoundException を回避してください。
// DllImport
[DllImport("mylib", EntryPoint = "process_records")]
private static extern int ProcessRecords(
[In] Record[] records, UIntPtr count, out uint outProcessed);
// LibraryImport
[LibraryImport("mylib", EntryPoint = "process_records")]
internal static partial int ProcessRecords(
[In] Record[] records, nuint count, out uint outProcessed);
ステップ 4:文字列を正しく処理する
- ネイティブ関数が想定するエンコーディングを確認します。 安全な既定値はありません。
- Windows API: 常に
W(UTF-16)バリアントを呼び出してください。Aバリアントを使用する場合は、明確な理由と明示的な ANSI エンコーディングが必要です。 - クロスプラットフォームの C ライブラリ: 通常は UTF-8 を想定します。
- エンコーディングを明示します。
CharSet.Autoに依存しないでください。 - 出力バッファーに
StringBuilderを使用しないでください。
❌ 絶対に
CharSet.Autoに依存したり、文字列のエンコーディングを省略したりしないでください。安全な既定値はありません。
// DllImport — Windows API (UTF-16)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetModuleFileNameW(
IntPtr hModule, [Out] char[] filename, int size);
// DllImport — Cross-platform C library (UTF-8)
[DllImport("mylib")]
private static extern int SetName(
[MarshalAs(UnmanagedType.LPUTF8Str)] string name);
// LibraryImport — UTF-16
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
SetLastError = true)]
internal static partial int GetModuleFileNameW(
IntPtr hModule, [Out] char[] filename, int size);
// LibraryImport — UTF-8
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int SetName(string name);
文字列のライフタイムに関する警告: マーシャリングされた文字列は、呼び出しが戻った後に解放されます。ネイティブコードがポインターをコピーせずに保持する場合は、ライフタイムを手動で管理する必要があります。Windows または .NET Framework では、境界をまたぐ所有権の受け渡しには CoTaskMemAlloc/CoTaskMemFree が第一候補です。Windows 以外のターゲットでは、NativeMemory API を使用してください。ライブラリ独自のアロケーターを使用する必要がある場合もあります。
ステップ 5:メモリの所有権を確立する
メモリが境界をまたえる場合、所有者は必ずどちらか一方でなければならず、双方が合意している必要があります。
❌ アロケーターを一致させずに絶対に解放しないでください。
mallocで確保したメモリにMarshal.FreeHGlobalを使うと、ヒープが破損します。
モデル 1 — 呼び出し側が確保し、呼び出し側が解放する(最も安全):
[LibraryImport("mylib")]
private static partial int GetName(
Span<byte> buffer, nuint bufferSize, out nuint actualSize);
public static string GetName()
{
Span<byte> buffer = stackalloc byte[256];
int result = GetName(buffer, (nuint)buffer.Length, out nuint actualSize);
if (result != 0) throw new InvalidOperationException($"Failed: {result}");
return Encoding.UTF8.GetString(buffer[..(int)actualSize]);
}
モデル 2 — 呼び出し先が確保し、呼び出し側が解放する(Win32 で一般的):
[LibraryImport("mylib")]
private static partial IntPtr GetVersion();
[LibraryImport("mylib")]
private static partial void FreeString(IntPtr s);
public static string GetVersion()
{
IntPtr ptr = GetVersion();
try { return Marshal.PtrToStringUTF8(ptr) ?? throw new InvalidOperationException(); }
finally { FreeString(ptr); } // Must use the library's own free function
}
重要なルール: 必ず対応するアロケーターで解放してください。malloc で確保したメモリに Marshal.FreeHGlobal または Marshal.FreeCoTaskMem を使用しないでください。
モデル 3 — ハンドルベース(呼び出し先が確保し、呼び出し先が解放する): SafeHandle を使用してください(ステップ 6 を参照)。
マネージドオブジェクトのピン留め — ネイティブコードがポインターを保持する場合、または非同期に実行される場合:
// Synchronous: use fixed
public static unsafe void ProcessSync(byte[] data)
{
fixed (byte* ptr = data) { ProcessData(ptr, (nuint)data.Length); }
}
// Asynchronous: use GCHandle
var gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
// Must keep pinned until native processing completes, then call gcHandle.Free()
ステップ 6:ネイティブハンドルには SafeHandle を使用する
生の IntPtr は例外発生時にリークし、二重解放を防ぐ仕組みもありません。SafeHandle の使用は必須です。
internal sealed class MyLibHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// Required by the marshalling infrastructure to instantiate the handle.
// Do not remove — there are no direct callers.
private MyLibHandle() : base(ownsHandle: true) { }
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
private static partial MyLibHandle CreateHandle(string config);
[LibraryImport("mylib")]
private static partial int UseHandle(MyLibHandle h, ReadOnlySpan<byte> data, nuint len);
[LibraryImport("mylib")]
private static partial void DestroyHandle(IntPtr h);
protected override bool ReleaseHandle() { DestroyHandle(handle); return true; }
public static MyLibHandle Create(string config)
{
var h = CreateHandle(config);
if (h.IsInvalid) throw new InvalidOperationException("Failed to create handle");
return h;
}
public int Use(ReadOnlySpan<byte> data) => UseHandle(this, data, (nuint)data.Length);
}
// Usage: SafeHandle is IDisposable
using var handle = MyLibHandle.Create("config=value");
int result = handle.Use(myData);
ステップ 7:エラーを処理する
ネイティブ API の契約で OS の最終エラー値をキャプチャする必要がある場合は、LibraryImportAttribute.SetLastError を true に設定し、Marshal.GetLastPInvokeError() で取得します。
// Win32 APIs — check SetLastError
[LibraryImport("kernel32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CloseHandle(IntPtr hObject);
if (!CloseHandle(handle))
throw new Win32Exception(Marshal.GetLastPInvokeError());
// HRESULT APIs
int hr = NativeDoWork(context);
Marshal.ThrowExceptionForHR(hr);
ステップ 8:コールバックを処理する(必要な場合)
推奨(.NET 8 以降):UnmanagedCallersOnly — デリゲートを完全に回避し、GC の存続期間に関するリスクもありません。
[UnmanagedCallersOnly]
private static void LogCallback(int level, IntPtr message)
{
string msg = Marshal.PtrToStringUTF8(message) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
}
[LibraryImport("mylib")]
private static unsafe partial void SetLogCallback(
delegate* unmanaged<int, IntPtr, void> cb);
unsafe { SetLogCallback(&LogCallback); }
このメソッドは static である必要があり、ネイティブコードに例外を返してはなりません。また、blittable なパラメーター型のみを使用できます。
代替手段(古い TFM、またはインスタンスの状態が必要な場合):ルート化したデリゲート
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] // Only needed on Windows x86
private delegate void LogCallbackDelegate(int level, IntPtr message);
// CRITICAL: prevent delegate from being garbage collected
private static LogCallbackDelegate? s_logCallback;
public static void EnableLogging(Action<int, string> handler)
{
s_logCallback = (level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
handler(level, msg);
};
SetLogCallback(s_logCallback);
}
ネイティブコードが関数ポインターを保持する場合、デリゲートはその存続期間全体にわたってルート化しておく必要があります。デリゲートが回収されると、クラッシュにつながります。
短命なコールバックには GC.KeepAlive を使用する: Marshal.GetFunctionPointerForDelegate でデリゲートを関数ポインターに変換すると、GC はポインターとデリゲートの関係を追跡しません。ネイティブ呼び出しが完了する前に回収されないよう、GC.KeepAlive を使用します。
var callback = new LogCallbackDelegate((level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
});
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // prevent collection — fnPtr does not root the delegate
クロスプラットフォームのライブラリ読み込み
複雑なシナリオでは NativeLibrary.SetDllImportResolver を使用し、単純なケースでは条件付きコンパイルを使用します。C の long/unsigned long には CLong/CULong を使用します。注:LibraryImport で CLong/CULong を使用するには、[assembly: DisableRuntimeMarshalling] が必要です。
// Trivial: use platform naming convention
// The default naming convention adds corresponding prefix and extension when
// searching the nativelibrary. The resultant file name will be mylib.dll on
// Windows, libmylib.so on Linux and libmylib.dylib on macOS.
private const string LibName = "mylib";
// Simple: conditional compilation
// WINDOWS, LINUX, MACOS are predefined only when targeting an OS-specific TFM
// (e.g., net8.0-windows). For portable TFMs (e.g., net8.0), these symbols are
// not defined — use the runtime resolver approach below instead.
#if WINDOWS
private const string LibName = "mylib.dll";
#elif LINUX
private const string LibName = "libmylib.so";
#elif MACOS
private const string LibName = "libmylib.dylib";
#endif
// Complex: runtime resolver
// When targeting netstandard2.0 or other frameworks when OperatingSystem.IsXXX
// is not available, use RuntimeInformation.IsOSPlatform(OSPlatform.XXX) api.
NativeLibrary.SetDllImportResolver(typeof(MyLib).Assembly,
(name, assembly, searchPath) =>
{
if (name != "mylib") return IntPtr.Zero;
string libName = OperatingSystem.IsWindows()
? "mylib.dll"
: OperatingSystem.IsMacOS()
? "libmylib.dylib" : "libmylib.so";
NativeLibrary.TryLoad(libName, assembly, searchPath, out var handle);
return handle;
});
DllImport から LibraryImport への移行
.NET 7 以降を対象とするコードベースでは、移行により AOT 互換性とトリミングの安全性が得られます。
- 外側のクラスに
partialを追加し、メソッドをstatic partialにします [DllImport]を[LibraryImport]に置き換えますCharSetをStringMarshallingに置き換えます- OS の最終エラー値のキャプチャが必要な API では、
SetLastError = trueを維持します - Windows x86 を対象とする場合を除き、
CallingConventionを削除します - ビルドし、
SYSLIB1054~SYSLIB1057のアナライザー警告を解消します
相互運用アナライザーを有効にします。
<PropertyGroup>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>
ツール
CsWin32(Win32 API)
Win32 P/Invoke では、手書きのシグネチャよりも Microsoft.Windows.CsWin32 を優先してください。メタデータから正しい宣言をソース生成します。必要な API を列挙した NativeMethods.txt を追加します。
dotnet add package Microsoft.Windows.CsWin32
CsWinRT(WinRT API)
WinRT 相互運用には、Microsoft.Windows.CsWinRT を使用して .winmd ファイルから .NET プロジェクションを生成します。
Objective Sharpie(Objective-C API)
Objective-C ライブラリ(macOS/iOS)をバインドするには、Objective Sharpie を使用して、Objective-C ヘッダーから P/Invoke とバインディング定義の初期案を生成します。
検証
レビューチェックリスト
- すべてのシグネチャがネイティブヘッダーと正確に一致している(型、サイズ)
- Windows x86 を対象とする場合は呼び出し規約を指定し、それ以外では省略している
- 文字列エンコーディングを明示している。既定値や
CharSet.Autoに依存していない - メモリの所有権を文書化し、整合させている(誰が、何を使って確保し、解放するか)
- すべてのネイティブハンドルに
SafeHandleを使用している(生のIntPtrを相互運用レイヤーの外に持ち出していない) - コールバックとして渡すデリゲートをルート化し、GC に回収されないようにしている
- ネイティブ API で最終エラー値のキャプチャが必要な場合は、
DllImportAttribute.SetLastErrorまたはLibraryImportAttribute.SetLastErrorをtrueに設定している - 構造体のレイアウトがネイティブ側と一致している(パッキング、アラインメント、フィールドの順序)
- クロスプラットフォームコードでは、C の
long/unsigned longにCLong/CULongを使用している -
LibraryImportでCLong/CULongを使用する場合、[assembly: DisableRuntimeMarshalling]を適用している - 明示的な
MarshalAsのないboolがない。言語境界を越えて正規化を保証するため、常にUnmanagedType.Bool(4 バイト)またはUnmanagedType.U1(1 バイト)を指定している。
実行可能な検証手順
- 相互運用アナライザーを有効にしてビルドする —
SYSLIB1054~SYSLIB1057の警告がゼロであることを確認します。<EnableTrimAnalyzer>true</EnableTrimAnalyzer> <EnableAotAnalyzer>true</EnableAotAnalyzer> - 構造体のサイズが一致することを検証する — 境界を越えるすべての構造体について、
Marshal.SizeOf<T>()がネイティブのsizeofと等しいことをアサートします。 - ラウンドトリップテスト — 既知の入力値でネイティブ関数を呼び出し、期待どおりの出力が得られることを検証します。
- 非 ASCII 文字列でテストする — ASCII 範囲外の文字を含む文字列を渡し、エンコーディングが正しいことを確認します。
参考ファイル
- references/type-mapping.md — ネイティブから .NET への型マッピング表、構造体のレイアウトパターン、blittable 型のルールを網羅しています。次の場合に読み込んでください: 上記のステップ 2 で扱われていない型に遭遇した場合、または構造体のレイアウトや blittable 型に関する質問に取り組む場合。
- references/diagnostics.md — よくある落とし穴、失敗の原因と復旧方法、デバッグのアプローチ、外部リソースをまとめています。次の場合に読み込んでください: 既存の P/Invoke の失敗をデバッグする場合、または正確性に問題がないか相互運用コードをレビューする場合。