見出し画像

VB.Netの構造体とC++DLLとのデータのやりとりをまとめます!

VB.NetやC#などのマネージド環境からアンマネージドなC++のDLLにデータを受け渡したり、逆にDLLからVBへ値を渡すのは中々に大変です。その中でも構造体の扱いはかなり難易度高め。そこでこの記事では僕があれこれ試してみて取り敢えず成功したVBの(単純な型限定の)構造体とC++のDLLとのやり取りを備忘録として記します。

VBの構造体 → C++DLL

例としてC++側の構造体を次のように定義します:

struct MyData {
    int iVal;
    bool bVal;
    float fVal;
};

このままでも良いのですが、C++ではboolが大抵1バイトなので、この構造体はbValの下に何バイトか無効領域が挟まっています。いわゆる「アラインメント」です。これが何バイト挿入されているかは環境依存があります。そこで次のように#pragma packでアラインメントを明確化する事をお勧めします:

# pragma pack(push) ' デフォルトを保存
# pragma pack(1)  ' 1バイト境界に変更
struct MyData {
    int iVal;
    bool bVal;
    float fVal;
};
# pragma pack(pop) ' デフォルトに戻す

上の例だと1バイトアラインメント、つまりアラインメントが無くなりますので、この構造体のサイズは9バイトで確定します。もし#pragma pack(4)とすると4バイトアラインメントになるので、bValの下に3バイトの無効領域が暗黙で挿入、結果MyDataのサイズは12バイトになります。

DLLで構造体をやり取りする時、このアラインメント解決はマストなんですが、運用上色々あって「今更DLL側の構造体のアラインメントを変更できないよー!」っという場合って多々ありますので、以下はこの#pragma packをあえて使わないデフォルトなMyData構造体を想定します。

とは言え、構造体の何バイト目にどの変数があるのかはVB側で情報を受けたり渡したりする際に絶対に必要なため、確定しておかなくてはなりません。C++側の構造体のアラインメントによる無効領域(パディング領域)は目に見えませんので、きちっとメモリを見てチェックするか、次のように各メンバ変数のオフセットバイト数を書き出して調べましょう:

MyData *myData = 0;
uint32_t iValPs = (uint32_t)( &myData->iVal );
uint32_t bValPs = (uint32_t)( &myData->bVal );
uint32_t fValPs = (uint32_t)( &myData->fVal );

なんじゃこれは?と思うかもしれません。myDataポインタは0で初期化しています。これに対してmyData->iValとすると、iValの値を参照する事になります。「NULL参照なんて危ない!」と思うかもしれませんが、書き込みをしなければ大丈夫です。で、このメンバ変数のアドレスを&で取ると、myDataは0なので、構造体の先頭からの相対アドレス位置となります。それを数値に変換すれば構造体の先頭からの何バイト目にiValやbValなどが位置しているかわかる、というカラクリです。

こうして各メンバ変数の位置をバッチリ調べたら、つぎにその構造体を受け取る関数をDLLで公開します:

void setMyData( MyData *data );

DLLで関数を公開する方法はいくつかありますが、Visual C++ならモジュール定義ファイルを書くのが圧倒的に楽です。Visual Studioで「追加」→「モジュール定義ファイル」でプロジェクトに追加出来ます。拡張子は.defです。例えばmydll.defの中に、

LIBRARY mydll
EXPORTS
	setMyData

こう書くだけでsetMyData関数(グローバル関数)はこの名前でDLL側に公開されます。LIBRARYにあるのがDLLの名前、EXPORTS以下にあるのが関数の名前となります。__stdcall云々とかdllinport云々書くよりず~っと楽。

構造体を渡す側であるVBではC++に合わせた構造体を作るのですが…

Public Structure MyData
    Public iVal As Int32    ' 型をきっちり合わせます
    Public bVal As Byte     ' Booleanはサイズが曖昧なのでByteで
    Public _Pading0 As Byte  ' パディングが必要!
    Public _Pading1 As Byte 
    Public _Pading2 As Byte
    Public fVal As Single
