.NET 9 の `LibraryImport` 自動マーシャリング機能を使いこなすうえでの注意点

---
## はじめに
以前の記事、[C/C++ DLL から C# プログラムへのコールバックを実現する方法](https://qiita.com/sukimaengineer/items/6b8a41c26fb84ca0d4ea)
で、P/Invoke の基本的な仕組みやコールバックの実装例を紹介しました。しかし、**LibraryImport の自動マーシャリング機能** に関しては簡単に述べるにとどまり、詳細や注意点を十分に取り上げられませんでした。


そこで本記事では、この **`.NET 9` でさらに強化された “自動マーシャリング機能”** にフォーカスし、その特徴・サポートされるデータ型・メリット・使いこなしのコツ、そして **まだ手動のマーシャリングが必要になるケース** などをまとめて解説します。

---

## 1. `LibraryImport` とは？ （おさらい）

`.NET 7` でプレビュー機能として登場し、`.NET 8`, `.NET 9` で最適化が進んでいる **次世代の P/Invoke** 方式です。  
- **ソースジェネレーター** により、P/Invoke 呼び出し用のコードをコンパイル時に自動生成。  
- 従来の `DllImport` と比較して、**型安全性・パフォーマンス・メンテナンス性** が大幅に向上。  
- ただし、現状は **動的ライブラリのロード**（`LoadLibrary` 等）には未対応のため、場合によっては `DllImport` を使うケースも残っています。

---

## 2. LibraryImport の自動マーシャリングの特徴

`.NET 9` では、**自動マーシャリング機能** が特に強化されました。以下に、その主な特徴をまとめます。

1. **ソースジェネレーターによるコード生成**  
   開発者が手動で変換処理を記述する必要が大幅に減り、煩雑な設定を省略できます。

2. **型安全性の向上**  
   マネージド側とアンマネージド側の型不一致によるエラーをコンパイル時に検知しやすく、実行時クラッシュを減らせます。

3. **パフォーマンスの最適化**  
   実行時のマーシャリング処理が最適化され、**オーバーヘッドを削減**。大量の 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` などの設定に応じて、より高度なマーシャリングに対応できます。

:::note info
プラットフォーム（OS やコンパイラ）によって、C/C++ の型サイズが異なる場合も注意が必要です。
**9.4 プラットフォームによる型サイズの違いに注意** も参照ください。
:::

---

## 4. 文字列（string）の自動マーシャリング

### 4.1 文字エンコーディングの指定

C# の `string` は UTF-16 が基本ですが、ネイティブ側の `char*` はマルチバイト/UTF-8、あるいは `wchar_t*` で Unicode など、エンコーディングが異なる場合があります。  
**LibraryImport** では **`StringMarshalling`** を指定することで、誤った変換を防げます。

#### UTF-8 文字列のマーシャリング

```csharp
[LibraryImport("example.dll", StringMarshalling = StringMarshalling.Utf8)]
public static partial void ProcessString(string input);
```

ネイティブの `char*` が UTF-8 として解釈され、C# の `string` → `char*`（UTF-8） 変換がソースジェネレーターによって自動生成されます。

#### Unicode 文字列（wchar_t*）のマーシャリング

```csharp
[LibraryImport("example.dll", StringMarshalling = StringMarshalling.Utf16)]
public static partial void ProcessString(string input);
```

`string` → `wchar_t*`（UTF-16） 変換が行われ、**ワイド文字列** としてネイティブへ渡されます。

---

## 5. 配列（int[]）の自動マーシャリング

**配列**は、ネイティブの **ポインタ（int* など）** として自動的にマーシャリングされます。

### 5.1 配列のマーシャリング例

#### C++ 側

```cpp
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# 側

```csharp
[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++ 側

```cpp
typedef void (*CallbackFunc)(int value);

extern "C" __declspec(dllexport) void RegisterCallback(CallbackFunc callback) {
   callback(42); // 値を渡してコールバックを実行
}
```

#### C# 側

```csharp
[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++ 側

```cpp
struct Point {
   int x;
   int y;
};

extern "C" __declspec(dllexport) int CalculateDistance(Point p) {
   return p.x * p.x + p.y * p.y;
}
```

#### C# 側

```csharp
[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`）

```csharp
[DllImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int SumArray(
    [MarshalAs(UnmanagedType.LPArray)] int[] array, 
    int size
);
```

- 手動で `[MarshalAs(UnmanagedType.LPArray)]` を付ける必要があり、設定ミスも起こりやすい。  
- エンコーディングや構造体レイアウトなど、細かい指定を明示的に書かないと不具合につながりやすい。

### 8.2 自動マーシャリングの例（`LibraryImport`）

```csharp
[LibraryImport("example.dll", CallingConvention = CallingConvention.Cdecl)]
public static partial int SumArray(int[] array, int size);
```

- ソースジェネレーターが自動でマーシャリングコードを生成するため、**面倒な属性指定が不要**。  
- 設定ミスや型不一致がコンパイル時に検出されやすく、実行時エラーが減少。  
- コードが **シンプル** で、メンテナンスしやすい。

---

## 9. 注意点 ～ まだ手動が必要なケース

`.NET 9` の `LibraryImport` でも、すべてのマーシャリングが完全に自動化できるわけではありません。ここでは代表的な **「手動のマーシャリングが必要になる例」** を詳しく紹介します。

### 9.1 戻り値として `char*` を返す関数

ネイティブ側が以下のように **動的に確保した文字列を `char*` で返す** 形式は、依然として手動の対応が必要です。

```cpp
// 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` などを呼ぶ必要があります。さらに、解放も自分で呼ばなければリークを招きます。

```csharp
[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 “出力用バッファ” としてポインタを渡すケース

```cpp
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` が齟齬を起こす** ことがあります。

#### 回避策
1. **ネイティブ側で固定幅の型 (`int32_t`, `int64_t`) を使う**  
   Windows / Linux など関係なく、同じビット幅で扱えるため、C# 側も `int` (32bit) や `long` (64bit) と正しく対応付けできる。  
2. **C# 側で型を合わせる**  
   ネイティブが `int64_t` を使うなら C# では `long`、ネイティブが `int32_t` を使うなら C# では `int` を使う。  
   `long` という名前に囚われず、**明示的にビット幅を固定する** のが最も確実です。

---

## 10. まとめ ～ 自動マーシャリングのメリットと今後の展望

### 10.1 特徴・メリット

1. **ソースジェネレーター** によるコード生成  
   - 開発者が手動で変換処理を記述する必要が減り、面倒な属性指定も最小限。  
2. **型安全性 & パフォーマンス向上**  
   - コンパイル時にエラーを検出しやすく、ランタイムオーバーヘッドを削減。  
3. **広範な型サポート**  
   - 配列、文字列、構造体、デリゲートなどのデータ型を自動的にマーシャリング。  
4. **コードが簡潔に**  
   - `[MarshalAs]` 等を手書きするよりコード量が減り、ミスも減少。

### 10.2 依然として手動が必要な場面

- ネイティブ側が動的に確保した `char*` を戻り値として返す場合。  
- 出力用バッファをポインタで受け取る関数。  
- 特殊エンコーディング（Shift-JIS 等）や、複雑なライフサイクル管理が絡む場合。  
- **クロスプラットフォームで `long` のサイズなどが変わる場合**（ビット幅の不一致）。

### 10.3 今後の展望

マイクロソフトは `LibraryImport` に対して継続的に機能を拡張しており、将来的にはさらに多くのユースケースで自動化が進むと考えられます。  
- **新規開発** では **「`LibraryImport` をメインに、動的ロードや特殊要件があるときに `DllImport`」** という使い分けがおすすめ。  
- 一部で手動マーシャリングが残るとはいえ、**「自動化で完結できる範囲」が `.NET 9` 以降で大幅に広がっている** ことは確かです。

---

# 最終まとめ

1. **`.NET 9` の `LibraryImport` は、ソースジェネレーターによる自動マーシャリングを大幅に強化**  
   - プリミティブ型・文字列・配列・構造体・デリゲートを簡潔にやり取りできる。  
   - `DllImport` に比べて型不一致エラーが起きにくく、パフォーマンス面でも有利。  

2. **まだ手動が必要なケースがある**  
   - ネイティブが動的に確保した `char*` を戻り値として返す場合などは、自前で `IntPtr` を扱う必要がある。  
   - バッファの受け渡し、特殊エンコーディング、クロスプラットフォームでのビット幅差にも注意。

3. **「完全に不要」にはならないが、従来より手動作業が「格段に減る」**  
   - `.NET 9` 時点で「よくある P/Invoke シナリオ」はほぼ自動化でき、コード量とミスが大幅に削減される。  
   - 今後のバージョンで対応範囲がさらに広がる期待がある。

**新規プロジェクトではまず `LibraryImport` の使用を検討** し、上記のような特殊事情に当てはまる場合のみ手動マーシャリングを追加する、というスタンスが望ましいでしょう。