渋谷ほととぎす通信

エンジニア社長によるUnityとAIのブログ & エンジニアの生存戦略

マーシャリングって何?C#からC++に配列を渡す方法【Unity】

マーシャリングって何?C#からC++に配列を渡す方法【Unity】

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

お悩みさん
お悩みさん
  • C#からC++に配列を渡したい
  • すんなり渡せない?
  • マーシャリング...?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    Unityでゲーム開発しているとOS固有の機能にアクセスしたくなるときがあります。すると途端にC#では対応できず、C++やObjective-C、Swift、Javaといったプラットフォームに対応した言語を使う必要が出てきます。

    本記事はC#からC++にデータ転送基礎シリーズ。過去にint型、文字列型と解説していきました。

    今回は 「配列型」 データをC#からC++に渡す方法を解説していきます。

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

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

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

    本記事で理解できること

    ①マーシャリングとは?

    ②Blittable型と非Blittable型の理解

    ③具体的なマーシャリング方法

    ④具体的な配列型の転送方法

    マーシャリングやBlittableといった特殊なキーワードが出てきましたが、これらは記事内で回収しますのでご安心を。

    1つ言えることは過去紹介した文字列型やint型のときより内容が複雑です。とはいっても本ブログはだれにでもわかりやすく伝えるをテーマにしているので、頑張ってわかりやすく紹介していきます。

    では本編にいきます。

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

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

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

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

    お悩みさん
    お悩みさん
    int型を転送するときにマーシャリングいらなかったような。。?

    その通り、C#からC++にint型を送信する際、マーシャリングは不要でした。理由は int型がBlittable型 だったからです。

    マーシャリング、Blittable型を理解する上で「マネージドメモリ」と「アンマネージドメモリ」について理解をする必要があります。

    マネージドメモリとアンマネージドメモリとは

    皆さんがプログラムで扱うデータは次の2つのメモリを使用しています。

    • マネージドメモリ
    • アンマネージドメモリ

    マネージドメモリとはC#(.NET)が管理する領域です。一方アンマネージドメモリとはC++側のメモリ領域です。両者は性質が異なります。

    具体的にはマネージドメモリはGC(ガベージコレクション)という仕組みで自動でメモリ管理をしてくれます。方アンマネージドメモリは開発者が自分でメモリを開放する必要があるのです。

    C#とC++でデータを受け渡すとき2つのメモリ領域でデータが行き来しているということを意識しておいてください。

    マネージドとアンマネージドのイメージができたら「Blittable型」について解説していきます。

    Blittable型とは?

    Blittable型とはマネージドメモリとアンマネージドメモリで共通して使用されるデータ型です。C#では以下がBlittable型です。

    • System.Byte
    • System.SByte
    • System.Int16
    • System.UInt16
    • System.Int32
    • System.UInt32
    • System.Int64
    • System.UInt64
    • System.IntPtr
    • System.UIntPtr
    • System.Single
    • System.Double
    • Blittable型で構成した構造体

    これらはC#からC++にデータ送信する上で変換の不要です。

    非Blittable型とは?

    Blittable型の反対「非Blittable型」もあります。Blittable型の逆なのでC#からC++へ送信時にデータの変換(マーシャリング)が必要になります。

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

    • 配列型
    • bool型
    • 文字列型(Char, String)
    • Object型
    • Class
    • ValueType
    • T[]

    など。

    これらの 非Blittable型 をC#とC++間でやり取りする場合は マーシャリングが必要 ということを覚えておきましょう。

    文字列も非Blittable型?

    お悩みさん
    お悩みさん
    あれ?文字列型も非Blittable型なの?

    以前紹介したC#からC++に文字列を渡す記事ではマーシャリングが登場しませんでした。

    理由はC#側からは文字列のポインターを渡すだけで済むため、明示的なマーシャリング処理が不要だからです。

    もちろんプログラムの内部ではマーシャリングされています。


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

    とりあえず次の2点を覚えておきましょう
    • Blittable型 : マーシャリング不要
    • 非Blittable型 : マーシャリング必要

    以降の章で具体的なマーシャリング方法について解説していきます。

    具体的な配列型のマーシャリング

    ではここから具体的なマーシャリング及び、C#とC++間のデータをやり取りしていきます。

    データを受け取るC++側の処理

    まずはデータを受け取るC++側の処理を実装していきます。

    C#側から実行されるメソッド 「TestIntArray」 を宣言し、C#から 配列のポインタ を受け取る処理を記述していきます。

    配列型を受け取るときは以下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つの手順です。

    1. C++に送るアンマネージドメモリの確保
    2. C#の配列データを「1.」で確保したアンマネージドメモリにコピー
    3. C#からC++に「1.」のポインタと要素数を渡す
    お悩みさん
    お悩みさん
    マーシャリングってどこでやるの?

    マーシャリングは「1.」「2.」のタイミングです。

    繰り返しになりますがマーシャリングとは 異なるシステム間のデータ変換 です。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++で管理するアンマネージドメモリは自分で開放する必要があります。前述のソースコード内に以下の行に注目してみてください。

    Marshal.FreeCoTaskMem(ptr);  
    

    このタイミングで確保したアンマネージドメモリの解放を行っています。この処理を入れないとメモリーリークを起こしてアプリがクラッシュするかもしれません。

    主にネイティブプラグインを作成するときにC++に触れることが多くなりますが、メモリの解放は念入りにチェックしておきましょう。

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

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

    • アンマネージドメモリを確保
    • C#配列のデータをコピー
    • C++にポインタと要素数を引き渡す

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

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

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

    オススメ記事
    検証環境
    • VisualStudioCommunity2017 v15.9.8
    • Windows10
    参考サイト