End Structure

先に示したようにC++で定義したデフォルトMyData構造体は何らかのアラインメントに従ってメンバ変数が並んでいます。僕がDLLを作ったVisual Studio 2017と2019のx64環境だと4バイト境界でしたので、boolであるbValの下に3バイトの無効領域が入っていました。VB側の構造体でそれに対応するためbValの下に3Byte分のパディング用ダミー変数を入れています。これをしないとC++側での構造体とサイズが食い違う事になる為、特にDLL内部で値を書き込む時に書き込み位置がずれてメモリを破壊してしまいます。ですからこの「構造体のサイズをC++とVBとで完全に合わせる」のは必須です。ただし、C++の構造体のアラインメントが必ずしも4バイト境界とは限りませんので、必ずDLLを作成した環境でそれを確認して下さい!

VB側でC++に合わせた構造体を正しく作ったら、DLLにアクセスする関数をモジュールの中に宣言します:

Module MyDataModule

   'MyDataを渡す
   <DllImport("mydll.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.Cdecl)>
   Sub setMyData(ByVal myData As MyData)
   End Sub

End Module

ここの関数名はモジュール定義ファイルに書き込んだ名前と同じにする必要があります。C++側のsetMyData関数の引数はMyData*型ですが、VB側ではByValで構造体のオブジェクトを値渡しても大丈夫な模様です。試してみると特にエラー無く値を渡せました。ちなみにここをByRefにしてもうまくいきます…この辺りちょっと謎(-_-;。実際に構造体を渡す例はこちら:

Sub func()
    Dim data as MyData
    data.iVal = 100
    data.bVal = 1
    data.fVal = 3.14
    
    'データが入った構造体をDLL側に渡す
    setMyData( data )
end Sub

ちなみに構造体内に文字列があるとかなり面倒なので、今回はちょっと考えない事にします。

C++DLL → VB構造体

次はVBで定義した構造体をC++のDLLに渡して、DLL内で値を書き込んでもらう方法です。違う言い方をするなら、DLLから構造体として値を得るゲッターな方法です。

C++のDLLに公開する関数はやはりポインタで書き込み先の構造体を渡してもらうようにします:

void getMyData( MyData *data );

モジュール定義ファイルに関数名を追加しましょう:

LIBRARY mydll
EXPORTS
	setMyData
    getMyData

VB側のDLLアクセス関数は次のように定義します:

Module MyDataModule

   ....

   'MyDataを受け取る
   <DllImport("mydll.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.Cdecl)>
   Sub getMyData(ByRef myData As MyData)
   End Sub

End Module

受け取る時はByRefで構造体を参照渡しにして下さい!こうしないと受け取りに失敗します。

VBで作った空の構造体に値を書き込んでもらうVBのコード例はこちら:

Sub func2()
    Dim data as MyData
    
    '空の構造体にDLL内で値を書き込んでもらう
    getMyData( data )
end Sub

VB構造体配列 → C++DLL

実はここからが本番。今度はVBで構造体の「配列」を作ったとして、それをごそっとC++のDLLに渡す方法です。ただ先にお伝えしておきますと、VB側の構造体配列をC++側のDLLに直接渡す事は多分出来ません。ここではその代替案を示します。

DLL側はMyData配列の先頭ポインタとその要素数を渡す設計にします:

void setMyDataAry( MyData *dataAry, uint32_t num );

しつこいようですが(^-^;、モジュール定義ファイルを書きましょう:

LIBRARY mydll
EXPORTS
	setMyData
    getMyData
    setMyDataAry

実際良く忘れるんですよ、これ。そして「あれーVB側でDLLの関数読めない、例外出るー、なんでー(T-T)」と慌てるんです。なので真っ先に書きましょう。

さてVB側ですが、残念ながらVBの構造体配列は直接C++のDLLには渡せません。それはVBの構造体配列がC++のそれとは全然違う概念で出来ていて、C++のように単純にメモリ上に一列に並んでくれていない為です。これ、散々色々なパターンを試したのですが、ついぞうまくいきませんでした。ではどうするか?「VB側で配列を一度メモリにずらっと並べてC++のDLLに渡す」という事をせざるを得ません。

それを実現する最初のステップ。VBの構造体をMarshal対応にします:

<StructLayout(LayoutKind.Sequential)>
Public Structure MyData
    Public iVal As Int32
    Public bVal As Byte
    Public _Pading0 As Byte
    Public _Pading1 As Byte
    Public _Pading2 As Byte
    Public fVal As Single
End Structure

StructLayout(LayoutKind.Sequential)という属性を付けます。これはこの構造体の変数が特定のサイズを持ち、且つメモリ内にこの順番で並ぶことを保証するものです。この属性を付けないと以後の操作が出来ません。

DLLアクセス関数は次のように構造体配列を書き込んだメモリの先頭ポインタをIntPtrで渡すようにします:

Module MyDataModule

    ....

   'MyDataの配列を渡す
   <DllImport("mydll.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.Cdecl)>
   Sub setMyDataAry(ByVal myDataPtr As IntPtr, ByVal num as UInt32)
   End Sub

End Module

IntPtrはポインタのようにアドレス位置を扱うVBの型です。C++でいう所のunsigned char*とほぼ同じと考えて良いです。

このsetMyDataAry関数にデータを渡すために、次にIntPtrの先に有効なメモリブロックを用意して、そこに構造体のデータを書き込んでいきます:

Sub func3()

    Dim dataAry(10) As MyData  ' MyDataの配列

    ' ここでdataAryに何らかの値を入れる '

    Dim sz = Marshal.SizeOf( Of MyData )()   ' 構造体のサイズを取得
    Dim ptr As IntPtr = Marshal.AllocHGlobal( 10 * sz )  ' 構造体10個分のメモリ確保
    Dim p = ptr
    For i = 0 To 10 - 1
        Marshal.StructureToPtr( Of MyData )( dataAry(i), p, False ) 'pが指すメモリに構造体の値を書き込み
        p = p + sz   ' 構造体のサイズ分だけオフセット
    Next
    setMyDataAry( ptr, 10 )   ' 配列データをごそっと渡す

    Marshal.FreeHGlobal( ptr )  ' メモリを解放

End Sub

うわ~ってなるかもしれませんが大丈夫。一つずつ噛み砕いていきましょう。

まず渡したいVB側のMyData配列を作ります。値は適当な物で埋めましょう。次にMyData構造体のサイズを計算しておきます。これはMarshal.SizeOfという関数で取得可能です。Marshal.SizeOf(Of <型>)()という書き方で<型>のC++(アンマネージド)でのサイズが返ります。ただし、<型>が構造体の場合は先のStructLayout(LayoutKind.Sequential)属性が付いていないとここでエラーになってしまいます。上の例ではsz=12になるはずです。

続いて得た構造体のサイズと配列の要素数からMarshal.AllocHGlobalでメモリを確保します。引数に確保したいメモリサイズを渡すと、ヒープメモリを確保してくれて、その先頭ポインタをIntPtrとして返します。これはC++のmallocそのものですね。上の例では120バイトが確保されます。

得たメモリブロックに構造体の情報を流し込むにはMarshal.StructureToPtr関数を使います。第1引数に構造体のオブジェクト、第2引数に書き込み先のIntPtrを与えます。第3引数は書き込み先のメモリに既にオブジェクトがある場合にそれを解放するならTrue、無条件で上書きするならFalseを指定します。今は空のメモリを確保したのでFalseでOKですね。第2引数のアドレスは次の構造体を書き込む時に正しい位置にずらさないといけません。その計算をp = p + szで行っています。12バイトずつずらして書き込み、ずらして書き込み…を10回やると。

これでptr先に10個のMyDataが並んだので、setMyDataAry関数にptrを渡せば、メモリに間違いなくデータが並んでいますから、C++側でそのメモリを直接参照して値を格納できます。

Marshal.AllocHGlobalで得たメモリはちゃんと解放しないとメモリリークになってしまいます。Marshal.FreeHGlobal(ptr)で解放しましょう。C++でのfree関数と同じですね。

C++DLL → VB構造体配列

最後はVB側の構造体配列へC++のDLL内からデータを書き込む方法です。これも残念ながらVBの構造体配列に直接データを書き込む事は出来ません(VBとC++とでメモリ配置が全然違うため)。その為上と同じように書き込み用の空メモリを用意して、それをDLLに渡してデータを書いてもらい、それをVBの構造体配列に再度書き戻します。

C++側はこれまでと同じように構造体配列の先頭ポインタと要素数を渡してもらう設計にします:

void getMyDataAry( MyData *dataAry, uint32_t num );

もう言わずもがなですが、モジュール定義ファイルに関数名を追加します:

LIBRARY mydll
EXPORTS
	setMyData
    getMyData
    setMyDataAry
    getMyDataAry

VB側のDLLアクセス関数は先と同様にIntPtrを渡すように宣言します:

Module MyDataModule

    ....

   'MyDataの配列を取得
   <DllImport("mydll.dll", CharSet:=CharSet.Auto, CallingConvention:=CallingConvention.Cdecl)>
   Sub getMyDataAry(ByVal myDataPtr As IntPtr, ByVal num as UInt32)
   End Sub

End Module

用意した構造体配列にデータを書き込んでもらうVBのコードは以下の通りです:

Sub func4()

    Dim dataAry(10) As MyData  ' MyDataの配列
    Dim sz = Marshal.SizeOf( Of MyData )()   ' 構造体のサイズ(12バイト)を取得
    Dim ptr As IntPtr = Marshal.AllocHGlobal( 10 * sz )  ' 構造体10個分のメモリ確保
    getMyDataAry( ptr, 10 )   ' 配列データをptr先のメモリに書き込んでもらう
    Dim p = ptr
    For i = 0 To 10 - 1
        dataAry( i ) = Marshal.PtrToStructure( Of MyData )(p) 'pの位置にあるデータをMyData構造体に変換
        p = p + sz   ' 構造体のサイズ分だけオフセット
    Next
    Marshal.FreeHGlobal( ptr )  ' メモリを解放

End Sub

最終的にdataAry配列にC++側から渡されたデータを格納します。構造体のサイズをMarshal.SizeOfで得て、ptrの先に書き込み分の空メモリをMarshal.AllocHGlobalで120バイト確保します。getMyDataAry関数にそのメモリの先頭アドレスptrと要素数10を渡して、DLL内でそのメモリに構造体10個分の値を直接書き込んでもらいます。関数を抜ければ、ptrの先にMyDataの情報が並んでいるはずです。

For文の中でpが指すメモリに書き込まれた生データをMyDataオブジェクトに変換してdataAry配列に格納しています。これにはMarshal.PtrToStructureを利用します。コードにあるように(Of MyData)と変換したい構造体の型を指定し、引数に有効なデータが存在するアドレスを指定します。ptrアドレス先にはすでに10個分の配列が一直線に並んでいますので、p = p + szでアドレス位置を構造体のサイズ分ずらして一つずつ格納していきます。最後にメモリの解放を忘れずに。

以上でVB.Net⇔C++DLLでの構造体のやり取り(単体、配列)が出来ます!これについて、ネットにはかなり古い情報ややり方の一部、解決してるのか良く分からないQ&Aが沢山見受けられますが、大丈夫、ここにまとめました(^-^)/。もし忘れたらまた見に来て下さい。

それではまた(^-^)/

(こちらの情報が貴方様のお役に立ちましたら、ぜひいいねをポチッとお願いします~。間違いやもっと良い方法がありましたらコメントにてご連絡頂けますと幸いです。)

この記事が気に入ったらサポートをしてみませんか?