プラットフォーム呼び出し (P/Invoke)
P/Invoke は、アンマネージド ライブラリ内の構造体、コールバック、および関数をマネージド コードからアクセスできるようにするテクノロジです。 P/Invoke API のほとんどは、System
と System.Runtime.InteropServices
の 2 つの名前空間に含まれます。 これら 2 つの名前空間を使用すると、ネイティブ コンポーネントと通信する方法を記述するツールを利用できます。
最も一般的な例から始めましょう。これはマネージド コードでアンマネージド関数を呼び出すものです。 コマンドライン アプリケーションからメッセージ ボックスを表示してみましょう。
using System;
using System.Runtime.InteropServices;
public partial class Program
{
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
private static partial int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
public static void Main(string[] args)
{
// Invoke the function as a regular managed method.
MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
}
}
上の例は単純ですが、マネージド コードからアンマネージド 関数を呼び出すために必要なことを示しています。 この例の手順について説明します。
- 2 行目は、必要なすべてのアイテムを保持する
System.Runtime.InteropServices
名前空間のusing
ステートメントを表しています。 - 8 行目で、LibraryImportAttribute 属性を導入しています。 この属性は、アンマネージド バイナリーを読み込む必要があることをランタイムに指示します。 渡される文字列は、ターゲット関数を含むアンマネージド バイナリです。 さらに、文字列のマーシャリングに使用するエンコードを指定します。 最後に、この関数が SetLastError を呼び出し、ランタイムでそのエラー コードをキャプチャしてユーザーが Marshal.GetLastPInvokeError() を介して取得できるように指定します。
- 9 行目は、P/Invoke 作業の最も重要な箇所です。 ここでは、アンマネージドと正確に同じシグネチャを持つマネージド メソッドを定義しています。 この宣言では、
LibraryImport
属性とpartial
キーワードを使用して、アンマネージド ライブラリを呼び出すコードを生成するようにコンパイラ拡張機能に指示します。- 生成されたコード内および .NET 7 以前のコードでは、
DllImport
が使用されます。 この宣言では、extern
キーワードを使用して、これが外部メソッドであることをランタイムに示し、このメソッドを呼び出すと、ランタイムはDllImport
属性で指定されたアンマネージド バイナリ内でそのメソッドを見つける必要があることを示します。
- 生成されたコード内および .NET 7 以前のコードでは、
例の残りの部分は、その他のマネージド メソッドと同じように、メソッドを呼び出しています。
サンプルは、macOS の場合も似ています。 macOS ではダイナミック ライブラリの名前付けのスキームが異なるため、LibraryImport
属性内のライブラリの名前を変更する必要があります。 次のサンプルでは、getpid(2)
関数を使用して、アプリケーションのプロセス ID を取得し、それをコンソールに出力しています。
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libSystem shared library and define the method
// corresponding to the native function.
[LibraryImport("libSystem.dylib")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
これは Linux でも同様です。 getpid(2)
は標準的な POSIX システム コールであるため、関数名が同じです。
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libc shared library and define the method
// corresponding to the native function.
[LibraryImport("libc.so.6")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
アンマネージド コードからのマネージド コードの呼び出し
ランタイムは通信が双方向にフローする通信を許可しているため、関数ポインターを使用して、ネイティブ関数からマネージド コードをコール バックすることができます。 マネージド コードで、関数ポインターに最も近いものが、デリゲートであるため、これをネイティブ コードからマネージド コードへのコールバックを許可するために使います。
この機能を使用する方法は、前述のマネージドからネイティブへのプロセスに似ています。 指定されたコールバックに対し、ユーザーがシグネチャと一致するデリゲートを定義し、それを外部メソッドに渡します。 ランタイムは、他のすべてのことを処理します。
using System;
using System.Runtime.InteropServices;
namespace ConsoleApplication1
{
public static partial class Program
{
// Define a delegate that corresponds to the unmanaged function.
private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll")]
private static partial int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);
// Define the implementation of the delegate; here, we simply output the window handle.
private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
{
Console.WriteLine(hwnd.ToInt64());
return true;
}
public static void Main(string[] args)
{
// Invoke the method; note the delegate as a first parameter.
EnumWindows(OutputWindow, IntPtr.Zero);
}
}
}
例の手順に従う前に、使用する必要があるアンマネージ関数のシグネチャについて見直しておくのはよいことです。 すべてのウィンドウを列挙するために呼び出す関数は、次のシグネチャを持ちます。BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);
最初のパラメーターでは、コールバックです。 上記のコールバックのシグネチャは次のとおりです。BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);
次の例を見てみましょう。
- 例の 9 行目では、アンマネージ コードからのコールバックのシグネチャと一致するデリゲートを定義しています。 マネージド コードで
IntPtr
を使用して、LPARAM および HWND 型を表す方法に注意してください。 - 13 行目と 14 行目では、user32.dll ライブラリから
EnumWindows
関数を導入しています。 - 17 - 20 行目は、デリゲートを実装しています。 この簡単な例では、ハンドルだけコンソールに出力します。
- 最後に、24 行目で、外部メソッドを呼び出し、デリゲートを渡しています。
Linux と macOS の例を、以下に示します。 それらの場合、libc
C ライブラリに見つかる ftw
関数を使用します。 この関数は、ディレクトリ階層をスキャンするために使用し、そのパラメーターの 1 つとして、関数へのポインターを受け取ります。 上記のメソッドのシグネチャは次のとおりです。int (*fn) (const char *fpath, const struct stat *sb, int typeflag)
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libc.so.6", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
macOS の例では、同じ関数を使用していますが、唯一の違いは LibraryImport
属性への引数です。macOS は libc
を別の場所で保持しているためです。
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libSystem.dylib", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
前述の例のどちらも、パラメーターに依存し、どちらの場合もパラメーターは、マネージド型として指定されています。 ランタイムは、"正しいこと" を実行し、これらを他方の側で同等のものに処理します。 型をネイティブ コードにマーシャリングする方法については、「Type marshalling (型のマーシャリング)」を参照してください。
その他のリソース
- クロス プラットフォーム P/Invoke の作成
- ソースで生成された P/Invoke マーシャリング
- C#/Win32 P/Invoke ソース ジェネレーターにより、Windows API の定義が自動的に生成されます。
- C++/CLI での P/Invoke
- P/invoke に関する Mono のドキュメント
.NET