こんな情報が今後どれだけ役に立つのか不明ですが、先日、とあるシステムの開発において、VC++6.0で作成されたDLLから、可変長の情報を受け取る必要がでてきました。
VB6.0側で最大サイズのメモリを確保しておいてDLLに引数として渡そうとしたのですが、64KBを超えるといってエラーになってしまいました。
どうしたものかと思って、GlobalAllocでメモリを確保してみたり、いろいろやったのですがなかなかうまくいかず。
今回の構造体はメンバがいくつかあるのですが、すべて文字、または文字列です。
VC++での宣言でいうと
typedef struct
{
char data1[1];
char data2[5];
char data3[10];
}TEST_DATA;
といった感じ。
VB側では
Type TEST_DATA
data1 As String * 1
data2 As String * 5
data3 As String * 10
End Type
という感じで宣言しています。
VB側でGlobalAllocで確保してみたけれど、メモリのコピーが面倒だし、どうもポインタがズレてしまうようでうまくいかず。
最終的にファイル渡しにして逃げたのですが(苦笑)
あのあと、いろいろ実験してみて一応の解決策が見つかったので記録として残しておきます。
やりたいこと
VB6.0からVC++6.0で作ったDLLに構造体の配列を渡し、DLL側で値をセットしてVB6.0に返したい。
構造体のメンバは、それぞれdata1、data2、data3という文字列型とする。
配列は本来可変長だが、VB6側でメモリを確保してDLLに渡すため、最大数を確保しておく。
文字コードが違う
まず、考えるべきこととしてVB6.0は内部的に文字をUnicodeという、半角文字も2バイトで扱っています。
VC++6.0では基本的にShift JISなので半角文字は1バイトです。
この文字コードの違いも考慮する必要があります。
例えば、String * 1と定義しても、VBでは2バイトとして扱っているということです。
どうもVB側の構造体はアライメントが適用される
VB6.0で構造体をつくると、4バイト境界になるように調整されるようなのです。
これ、VC++側でも同じようにすればいいのかもしれないけど、なかなか大変です。
構造体の定義
試行錯誤の結果、一応これでできそう、というものが見つかったのでメモしておきます。
今回の場合、VB6.0でふつうに書けば下記のようになります。
Type TEST_DATA
data1 As String * 1
data2 As String * 5
data3 As String * 10
End Type
しかし、これらは、2バイト、10バイト、20バイトのメモリが確保された状態となります。
これを考慮してVC側で倍のメモリを確保してもいいのですが、ちょっと面倒ですね。
というわけで、逆にVB側でバイト単位で確保しようという作戦です。
アライメントを気にしなくてもいいように、文字列はByte型で定義しました。
どうもByte型で定義したものはアライメント調整されないようなのです。
型によって違うんですね。
Type TEST_DATA
data1(0) As Byte
data2(4) As Byte
data3(9) As Byte
End Type
と書きます。
ちょっと注意する必要があるのは、C言語と配列の宣言の仕方が違うという点。
data1(0) As Byte
と書くと、インデックスの最大値が0の配列を作る、という意味になります。
そのため、1バイトです。
data1(1) As Byte
と書くと、インデックスの最大値が1、つまり0と1の2バイト確保されるのです。
C言語だと char data1[1]; とすると1バイトっていう意味なので、混乱しそうですよね。
data3(0 To 9) As Byte
と書くと、より分かりやすいかもしれません。
0から9までの10バイト、という意味です。
配列の宣言
配列を作ってみましょう。
Dim DataArray() As TEST_DATA
ReDim DataArray(9)
という感じで、10個の配列を作成します。
ここでもインデックスの最大値を指定します。10と書いたら11個の領域が確保されちゃう事に注意。
C言語側の関数の宣言
呼び出される側のC言語の関数は、ポインタを受け取る必要があります。
また、今回の場合、VB側でメモリの確保を行っているので、配列の個数も受け取っておきます。
__declspec(dllexport) void __stdcall GetData(void *dest, long len)
という感じですかね。
C言語側では下記のように構造体を宣言しておいて、ポインタdestをキャストします。
typedef struct
{
char data1[1];
char data2[5];
char data3[10];
}TEST_DATA;
TEST_DATA* ptr = (TEST_DATA*)dest;
for(i = 0; i < len; i++)
{
// 処理
}
という感じでループしながら、データをセットしていけばOK。
今回、ちょっとややこしいのが、たんに文字列のコピーではないという点。
data1は1バイトしかありません。
一般的なstrcpyで文字列をセットしちゃうと、1文字だけのつもりが終端のNULL文字まで含めて2バイトが書き込まれてしまいます。
そこで、ちょっと面倒ですがこんな感じで書けます。
char buf[11]; // 11バイト確保している理由は、data3が10バイトなので、NULLを足して11バイト必要だから。
memset(buf, 0, 11); // bufをクリア
strcpy(buf, "0"); // bufに文字列をセット。ここで2バイト書き込まれる
memcpy(ptr->data1, buf, sizeof(ptr->data1)); // 1バイトだけコピーする
という感じで書くか、値が決まっているならいっそ値だけセットしてもいいかも。
ptr->data1[0] = '0';
さて、とりあえずデータをセットしてみます。
実際はもっと複雑なデータなんですが、サンプルとしてとりあえず固定値で入れてみます。
memset(buf, 0, 10);
strcpy(buf, "0");
memcpy(ptr->data1, buf, sizeof(ptr->data1));
memset(buf, 0, 10);
strcpy(buf, "12345");
memcpy(ptr->data2, buf, sizeof(ptr->data2));
memset(buf, 0, 10);
strcpy(buf, "文字列です");
memcpy(ptr->data3, buf, sizeof(ptr->data3));
さあ、次はこれをVBで受け取らなければいけません。
VB側のDeclare宣言
VB側でVCの関数の宣言は下記のようにします。
ポインタの部分の型を「Any」にするのがミソです。
Private Declare Sub GetData Lib "TestDLL.dll" (dest As Any, ByVal len As Long)
VCでVBから呼び出せるDLLの作り方についてはここでは書きません。
分からない人は調べてみてください。
VBからVCの関数の実行
Dim DataArray() As TEST_DATA
Dim ptr As Long
' 構造体の配列を作成
ReDim DataArray(9)
' DataArrayの先頭アドレスを取得
ptr = VarPtr(DataArray(0))
' 関数呼び出し。ポインタには、ByValをつけることに注意。
Call GetData(ByVal ptr, 10)
DataArrayの先頭アドレスを取得、という部分。
VBではメモリアドレスとかポインタをあまり意識しないため、なかなかややこしいかもしれませんが、VarPtrを使えばアドレスを取得することができます。
これをGetDataの引数に渡すのですが、このとき、ByValをつけることに注意してください。
ByValがないとちゃんと動きません。
これは、ptrにはDataArrayのメモリアドレスが格納されていますが、Byvalをつけないと、「参照渡しされる」ためです。
これ、説明がややこしいのですが、参照渡しの場合はptrの中身ではなくて、ptrが格納されたアドレスが渡される、ということなんです。
今回はptrに格納された値を渡したいため、ByValを付けます。
まあ、わたしの説明もへたくそなので分からないかもしれません。とりあえずByValをつけるんだ、という事だけ覚えていただければいいです。
ようやくVBからVCの関数を呼び出すことができました。
これで、DataArrayにはデータが入って返ってきます。
ところが!
ブレイクして中身を見てみると、"????????"という感じの変なデータが入っています。
文字コードの変換
はい、ここでやってくる文字コード問題。
C言語から返されたデータはShift JISのため、Unicodeで動いているVBからは見えないのです。
そこで、文字コードの変換を行います。
Dim tmp As String
tmp = StrConv(DataArray(0).data1, vbUnicode)
こうすることで、VBでも見える文字に変換できます。
いやー、長かった。
今回のように、10個の配列を作ったのであれば、ループしながら、各メンバごとに文字コード変換を行えばOK。
また、例えば氏名とか住所みたいな可変長の情報を受け取る場合、文字列の後ろに変なコードがくっついてくることがあります。
これは、VCでは文字列の終端に「NULL文字」をセットしますが、VBでは扱いが違うためです。
NULL文字があるかどうかはInStrを使えば判断できます。
InStr(1, DataArray(0).data3, chr(0))
として、返ってきた値が0より大きいとき、NULL文字が含まれるということです。
Left(DataArray(0).data3, NullIndex -1)
という感じで、NULL文字の手前まで取得してやればOK。
一連の流れを書くとこう。
Dim NullIndex As Integer
NullIndex = InStr(1, DataArray(0).data3, chr(0))
If NullIndex > 0 Then
DataArray(0).data3 = Left(DataArray(0).data3, NullIndex -1)
EndIf
何回も実行する必要があるなら関数化しておきましょう。
最後に
ここに至るまで苦労しました・・・。
いろんなハードルがありました。
とくに文字コード問題、アライメント問題はちょっと頭を悩ませましたね。
こんな苦労するくらいならファイル渡しでいいじゃんっていうのもアリ(僕は今回それで逃げた)だと思います。
セキュリティの問題などでどうしてもファイル化できない場合もあるでしょうし、そういうときはこんなやり方もあるよ、というわけで。
たんに引数として渡せるならこんなに苦労しないんですけどね。
構造体の配列となると急に難しくなる。
ま、いまどきVB6.0でプログラム開発してるところも・・・まあ、あるか。たぶん。
古いシステムの改修なんかだとそもそも納期も予算もないから既存システムの改造ですませることって多いでしょうしね。
古い技術でプログラム組まなきゃならないとき、同じような壁にぶつかった人のお役に立てましたら幸いです。
ないとおもうけど。