LoginSignup
14
13

More than 1 year has passed since last update.

C#とC++間の配列と構造体の授受まとめ

Last updated at Posted at 2022-03-05

はじめに

C#とC++間の構造体と配列の授受の方法をまとめました。
C#側で unsafe文は使わない のと、C++/CLI は使わずに純粋な C++ を使うという条件にします。

○ 条件の理由 ↓
C++/CLIはマネージドコードとアンマネージドコードを混在させることができるのでできれば使用したいのですが、FFIの機能である P/Invoke だけを使用します。残念ながら現在(2022/3/6)、Linux上で C++/CLI が動かないからです。
C#のunsafe文を使っても良いのですが、使わずにできることに越したことはないと思うのでこの記事では使わずにできる方法を書いてます。

概要

早見表的なものなのと、情報&記述は粗削りです。ごめんなさい。
メモリの解放等は省略しています。わかりやすさ重視の最低限のコードです。

基本的に構造体も配列も
主体がC#①マネージド領域②アンマネージ領域
主体がC++③アンマネージ領域
の3種類です。
(本当はもっといろいろな方法があるかもしれません、もしあったら教えてください)

まだ書きかけです。

C++のコードにあるDLL_APIはLinuxの場合は空白に、Windowsの場合は__declspec(dllimport)に置換されます。

マネージドコードとアンマネージコード について

マネージド コードとは、その実行がランタイムによって管理されるコードです。

C#の通常のコードはマネージドコードであり、CLRに管理されたものです。
しかし、意図的にC#側でCLRに管理されないアンマネージコードを生成することもできます。
この記事では、C++側のネイティブなメモリ領域にあるコードもアンマネージコード(あるいはアンマネージ領域)と呼ぶことにします。
名称未設定ファイル.drawio.png
※ C++からマネージド領域のアクセスはC#側から提供されるマネージド領域のポインタを介してなら可能です。

構造体の授受

① C#側のマネージド領域に生成した構造体を直接C++側で読み書きする方法(C#→C++)

  C#側でアンマネージド領域に構造体をコピーするコストが無く、C++で書き換え後もC#側でそのまま読み書きできるのがメリットです。

  • C#側
C#
//C#
 public struct CsStruct { //授受する構造体の型を定義
    public int x;
    public int y;
}

public static void Main() {
    CsStruct cst = new CsStruct{x=1, y=2};// 普通の構造体と同じように定義&初期化
    NativeMethod.getCsStruct(ref cst);    // refでそのまま構造体(の参照)をC++側に渡せる。
    Console.WriteLine(cst.x);             // C++側で書き換えた内容が出力される。(この例では10)
}
public static class NativeMethod { 
    [DllImport("xxx.dll")]
    public static extern void getCsStruct(ref CsStruct ret); //引数はref ([out, in]でも可能...?未検証)
}
  • dll(共有ライブラリ)側
C++
//C++
struct DLL_API CsStruct {
    int x;
    int y;
};

DLL_API void getCsStruct(CsStruct* ret); // C#の構造体のrefはポインターで受け取る。

void getCsStruct(CsStruct* ret) {
    ret->x = 10;  //メンバー(x)に数字を代入してみる。
}

コンソールには 10 と表示される。

② C#側のマネージド領域の構造体をアンマネージド領域にコピーしそのポインターをC++側に渡す方法

・まだ書いてません...

③ C++側のアンマネージ領域に構造体を定義(生成)しポインターをC#側に送りマネージド領域にコピーして使用する方法

この方法のキーポイントは、Marshal.PtrToStructure(IntPtr,typeof(構造体の型))を使ってアンマネージド領域からマネージド領域にコピーする点。

C#
//C#
public struct CppStruct {
    public int x;
    public int y; 
}

public static void Main() {
    //C++側で定義した構造体のポインターを取得
    IntPtr ptr = NativeMethod.getCppStruct();
    //ポインター(ptr)先の構造体をマネージド領域にコピー(マーシャリング)
    CppStruct cpps = (CppStruct) Marshal.PtrToStructure(ptr, typeof(CppStruct)); 
    // 10 と表示される
    Console.WriteLine(cpps.x); 
}

