渋谷ほととぎす通信

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

【Unity】C#とC++間で構造体を送信し合う方法

【Unity】C#とC++間で構造体を送信し合う方法

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

お悩みさん
お悩みさん
  • C#からC++に構造体を渡したい
  • C#からC++ってデータを渡せるの?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    C#とC++間のデータやりとりを実装したことありますか?Unityが提供する機能を普通に使っているだけだと、あまり出てこないシチュエーションかも知れません。

    Unityが提供していない機能にアクセス といったUnityの外へ一歩出ようとするとC++や別の言語が必要になります。

    今回はC#から++に対して「構造体」のデータをC++に渡してみます。

    構造体とはとは値型のデータをまとめて扱うためのものです。クラス(class)と似ていますが値型で管理され軽量で高速なのが特徴です。ここで言う値型とは「int型」「float型」「double型」などを指し、bool型、string型は値型ではありません。

    今回は値型を変数に持つ構造体なので以下のような構造体をC#からC++に送ってみましょう。

    public struct SendStruct  
    {
        public int level;  
        public float avoidRatio;  
    }
    

    C#からC++にデータを送るための環境構築はこちらの記事を参考にしてみてください。

    C#、C++ともに同じ構造体をつくる

    結論を簡単にまとめます。

    • 型を合わせた構造体をC#、C++両方に定義
    • 構造体をマーシャリングして送信

    「マーシャリング」 とは異なるプラットフォーム間でのデータのやり取りに使う技術です。C#とC++ではメモリの取り扱いが違います。

    C#とC++間でデータをやり取りするためには メモリ空間をC#とC++で合わせる必要がある のです。

    今回はC#からC++へデータを送信するだけでなく C++からC# へデータを戻すということもやっていきます。

    具体的には次のような処理を作成します。

    【Unity】C#とC++間で構造体を送信し合う方法_0

    C#からC++の関数を呼び出す際にC#の構造体データをを渡します。呼び出したC++関数内で構造体を作成しC#に送ります。

    では具体的なC#/C++間の構造体データ送信の方法について解説をしていきます。

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

    送信し合うデータをC#とC++両方で同じように定義

    まずはC#とC++側で送信する構造体のデータをそれぞれで定義していきます。ポイントはC#とC++間で書式は違えど 型を合わせること です。型が異なると正しくデータ送信できません。

    C++に送信するC#の構造体

    まずはC#側でC++に送信する構造体を定義します。今回使用する構造体はこちらです。int型変数1つ、float型変数1つのシンプルなデータにしました。

    💻ソースコード : C#側の構造体
    public struct StructData  
    {
        public int id;  
        public float value;  
    }
    

    上記のC#構造体をC++で受け取るために C++側でも同様の型を定義 していきます。

    C#と同じ構造体をC++ではポインタで定義

    C#の構造体を受け止めるためにC++内でも同様の構造体を定義します。

    💻ソースコード : C++の構造体
    typedef struct  
    {
        int id;  
        float value;  
    }StructData;  
    

    C++側の構造体は上記通りです。C#とC++では文法が違うので注意してください。ただし似た点が多いため理解しやすいと思います。

    intやfloatはC#/C++間で共通で使用可能です。

    C#/C++間で送信し合う処理の実装

    C#とC++それぞれで送信する構造体の定義ができたため、ここから送信処理を実装していきます。

    C++側の実装

    C++側の実装から進めていきます。今回は SendStruct という関数を宣言しました。この関数を通してC#とC++間で構造体を受け渡します。

    C#から呼び出したい関数にリンケージ指定子 「extern "C"」 と「__declspec(dllexport) 」をつけます。

    💻ソースコード : C++側にC#から呼ばれるのメソッドを宣言
    extern "C" __declspec(dllexport) void SendStruct(StructData* output, StructData* input)  
    

    以上のようにSendStructを宣言しました。SendStruct 関数には2つの引数を定義しています。

    • C++からC#に送る構造体の変数「output」
    • C#からC++に送る構造体の変数 「input」

    どちらもポインタ型です。SendStruct関数内の処理を実装していきます。

    💻ソースコード : C#から呼ぶC++内のメソッド定義全容
    extern "C" __declspec(dllexport) void SendStruct(StructData* output, StructData* input)  
    {
        // outputのidに、inputのidに20加算した値を代入  
        output->id = input->id + 20;  
        // outputのvalueに、inputのvalueに0.9999加算した値を代入  
        output->value = input->value + 0.9999;  
    }
    

    SendStruct 関数の実装自体はシンプルで input で渡された値に定数を加算した値を output に代入します。

    「->」 はアロー演算子と呼ばれ、ポインタ型の構造体内の値にアクセスできます。

    以上でC++側の実装は完了です。C++ソースコード全文は記事のの最後で共有しているのでぜひ参考にしてみてください。

    C#側の実装

    次にC#側の実装に移ります。C++側で実装した関数名SendStructを同命で宣言し、引数もポインタ型にします。ただしC++側とは少し引数の書式に差分があるため詳しく解説していきます。

    [DllImport("TestDll.dll", CallingConvention = CallingConvention.Cdecl)]  
    static extern void SendStruct(ref StructData output, IntPtr input);  
    
    • 第1引数 output・・・引数修飾子「ref」をつけた構造体型
    • 第2引数 input : 構造体のポインタ

    このようにC#で定義します。

    outputはC++側から送られる構造体です。「ref」をつけただけで構造体StructData型をそのまま宣言できるため特に落とし穴はありません。

    ポイントは第2引数のinputが IntPtr型 つまりポインタ型になった点です。

    お悩みさん
    お悩みさん
    構造体をどうやってポインタ型に変換するの?

    ポインタはあくまでメモリのアドレスです。マーシャリングしてデータを送信します。

    C#からC++に送る構造体をマーシャリング

    繰り返しになりますが「マーシャリング」とは 異なるシステム間で必要なデータ変換 です。C#/C++間でデータをやり取りする場合はマーシャリングが必要になります。手順は次の2ステップ。

    1. C++に送信するデータのメモリ確保
    2. 確保したメモリにC#のデータをコピー

    1.C++に送信するデータのメモリ確保

    今回C#からC++に送信するデータ(構造体)のメモリを確保します。メモリ量を調べるときは Marshal.SizeOf関数 を使います。

    💻ソースコード : 必要なメモリ量の調査
    Marshal.SizeOf(typeof(StructData))  
    

    上記の1行で構造体StructDataが必要とするメモリ量が取得できます。

    必要なメモリ量がわかったら次は実際に確保を行います。具体的なコードで表すと 「Marshal.AllocCoTaskMem」 を使用します。

    // StructDataが必要とするメモリ量  
    var memorySize = Marshal.SizeOf(typeof(StructData))  
    
    // アンマネージドの構造体のメモリ確保  
    IntPtr inputPtr = Marshal.AllocCoTaskMem(memorySize);  
    

    Marshal.AllocCoTaskMem関数 の引数に必要なメモリ量を渡すことでメモリの確保が完了します。

    配列型データを送信する際もマーシャリングが登場しました。↓詳しくはこちらの記事を参考にしてみてください。

    2.確保したメモリにC#のデータをコピー

    確保したメモリにC++に送信するC#の構造体データをコピーします。

    💻ソースコード : C++に送るC#の構造体をメモリにコピー
    var sendCsToCppData = new StructData()  
    {
        id = 10,  
        value = 0.5f  
    };  
    // C++に送るC#の構造体をメモリにコピー  
    Marshal.StructureToPtr(sendCsToCppData, inputPtr, false);  
    

    Marshal.StructureToPtr関数 を使ってC++に送信するデータ「sendCsToCppData」をコピーします。

    Marshal.StructureToPtr関数
    引数内容備考
    第1引数コピーする構造体C#の構造体
    第2引数コピー先のメモリのポインタ-
    第3引数既存のメモリを解放するフラグfalseだと解放しない

    こんな感じでC#の構造体(sendCsToCppData)をメモリにコピーしました。

    以上でマーシャリングは完了です。最後にC#からC++の関数を呼び出してデータの送受信を行います。

    C#とC++間でデータ送信

    準備が整ったのでC#とC++間でデータを送信しましょう。C#側で定義したSendStruct関数を呼び出します。

    呼び出す際にC++側から受け取る構造体をあらかじめC#側から渡すのがポイントです。

    // C++から渡される構造体  
    var result = new StructData();  
    SendStruct(ref result, inputPtr);  
    

    このように第1引数のresult変数にC++から送信された構造体データが格納されます。C#側はresult変数にアクセスして処理の続きを実装していくというイメージです。

    以上でC#とC++間における構造体データの送受信は完了です。

    まとめ : C#とC++間で構造体を送り合う方法

    本記事ではC#とC++間で構造体を送り合う方法を解説してきました。記事の内容を簡単にまとめます。

    C#とC++間で構造体を送り合う方法

    ①C#、C++ともに構造体の型を合わせる

    ②C++は構造体をポインタで受け取る

    ③マーシャリングしてC#からC++へデータ送信

    ④C++から受け取る構造体はC#側から参照を渡す

    こんな感じです。

    C#とC++間で構造体をデータを実際に送り合ってみていかがだったでしょうか。ポインタやマーシャリングといったUnityだけではあまり登場しない作法が混乱を招いたかもしれません。

    しかし慣れてしまえばそこまで難しくはありません。内容を変更した構造体を定義して実際に自分で実装してみましょう。回数をこなすことがスキルアップの第一歩です。

    最後に今回作成したサンプルコード全文を共有いたします。参考にしてみてください。

    → サンプルコード全体

    「Unity初心者大学」というUnity初心者向けのYouTube始めました!!
    ぜひチャンネル登録をお願いします!

    最後まで読んでいただきありがとうございました!
    すばらしいC#/C++間データ送信ライフをお過ごしください。

    オススメ記事
    検証環境
    • Visual Studio Community 2022 (64 ビット) v17.13.1
    • Windows11
    • Unity6000.0.39f1
    参考サイト