こんにちは、Unityエンジニアのオオバです。

C#からC++にデータを渡すシリーズ続けていきます。
前回はint型をC#からC++にわたしました。

あわせて読みたい記事

C#からC++にintを渡す方法

今回は配列を渡します。

ビルド環境については
コチラの記事をどうぞ。

あわせて読みたい記事

C#からC++に文字列を渡す方法

マーシャリングを理解して値を変換する

C#からC++に配列を渡してマーシャリングを理解しよう_0

C#からC++に配列はそのまま送信できません。
マーシャリングしてC++の読み取り可能なメモリ空間に
C#の値を変換します。

↑の内容を学んでいきます。
文字列、int型の送信のときより
若干内容が難しいです。

がんばって理解していきましょう。

👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

マーシャリングとはC#とC++間のデータの変換

C#からC++に配列を渡してマーシャリングを理解しよう_1

今回最も重要になのはマーシャリングの理解
配列をC#からC++に引き渡す上で必要になる概念です。

マーシャリングとは 異なるシステム間のデータ変換 を意味します。
今回の場合は、C#とC++間のデータの変換です。

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

Blittable型とは?

など。

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

非Blittable型とは?

非Blittable型とは以下のような型です。

など。

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

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

📚 参考サイト : 文字列に対する既定のマーシャリング | Microsoft Docs

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

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

C++は配列のポインタと要素数を受け取る

C#からC++に配列を渡してマーシャリングを理解しよう_2

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

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

💻ソースコード : TestIntArrayDll.cpp
#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++に配列を渡してマーシャリングを理解しよう_3

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++に送る点です。

オオバ
オオバ
しっかり覚えておきましょう。

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

C#からC++に配列を渡してマーシャリングを理解しよう_4

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

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

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++側でログ出力して正常に処理が実行されるでしょう。

まとめ : C#からC++に配列を渡す方法

C#からC++に配列を渡してマーシャリングを理解しよう_5

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

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

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

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

オススメ記事
検証環境
参考サイト