こんにちわ、C#大好きオオバです。

C#からC++にデータを渡すシリーズ続けていきます。
前回はint型をC#からC++にわたしました。
👉オススメ記事 : C#からC++(DLL)にintを渡す

今回は配列を渡します。

ビルド環境については、シリーズ初回の記事をどうぞ。
👉オススメ記事 : C#からC++(DLL)に文字列を渡す

📝 目次

マーシャリングの登場

今回配列をC#からC++に引き渡す上で重要になるのはマーシャリングです。
マーシャリングとは 異なるシステム間のデータ変換 を意味します。
今回の場合は、C#とC++間のデータの変換です。

以前C#からC++に int型 の値を送信しましたが、
マーシャリングしませんでした。
理由はint型がBlittable型だからです。

Blittable型とは?

など。

このようなプリミティブ型を
Blittable型と呼ばれます。

非Blittable型とは?

などです。

非Blittable型 をC#とC++間でやり取りする場合は、
マーシャリングが必要ということです。

文字列は非Blittable型ですが、
C#側からは文字列のポインターを渡すだけで済み、
明示的な変換処理は不要でした。
※内部的にマーシャリングされています
📚参考サイト : 文字列に対する既定のマーシャリング | Microsoft Docs

今回あつかう 配列非Blittable型です。
文字列のようにそのままネイテイブに渡すことはできないため、
マーシャリングが必要になります。

↓いったんこれだけ覚えておきましょう。

C++側の処理

まずはC++(dll)側の処理を見ていきます。
TestIntArrayメソッドでC#から配列を受け取ります。
※厳密には配列のポインタです

TestIntArrayメソッドに引数を2つ用意します。
C++のソースコードを見てみましょう。

#include "stdafx.h"
#include <iostream>
// C#から配列と要素数を渡す関数を定義  
DllExport void TestIntArray(int* array, int length);  

void TestIntArray(int* array, int length)  
{
    for (int i = 0; i < length; i++)  
    {
        std::cout << array[i] << std::endl;  
    }
}

配列を渡す場合は、
その要素数を渡す必要があることを覚えておきましょう。

int* arrayがポインタのため、
array.lengthと要素数を取得することはできないためです。

C#側の処理でマーシャリング

C#からC++に対して配列を送る手順です。

  1. C++の配列(アンマネージド配列)のメモリを確保
  2. C#の配列(マネージド配列)を「1.」で確保したメモリにコピー
  3. C#からC++に「1.」のポインタを渡す

ここではマーシャリングをしています。

マーシャリングとは 異なるシステム間のデータ変換 です。
C#とC++のメモリの扱いは違うため、
C#からC++にデータを送信するためには、
C++側のメモリ構造に合わせる必要があります。

C#側でアンマネージド配列のメモリを確保したのは、
そうしないとC++にデータを渡すことができないためです。

以上を頭に入れつつC#のソースコードを見ていきましょう。

💻ソースコード : Cs2CppArray.cs
[DllImport("TestDll.dll", CallingConvention = CallingConvention.Cdecl)]  
static extern void TestIntArray(System.IntPtr array, int length);  

static void Main()  
{
    // 配列要素数  
    var array = new int[] { 0, 1, 2, 3, 4 };  
    int length = array.length;  

    // 確保する配列のメモリサイズ(int型 5つ)  
    int size = Marshal.SizeOf(typeof(int)) * length;  

    // C++に渡す配列のアンマネージドメモリを確保  
    // ※「ptr」は確保したメモリのポインタ  
    System.IntPtr ptr = Marshal.AllocCoTaskMem(size);  

    // C#の配列をアンマネージドメモリにコピーする  
    Marshal.Copy(array, 0, ptr, length);  

    // C++に配列を渡す(ポインタを渡す)  
    TestIntArray(ptr, length);  

    // アンマネージドのメモリを解放  
    Marshal.FreeCoTaskMem(ptr);  
}

ここで重要なのは、
確保したアンマネージドメモリのポインタをC++に送る点です。
しっかり覚えておきましょう。

あらためてマーシャリングとは?

マーシャリングどこだったの?
という方に向けて復習していきます。

マーシャリングは以下の部分で実行していました。

int size = Marshal.SizeOf(typeof(int)) * length;  
// C++に送ることができるアンマネージドメモリの確保  
System.IntPtr ptr = Marshal.AllocCoTaskMem(size);  
Marshal.Copy(array, 0, ptr, length);  

重要なのは下の2行で、マーシャリングを行っているコードです。

Marshal.AllocCoTaskMem(メモリ確保するサイズ)で、
C++側に送信可能なアンマネージドメモリを確保

Marshal.Copy(C#の配列, 0, コピー先のポインタ, コピーする個数)で、
マネージド配列(array)をアンマネージドメモリにコピーし、
C#/C++間の値の変換をしたことになります。

このC#を実行すると無事に配列の中身を
C++側でログ出力して正常に処理が実行されるでしょう。

まとめ

int型、文字列の引き渡しは簡単でしたが、
配列はマーシャリング処理が必要となり、
少し難易度が上がりました。

この一連の流れがイメージできると、
そんなに難しくはないかもしれません。

使い終わったアンマネージドのメモリ解放も忘れずに。

C#とC++間のデータがやり取りできるようになると、
おもしろいですよね!

👉 オススメ記事

2021秋 Asset Refreshセール
100以上のアセットがなんと50%OFF!!オオバもいくつか買いました!
期間 : 10月2日午後3時59分まで

👩‍💻 検証環境

🙏 参考サイト