public static class NativeMethod {
   //C++側から構造体のポインターを取得する
   public static exterm IntPtr getCppStruct();
}
C++
struct CppStruct {
    int x;
    int y;
};

DLL_API CppStruct* getCppStruct();

//静的領域に生成しているけど、newで動的に生成してもOK
CppStruct cpps = {10, 20}; 

CppStruct* getCppStruct() {
    return &cpps;
}

配列の授受

① C#側のマネージド領域に生成した配列を直接C++側で読み書きする(C#→C++)

C#側でアンマネージド領域に配列をコピーするコストが無く、C++で書き換え後もC#側でそのまま読み書きできるのがメリットです。

C#
//C#
pullic static void Main() {
   int len = 10;
   double[] marray = new double[len]; //マネージド配列を作る
   NativeMethod.writeManagedArray(len, marray); // C++側にマネージド配列(の参照?ポインタ?)を渡す
   foreach (double i in marray) { 
       Console.WriteLine(i); //C++側で書き換えた内容を表示する
   }
}
 
public static class NativeMethod {
  [DllImport("xxx.dll")]
  public static extern void writeManagedArray(int len, double[] parr);
}

C++
//C++
 extern "C" DLL_API void writeManagedArray(int len, double* parr);

// 配列の長さとマネージド領域を指すポインター。マネージドであっても普通のポインターで受け取れるらしい。
void writeManagedArray(int len, double* parr) {     
    for (size_t i = 0; i < len; ++i) {
        parr[i] = i; //マネージド領域にある配列をC++側でループして初期化できる!
    }
}

② C#側のマネージド領域の配列をアンマネージド領域にコピーしそのポインターをC++側に渡す方法

もしかしたら、C++側から長く使っているとGCに削除されるかもしれない...?
その場合はunsafeなfixedを使ってFCの対象外にするか、GCHandle構造体でラップすることでGCの対象外にすることができるらしい。

以下のコードのように、Marshal.AllocCoTaskMem() でメモリを確保する場合は大丈夫らしいです。
コメントで教えて頂きました!@radian-jp さんありがとうございます!

C#
//C#
public static void Main() {
   int length = 4;
  // C#側でマネージド領域の配列を作る
   double[] array = new double[length] { 1.0, 2.0, 3.3, 4.0}
   // 確保する配列のメモリサイズ(double型 × 長さ)  
   int size = Marshal.SizeOf(typeof(double)) * length;
   // C++に渡す配列のアンマネージドメモリを確保  
   // ptrは確保したメモリのポインタ  
   System.IntPtr ptr = Marshal.AllocCoTaskMem((int)size);
   // C#のマネージド配列をアンマネージドメモリにコピーする  
   Marshal.Copy(array, 0, ptr, length);
   // C++に配列を渡す(ポインタを渡す)  
   // System.IntPtr型で渡す
   NativeMethod.sendArray(ptr, len); 
   // アンマネージドのメモリを解放(メモリの解放が必要...?)
   Marshal.FreeCoTaskMem(ptr);
}
public static class NativeMethod {
   //要素の型がdoubleでもすべてIntPtrを使う。(void *みたいなものかな?)
  [DllImport("xxx.dll")]
  public static extern void sendArray(IntPtr ptr, int len); 
}
C++
//C++
extern "C" {
    DLL_API int* sendArray(double* ptr, int len);
}
int* sendArray(double* ptr, int len) {
   // 省略
}

③ C++側のアンマネージ領域に配列を定義(生成)しポインターをC#側に送りマネージド領域の配列にコピーして使用する。

アンマネージド領域にある配列をC#側で直接アクセスできそうなのですが、やり方がよくわからず...
(力技でできるのかな??)
unsafe でないと無理らしいです。 Span<T> はよくわかってないのですが unsafe でアンマネージな配列も unsafe 無しに扱えるようになるのでしょうか...?
@radian-jp さんありがとうございます!

