bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] ポインター実践例② — P/Invoke を使った C++ DLL との相互運用

[C#] ポインター実践例② — P/Invoke を使った C++ DLL との相互運用

Jun 12, 2026 C# , .NET bucket-sort

このシリーズは全 4 記事で構成されています。

  • ① なぜポインターが必要か / unsafe の基本 / * と & 演算子
  • ② -> 演算子 / stackalloc / fixed / sizeof
  • ③ 実践例:ビットマップ画像処理
  • ④ 本記事 — C++ DLL との相互運用

① で述べたように、C# でポインターを使うもう一つの主な理由は、ポインター型を引数に要求する C ベースの DLL や Windows API を呼び出すことです。この記事では C++ で実装された DLL を例に、C# 側でポインターをどう扱うかを解説します。

P/Invoke とは

P/Invoke(Platform Invocation Services)は .NET が提供するネイティブコード呼び出しの仕組みです。[DllImport] 属性を使って DLL のエクスポート関数を C# のメソッドとして宣言し、通常のメソッド呼び出しと同じように使えます。

C++ 関数の引数にポインターが含まれる場合、C# 側の宣言でも対応する型を正しく指定する必要があります。

題材にする C++ DLL

以下の 3 つの関数を持つ C++ DLL を想定します。

// MathLib.cpp(C++ 側のコード)

#include <cstring>

extern "C" {

    // 2 つの整数を加算して結果をポインター経由で返す
    __declspec(dllexport)
    void Add(int a, int b, int* result)
    {
        *result = a + b;
    }

    // 配列の各要素を 2 倍にする(配列をポインターで受け取る)
    __declspec(dllexport)
    void DoubleArray(int* data, int length)
    {
        for (int i = 0; i < length; i++)
            data[i] *= 2;
    }

    // 文字列を大文字に変換して出力バッファに書き込む
    __declspec(dllexport)
    void ToUpperCase(const char* input, char* output, int bufferSize)
    {
        for (int i = 0; i < bufferSize - 1 && input[i] != '\0'; i++)
            output[i] = (char)toupper((unsigned char)input[i]);
        output[bufferSize - 1] = '\0';
    }

}  // extern "C"

extern "C" は C++ の名前マングリング(名前修飾)を防ぐためのものです。C# の P/Invoke は関数名をそのまま使うため、C++ のマングリングが入ると名前が一致しなくなります。

アプローチ A:unsafe + ポインターで呼び出す

Add 関数の呼び出し

using System.Runtime.InteropServices;

class Program
{
    // DLL の関数宣言(unsafe 版)
    [DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern unsafe void Add(int a, int b, int* result);

    static unsafe void Main()
    {
        int result = 0;
        Add(10, 20, &result);
        Console.WriteLine($"10 + 20 = {result}"); // 30
    }
}

int* をそのまま引数に使えるのがポインター方式のシンプルなところです。

DoubleArray 関数の呼び出し

配列をポインターで渡すには fixed でピン止めしてからアドレスを渡します。

[DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern unsafe void DoubleArray(int* data, int length);

static unsafe void CallDoubleArray()
{
    int[] numbers = { 1, 2, 3, 4, 5 };

    fixed (int* p = numbers)
    {
        DoubleArray(p, numbers.Length);
    }
    // fixed を抜けるとピン止めが解除される

    foreach (var n in numbers)
        Console.Write($"{n} "); // 2 4 6 8 10
    Console.WriteLine();
}

fixed が必要な理由は ② で解説した通りです。GC がオブジェクトを移動するとポインターが無効になるため、fixed で移動を禁止しています。

ToUpperCase 関数の呼び出し(文字列)

文字列は char*(非管理型の文字バッファ)を介してやり取りします。

[DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern unsafe void ToUpperCase(byte* input, byte* output, int bufferSize);

static unsafe void CallToUpperCase()
{
    string input = "hello, world!";
    byte[] inputBytes  = System.Text.Encoding.ASCII.GetBytes(input + "\0"); // null 終端
    byte[] outputBytes = new byte[256];

    fixed (byte* pIn  = inputBytes)
    fixed (byte* pOut = outputBytes)
    {
        ToUpperCase(pIn, pOut, outputBytes.Length);
    }

    string result = System.Text.Encoding.ASCII.GetString(outputBytes).TrimEnd('\0');
    Console.WriteLine(result); // HELLO, WORLD!
}

アプローチ B:IntPtr + Marshal で呼び出す(unsafe 不要)

① で触れたように、多くのケースでは System.IntPtr と System.Runtime.InteropServices.Marshal クラスを使えば unsafe なしで同じことができます。

Add 関数の呼び出し(IntPtr 版)

// unsafe なし版
[DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void Add(int a, int b, IntPtr result);

static void CallAddSafe()
{
    // アンマネージドヒープに int 1 個分のメモリを確保
    IntPtr ptr = Marshal.AllocHGlobal(sizeof(int));
    try
    {
        Add(10, 20, ptr);
        int result = Marshal.ReadInt32(ptr);
        Console.WriteLine($"10 + 20 = {result}"); // 30
    }
    finally
    {
        Marshal.FreeHGlobal(ptr); // 必ず解放
    }
}

Marshal.AllocHGlobal はアンマネージドヒープにメモリを確保します(C の malloc 相当)。GC の管理外なので FreeHGlobal で必ず解放する必要があります。

DoubleArray 関数の呼び出し(IntPtr 版)

[DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void DoubleArray(IntPtr data, int length);

static void CallDoubleArraySafe()
{
    int[] numbers = { 1, 2, 3, 4, 5 };
    int byteSize = numbers.Length * sizeof(int);

    IntPtr ptr = Marshal.AllocHGlobal(byteSize);
    try
    {
        // 配列をアンマネージドメモリにコピー
        Marshal.Copy(numbers, 0, ptr, numbers.Length);

        DoubleArray(ptr, numbers.Length);

        // 結果を配列にコピーして戻す
        Marshal.Copy(ptr, numbers, 0, numbers.Length);
    }
    finally
    {
        Marshal.FreeHGlobal(ptr);
    }

    foreach (var n in numbers)
        Console.Write($"{n} "); // 2 4 6 8 10
    Console.WriteLine();
}

ToUpperCase 関数の呼び出し(IntPtr / 文字列マーシャリング版)

文字列の場合は [DllImport] の CharSet や MarshalAs を使えば、マーシャリングを自動化できます。

// 文字列を自動マーシャリング(ANSI 文字列として渡す)
[DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void ToUpperCase(
    [MarshalAs(UnmanagedType.LPStr)] string input,
    System.Text.StringBuilder output,
    int bufferSize
);

static void CallToUpperCaseSafe()
{
    var output = new System.Text.StringBuilder(256);
    ToUpperCase("hello, world!", output, output.Capacity);
    Console.WriteLine(output); // HELLO, WORLD!
}

StringBuilder を出力バッファとして渡すと、P/Invoke が自動的に char* に変換してくれます。入力文字列には [MarshalAs(UnmanagedType.LPStr)] を付けると ANSI(const char*)として渡されます。

2 つのアプローチの比較

unsafe + ポインター IntPtr + Marshal
unsafe キーワード 必要 不要
コードの簡潔さ シンプル(ポインターをそのまま渡す) やや冗長(AllocHGlobal/Copy/Free が必要)
メモリ管理の責任 fixed の範囲で自動 FreeHGlobal を手動で呼ぶ必要がある
配列の扱い fixed でピン止め Marshal.Copy でコピー(オーバーヘッドあり)
向いている用途 高頻度呼び出し・大きな配列 稀な呼び出し・チームが unsafe を避けたい場合

パフォーマンスが重要で配列が大きいときは unsafe + ポインター方式のほうが有利です(Marshal.Copy のコピーコストがない)。コードの安全性・可読性を重視するなら IntPtr + Marshal 方式が無難です。

呼び出し規約(Calling Convention)

DLL の関数を呼び出すとき、CallingConvention の指定が一致していないとスタックが壊れてクラッシュします。C 言語で書かれた DLL は通常 Cdecl、Win32 API は StdCall です。

// C ライブラリ → Cdecl
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]

// Win32 API → StdCall(デフォルトなので省略可)
[DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall)]

C++ のメンバー関数(__thiscall)は P/Invoke で直接呼び出せないため、extern "C" のラッパー関数を C++ 側に用意するのが一般的です。

まとめ

トピック ポイント
P/Invoke の基本 [DllImport] で DLL 関数を宣言し、通常のメソッドとして呼び出す
ポインター引数(unsafe 版) fixed でピン止めしてからポインターを渡す
ポインター引数(Marshal 版) AllocHGlobal でアンマネージドメモリを確保し IntPtr を渡す
文字列 [MarshalAs] と StringBuilder を使えば自動マーシャリングが効く
呼び出し規約 Cdecl(C DLL)か StdCall(Win32 API)を DLL に合わせて指定する

C++ の DLL と連携する際は、できる限り Marshal や自動マーシャリングを使って unsafe の範囲を最小化するのが C# らしい書き方です。ただしパフォーマンスが求められる場面では、ポインターを直接扱う unsafe 方式が有効な選択肢になります。

C# .NET Unsafe ポインター Pointer P/Invoke DllImport 相互運用 Interop
← [C#] ポインター実践例① — unsafe コードによるビットマップ画像処理の高速化 [C#] デリゲートを理解する: デリゲート型の基本 →

Related Posts

  • [C#] ポインター実践例① — unsafe コードによるビットマップ画像処理の高速化 Jun 11, 2026
  • [C#] ポインター型(Pointer Types)② — -> 演算子 / stackalloc / fixed / sizeof Jun 10, 2026
  • [C#] ポインター型(Pointer Types)入門 ① — unsafe コードの基本と * / & 演算子 Jun 9, 2026
  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026

Table of Contents

  • P/Invoke とは
  • 題材にする C++ DLL
  • アプローチ A:unsafe + ポインターで呼び出す
    • Add 関数の呼び出し
    • DoubleArray 関数の呼び出し
    • ToUpperCase 関数の呼び出し(文字列)
  • アプローチ B:IntPtr + Marshal で呼び出す(unsafe 不要)
    • Add 関数の呼び出し(IntPtr 版)
    • DoubleArray 関数の呼び出し(IntPtr 版)
    • ToUpperCase 関数の呼び出し(IntPtr / 文字列マーシャリング版)
  • 2 つのアプローチの比較
  • 呼び出し規約(Calling Convention)
  • まとめ

Recent Posts

  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026
  • [C#] 属性でコードに意味を与える Jul 8, 2026
  • [C#] 外部アセンブリを読み込み実行時に型を呼び出す Jul 7, 2026
  • [C#] リフレクションで型の中身を調べる Jul 6, 2026
  • [C#] 型メタデータとリフレクションの基礎を理解する Jul 5, 2026

Categories

  • C#120
  • .NET119
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • コレクション
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • Apache
  • System.Collections.Generic
  • デリゲート
  • Code Snippet
  • DynamoDB
  • LINQ
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メモリ管理
  • リフレクション
  • DoS
  • Nginx
  • WordPress
  • メタデータ
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
  • Webサーバー
  • インターフェース
  • オブジェクト指向
  • クラス設計
  • デザインパターン
  • パターンマッチング
  • ポインター
  • 継承
Powered by Hugo & Explore Theme.