はじめに
以前の記事、C/C++ DLL から C# プログラムへのコールバックを実現する方法
で、P/Invoke の基本的な仕組みやコールバックの実装例を紹介しました。しかし、LibraryImport の自動マーシャリング機能 に関しては簡単に述べるにとどまり、詳細や注意点を十分に取り上げられませんでした。
そこで本記事では、この .NET 9
でさらに強化された “自動マーシャリング機能” にフォーカスし、その特徴・サポートされるデータ型・メリット・使いこなしのコツ、そして まだ手動のマーシャリングが必要になるケース などをまとめて解説します。
1. LibraryImport
とは? (おさらい)
.NET 7
でプレビュー機能として登場し、.NET 8
, .NET 9
で最適化が進んでいる 次世代の P/Invoke 方式です。
- ソースジェネレーター により、P/Invoke 呼び出し用のコードをコンパイル時に自動生成。
- 従来の
DllImport
と比較して、型安全性・パフォーマンス・メンテナンス性 が大幅に向上。 - ただし、現状は 動的ライブラリのロード(
LoadLibrary
等)には未対応のため、場合によってはDllImport
を使うケースも残っています。
2. LibraryImport の自動マーシャリングの特徴
.NET 9
では、自動マーシャリング機能 が特に強化されました。以下に、その主な特徴をまとめます。
-
ソースジェネレーターによるコード生成
開発者が手動で変換処理を記述する必要が大幅に減り、煩雑な設定を省略できます。 -
型安全性の向上
マネージド側とアンマネージド側の型不一致によるエラーをコンパイル時に検知しやすく、実行時クラッシュを減らせます。 -
パフォーマンスの最適化
実行時のマーシャリング処理が最適化され、オーバーヘッドを削減。大量の P/Invoke 呼び出しを行うシナリオでも従来より高速化が期待できます。
3. 自動マーシャリングでサポートされるデータ型
以下に、LibraryImport による自動マーシャリング がサポートされる代表的なデータ型を挙げます。
C# 型 | ネイティブ型 | 説明 |
---|---|---|
int | int / long | そのまま 32 ビット整数として変換 |
float | float | 32 ビット浮動小数点型 |
double | double | 64 ビット浮動小数点型 |
string | char* / wchar_t* | UTF-8 や Unicode の文字列として変換(指定可能) |
bool | int | C 言語の int(0: false, 非0: true)として変換 |
配列 (int[]) | ポインタ (int*) | 配列の先頭アドレスとして渡される |
構造体 | ネイティブ構造体(POD型) | フィールドが 1 対 1 にマッピング |
デリゲート | 関数ポインタ (void(*)(...)) | C++ 側の関数ポインタとして変換 |
このほかにも、StringMarshalling.Utf8
などの設定に応じて、より高度なマーシャリングに対応できます。
プラットフォーム(OS やコンパイラ)によって、C/C++ の型サイズが異なる場合も注意が必要です。
9.4 プラットフォームによる型サイズの違いに注意 も参照ください。
4. 文字列(string)の自動マーシャリング
4.1 文字エンコーディングの指定
C# の string
は UTF-16 が基本ですが、ネイティブ側の char*
はマルチバイト/UTF-8、あるいは wchar_t*
で Unicode など、エンコーディングが異なる場合があります。
LibraryImport では StringMarshalling
を指定することで、誤った変換を防げます。
UTF-8 文字列のマーシャリング
[LibraryImport("example.dll", StringMarshalling = StringMarshalling.Utf8)]
public static partial void ProcessString(string input);
ネイティブの char*
が UTF-8 として解釈され、C# の string
→ char*
(UTF-8) 変換がソースジェネレーターによって自動生成されます。
Unicode 文字列(wchar_t*)のマーシャリング
[LibraryImport("example.dll", StringMarshalling = StringMarshalling.Utf16)]
public static partial void ProcessString(string input);
string
→ wchar_t*
(UTF-16) 変換が行われ、ワイド文字列 としてネイティブへ渡されます。
5. 配列(int[])の自動マーシャリング
配列は、ネイティブの ポインタ(int など)* として自動的にマーシャリングされます。
5.1 配列のマーシャリング例
C++ 側
extern "C" __declspec(dllexport) int SumArray(const int* array, int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += array[i];
}
return sum;
}
C# 側
[LibraryImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
public static partial int SumArray(int[] array, int size);
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
int result = SumArray(numbers, numbers.Length);
Console.WriteLine($"Sum: {result}");
}
-
int[]
をそのまま引数にするだけで、先頭アドレス(int)* をネイティブに渡すコードが自動生成されます。 - 従来の
[MarshalAs(UnmanagedType.LPArray)]
を手書きする必要がありません。
6. デリゲート(関数ポインタ)の自動マーシャリング
C# のデリゲートは、ネイティブの関数ポインタに相当します。
6.1 デリゲートのマーシャリング例
C++ 側
typedef void (*CallbackFunc)(int value);
extern "C" __declspec(dllexport) void RegisterCallback(CallbackFunc callback) {
callback(42); // 値を渡してコールバックを実行
}
C# 側
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void CallbackDelegate(int value);
[LibraryImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
public static partial void RegisterCallback(IntPtr callback);
static void Main()
{
CallbackDelegate callback = (value) => Console.WriteLine($"Callback received: {value}");
IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(callback);
RegisterCallback(callbackPtr);
}
- デリゲートは、
Marshal.GetFunctionPointerForDelegate
を呼ぶことで関数ポインタ化し、ネイティブへ渡します。 -
.NET 9
+LibraryImport
でもここは手動ステップが必要ですが、以前より煩雑さは減っています。
7. 構造体(struct)の自動マーシャリング
構造体(C# の struct
と C++ の POD 型)が、フィールド同士で 1 対 1 に対応する場合は自動マーシャリングが適用されます。
7.1 構造体のマーシャリング例
C++ 側
struct Point {
int x;
int y;
};
extern "C" __declspec(dllexport) int CalculateDistance(Point p) {
return p.x * p.x + p.y * p.y;
}
C# 側
[StructLayout(LayoutKind.Sequential)]
struct Point
{
public int X;
public int Y;
}
[LibraryImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
public static partial int CalculateDistance(Point p);
static void Main()
{
Point point = new Point { X = 3, Y = 4 };
int distance = CalculateDistance(point);
Console.WriteLine($"Distance: {distance}");
}
- C# 側では
[StructLayout(LayoutKind.Sequential)]
を指定して、ネイティブの構造体との順序を揃えます。 - 従来の
[MarshalAs]
設定なしで、ソースジェネレーターが自動的にマッピングを行います。
8. 従来の DllImport との違い
8.1 手動マーシャリングの例(従来の DllImport
)
[DllImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int SumArray(
[MarshalAs(UnmanagedType.LPArray)] int[] array,
int size
);
- 手動で
[MarshalAs(UnmanagedType.LPArray)]
を付ける必要があり、設定ミスも起こりやすい。 - エンコーディングや構造体レイアウトなど、細かい指定を明示的に書かないと不具合につながりやすい。
8.2 自動マーシャリングの例(LibraryImport
)
[LibraryImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
public static partial int SumArray(int[] array, int size);
- ソースジェネレーターが自動でマーシャリングコードを生成するため、面倒な属性指定が不要。
- 設定ミスや型不一致がコンパイル時に検出されやすく、実行時エラーが減少。
- コードが シンプル で、メンテナンスしやすい。
9. 注意点 ~ まだ手動が必要なケース
.NET 9
の LibraryImport
でも、すべてのマーシャリングが完全に自動化できるわけではありません。ここでは代表的な 「手動のマーシャリングが必要になる例」 を詳しく紹介します。
9.1 戻り値として char*
を返す関数
ネイティブ側が以下のように 動的に確保した文字列を char*
で返す 形式は、依然として手動の対応が必要です。
// C++: malloc で確保した文字列を返す例
extern "C" __declspec(dllexport) char* GetAllocatedString()
{
const char* text = "Hello from Native!";
size_t len = strlen(text) + 1;
char* result = (char*)malloc(len);
memcpy(result, text, len);
return result;
}
// 解放用の関数も用意
extern "C" __declspec(dllexport) void FreeAllocatedString(char* ptr)
{
free(ptr);
}
C# 側では以下のように IntPtr
で受け取り、Marshal.PtrToStringAnsi
などを呼ぶ必要があります。さらに、解放も自分で呼ばなければリークを招きます。
[LibraryImport("example.dll")]
private static partial IntPtr GetAllocatedString();
[LibraryImport("example.dll")]
private static partial void FreeAllocatedString(IntPtr ptr);
static void Main()
{
// 1) ポインタを取得
IntPtr ptr = GetAllocatedString();
// 2) 文字列へ変換 (ANSI or UTF-8 対応可)
string msg = Marshal.PtrToStringAnsi(ptr)!;
Console.WriteLine($"[C#] Received: {msg}");
// 3) メモリ解放
FreeAllocatedString(ptr);
}
- ネイティブが
malloc
/free
等で管理するメモリを、.NET の自動マーシャリングだけで扱うのは困難です。 - そのため、誰がいつ解放するか といったライフサイクル管理を呼び出し側で対処しなくてはなりません。
9.2 “出力用バッファ” としてポインタを渡すケース
extern "C" __declspec(dllexport) void FillBuffer(char* buffer, int size);
-
呼び出し側がバッファを確保し、ポインタを渡す 設計の場合、
.NET
は「バッファサイズ」「メモリ確保/解放の方法」などを自動推測できません。 -
LibraryImport
の自動化だけではまかないきれず、Span<byte>
やIntPtr
を使って低レベルコードを書く必要があります。
9.3 特殊なエンコーディング(Shift-JIS など)や複雑なメモリ管理
-
.NET
は主に UTF-8/UTF-16 を想定しており、Shift-JIS や EUC-JP といった独自エンコーディングは標準サポート外。 - 自前で
IntPtr
を取り出して、カスタムの文字列変換ルーチンを呼ぶ必要があります。 - また、独自のプールアロケータや参照カウントで管理されるメモリなど、
.NET
が解放責任を推定できないパターンも、手動のコードが必須です。
9.4 プラットフォームによる型サイズの違いに注意
プラットフォーム(OS やコンパイラ)によって、C/C++ の型サイズが異なる 場合も注意が必要です。
- 例: Windows では
long = 4 bytes
、Linux/macOS ではlong = 8 bytes
。 - C# の
int
は常に 32 ビット、long
は常に 64 ビットで固定されているため、ネイティブの “long
” とマネージドのint
/long
が齟齬を起こす ことがあります。
回避策
-
ネイティブ側で固定幅の型 (
int32_t
,int64_t
) を使う
Windows / Linux など関係なく、同じビット幅で扱えるため、C# 側もint
(32bit) やlong
(64bit) と正しく対応付けできる。 -
C# 側で型を合わせる
ネイティブがint64_t
を使うなら C# ではlong
、ネイティブがint32_t
を使うなら C# ではint
を使う。
long
という名前に囚われず、明示的にビット幅を固定する のが最も確実です。
10. まとめ ~ 自動マーシャリングのメリットと今後の展望
10.1 特徴・メリット
-
ソースジェネレーター によるコード生成
- 開発者が手動で変換処理を記述する必要が減り、面倒な属性指定も最小限。
-
型安全性 & パフォーマンス向上
- コンパイル時にエラーを検出しやすく、ランタイムオーバーヘッドを削減。
-
広範な型サポート
- 配列、文字列、構造体、デリゲートなどのデータ型を自動的にマーシャリング。
-
コードが簡潔に
-
[MarshalAs]
等を手書きするよりコード量が減り、ミスも減少。
-
10.2 依然として手動が必要な場面
- ネイティブ側が動的に確保した
char*
を戻り値として返す場合。 - 出力用バッファをポインタで受け取る関数。
- 特殊エンコーディング(Shift-JIS 等)や、複雑なライフサイクル管理が絡む場合。
-
クロスプラットフォームで
long
のサイズなどが変わる場合(ビット幅の不一致)。
10.3 今後の展望
マイクロソフトは LibraryImport
に対して継続的に機能を拡張しており、将来的にはさらに多くのユースケースで自動化が進むと考えられます。
-
新規開発 では 「
LibraryImport
をメインに、動的ロードや特殊要件があるときにDllImport
」 という使い分けがおすすめ。 - 一部で手動マーシャリングが残るとはいえ、「自動化で完結できる範囲」が
.NET 9
以降で大幅に広がっている ことは確かです。
最終まとめ
-
.NET 9
のLibraryImport
は、ソースジェネレーターによる自動マーシャリングを大幅に強化- プリミティブ型・文字列・配列・構造体・デリゲートを簡潔にやり取りできる。
-
DllImport
に比べて型不一致エラーが起きにくく、パフォーマンス面でも有利。
-
まだ手動が必要なケースがある
- ネイティブが動的に確保した
char*
を戻り値として返す場合などは、自前でIntPtr
を扱う必要がある。 - バッファの受け渡し、特殊エンコーディング、クロスプラットフォームでのビット幅差にも注意。
- ネイティブが動的に確保した
-
「完全に不要」にはならないが、従来より手動作業が「格段に減る」
-
.NET 9
時点で「よくある P/Invoke シナリオ」はほぼ自動化でき、コード量とミスが大幅に削減される。 - 今後のバージョンで対応範囲がさらに広がる期待がある。
-
新規プロジェクトではまず LibraryImport
の使用を検討 し、上記のような特殊事情に当てはまる場合のみ手動マーシャリングを追加する、というスタンスが望ましいでしょう。