なので、C#側でマネージド配列に値をコピーします。(コピーコストがかかる)

C#
//C#
public static void Main() {
  //C++側から配列のポインタを受け取る
  IntPtr pArray = NativeMethod.getArray();
  // コピー先のマネージド配列を定義
  int[] array = new int[5];
  //C++側から受け取ったポインタを使用してアンマネージド配列からマネージド配列に値をコピー   
  Marshal.Copy(pArray, array, 0, array.Length); 
  foreach (int i in array) {
      Console.WriteLine(i); // 配列の内容を表示
  }
}

public static class NativeMethod { 
  [DllImport("xxx.dll")]
    public static extern IntPtr getArray(); 
}
C++
//C++
extern "C" {
    DLL_API int* getArray();
}
int* getArray() {
    int t[] = {1, 2, 3, 4, 10};  // ↓の配列を初期化するための配列なので気にしない
    int* arr = new int[5];       // 長さ5の配列をヒープ領域に生成。このポインタをC#に渡す。
    for (size_t i = 0; i < 5; ++i) {
        arr[i] = t[i]; //ループして初期化
    }
    return arr; // 渡す
}

構造体の中に配列を含めたものの授受

まだ書いてません・・・ 追記しました↓
基本的に上記の組み合わせでできると思います。

例えば、C#側でマネージド構造体を作り、その中にマネージド配列をメンバに持つ構造体をC++側に送って配列の中身を書き換えたい とします。

① C#側のマネージド領域に生成した構造体を直接C++側で読み書きする方法(C#→C++)

① C#側のマネージド領域に生成した配列を直接C++側で読み書きする(C#→C++)

を使えば良いことがわかります。
なので以下のような構造体をC#側で定義してしまうでしょう(結論を言うとこの方法はダメです。)

C#
//C#
 public struct Hoge { //この構造体をマネージド領域に生成しC++側に送りたい
     public double[] array; // マネージド配列をC++側に渡したい。
   public Hoge(int size) {
           array = new array[size]; //マネージド配列を生成
     }
 }
public static void Main() {
        Hoge hoge = new Hoge(5); //マネージド領域に構造体を生成
        NativeMethod.sendHoge(ref hoge); //hogeのポインタを送る
        foreach (double i in hoge.array) {
            Console.WriteLine("hoge.array " + i); //arrayの中身を見る
        }
}

C++側は以下のように定義します。

C++
//C++
struct Hoge {
   double* array; // マネージド配列にアクセスしたい
};
void get(Hoge* hoge) {
    std::array<double> arr = { 1, 3.3, 4, 12, 15 };
    int len = 5;
    for (size_t i = 0; 0 < len; ++i) {
        hoge->array[i] = arr.at(i); // ここでエラーになる。System.Runtime.InteropServices.SEHException
    }
}

色々見て回ったのですがこの方法ではダメらしい。
C#側で以下のように属性を使って構造体のメンバの配列を固定長配列として定義しないといけないと書いてあったのですが、私の環境ではこれでもできませんでした...うーむ

C#
public struct Hoge {
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 5)] //長さ5の固定長配列
  public double[] array; // ダメでした
}
 

この方法でできたとしても、動的な長さを持つ配列をメンバに持つことができないので、不便です。

従って、今のところできる解決策(メンバに配列を持った構造体をC++側に渡す)は、

C#側のマネージド構造体をアンマネージド領域にコピーしてC++側でその構造体にアクセス

するしかないようです。

C++からC#に返すときは逆(C++側で構造体を生成して、C#側でC++側のアンマネージ領域の構造体をC#側のマネージド領域にコピー )をします。

↓その方法はまだ書いてません....

その他

[StructLayout(LayoutKind.Sequential)] など、アライメントを意識して書かないといけないらしいのですが、まだ不具合にあったことがありません。
その辺りはまだ全然わからないまま記事を書いています。
何かわかれば追記するかもしれません。

参考・リンク

14
13
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
13