C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[10]: VB.netからVC++6.0のへの構造体配列 [1]


(過去ログ 127 を表示中)

[トピック内 28 記事 (21 - 28 表示)]  << 0 | 1 >>

■75371 / inTopicNo.21)  Re[6]: VB.netからVC++6.0のへの構造体配列参照渡し
  
□投稿者/ 魔界の仮面弁士 (248回)-(2015/03/19(Thu) 11:50:26)
No75321 (魔界の仮面弁士) に追記
> MarshalAs 指定は必要になるだろうということで、SafeArray 指定で
>  ・SafeArray
>  ・SafeArray, SafeArraySubType:=VT_USERDEFINED
>  ・SafeArray, SafeArraySubType:=VT_RECORD, SafeArrayUserDefinedSubType:=GetType(Structure1)
> などを試してみましたが、HRESULT が E_INVALIDARG(0x80070057) を返すばかり。

焦って試行錯誤してるけど、理論武装無しで闇雲に試しても駄目だよなぁ…。


そもそも構造体のセーフ配列について知識が全然無いので、
VB6 当時のマーシャリングがどうなっているかを調査してみるかな。


まずは VB6 の VarType 関数を使って、バリアントの内部型を確認。

ユーザー定義型に対して VarType を使うためには、その型を
パブリックオブジェクトモジュールで公開しておく必要があるので、
今回は ActiveX EXE プロジェクトにてテスト。

'=== クラス モジュール(Instacing を「1:Private」以外に設定) ===
Option Explicit
' No75314 のユーザー定義型
Public Type Type1
 Long1 As Long
 Long2 As Long
 Long3 As Long
End Type

'=== 標準モジュール ===
Option Explicit
Public oType(0 To 9) As Type1
Private Sub Main()
 Debug.Print Hex(VarType(oType))
 Debug.Print TypeName(oType)
End Sub


結果は、VarType = &H2024、TypeName = "Type1()" 。

&H2024 は、「vbArray Or vbUserDefinedType」のことなので、
VC++ 側の表現で言えば、「VT_ARRAY | VT_RECORD」相当だと分かる。



念のため、SafeArrayGetVartype API でも調べてみる。

 Declare Function SafeArrayGetVartypeVB Lib "oleaut32" Alias "SafeArrayGetVartype" _
  (oType() As Any, pvt As VBA.VbVarType) As Long

 Declare Function SafeArrayGetVartypeOLE Lib "oleaut32" Alias "SafeArrayGetVartype" _
  (oType() As Any, pvt As olelib.VARENUM) As olelib.HRESULTS

やはり、vbUserDefinedType = VT_RECORD = &H0024 が返されてきた。


ということは、.NET 側のマーシャリングでは、
 <MarshalAs(UnmanagedType.SafeArray, SafeArraySubType:=VarEnum.VT_RECORD)>
を指定すれば良さそうだ、ということまで理解。

でもまだうまく呼べない。次の手を考えないと。
引用返信 編集キー/
■75372 / inTopicNo.22)  Re[7]: VB.netからVC++6.0のへの構造体配列参照渡し
□投稿者/ 魔界の仮面弁士 (249回)-(2015/03/19(Thu) 11:53:45)
No75371 (魔界の仮面弁士) に追記
> ということは、.NET 側のマーシャリングでは、
>  <MarshalAs(UnmanagedType.SafeArray, SafeArraySubType:=VarEnum.VT_RECORD)>
> を指定すれば良さそうだ、ということまで理解。
> でもまだうまく呼べない。次の手を考えないと。

チラ裏投稿の続き。


この段階で、VB.NET 側の実装はこのように書いている。

 Declare Function Test1 Lib "TestFunc.dll" _
  (<MarshalAs(UnmanagedType.SafeArray, SafeArraySubType:=VarEnum.VT_RECORD)> _
  ByRef Type1() As Structure1) As Integer

 Public Structure Structure1
  Dim Long1 As Integer
  Dim Long2 As Integer
  Dim Long3 As Integer
 End Structure

結果は、『パラメーターが間違っています。』な E_INVALIDARG(0x80070057) の例外。


…あぁ、そういえば構造体を COM 公開していなかった。

<ComVisible(True), Guid("8D60602D-452A-48A1-ACEF-AF148A6E41B8")> _
Public Structure Structure1
 Dim Long1 As Integer
 Dim Long2 As Integer
 Dim Long3 As Integer
End Structure


まだ駄目か。 No75336 に書いたとおり、RegAsm もしておこう。
「RegAsm ConsoleApplication1.exe」…っと。

--------------------
  RegAsm : warning RA0000 : 型は何も登録されませんでした。
--------------------

あれ?なんか忘れてるっけ?

わかった、構造体を「Module Module1」の中で宣言しているからだ。
「Public Module Module1」にしないと意味が無いわな。


Module のスコープを Public に直して再実行。

 Dim iRet As Integer = -1
 For n = 0 To 9
  oType(n).Long1 = n * 100 + 1
  oType(n).Long2 = n * 100 + 2
  oType(n).Long3 = n * 100 + 3
 Next
 iRet = Test1(oType)

おぉ、エラーなく呼べた!!
配列が 10 件→1 件になる現象も起きていない!


…いや、ちょっと待てよ。そういえば、Public Module 化した後、RegAsm 呼んでなかったな。
もしかして COM 公開って意味が無いのだろうか。

確認のため、COM 公開を止めてみよう。
 <ComVisible(False)> _
 Public Structure Structure1
  Dim Long1 As Integer
  Dim Long2 As Integer
  Dim Long3 As Integer
 End Structure

……うん。やっぱりコレだと E_INVALIDARG(0x80070057) の例外が発生する。
ComVisible は必要っぽいな。RegAsm や TlbExp はしなくても大丈夫そう。


呼び出しまではできるようになったので、次は、DLL からの情報を受け取れるかどうかの確認だな。
引用返信 編集キー/
■75377 / inTopicNo.23)  Re[8]: VB.netからVC++6.0のへの構造体配列参照渡し
□投稿者/ 魔界の仮面弁士 (252回)-(2015/03/19(Thu) 12:09:19)
No75372 (魔界の仮面弁士) に追記
> iRet = Test1(oType)
> おぉ、エラーなく呼べた!!
> 呼び出しまではできるようになったので、次は、DLL からの情報を受け取れるかどうかの確認だな。


今のところ、iRet の値は &H0024。これは DLL 側の実装を
 LPSAFEARRAY psa = *Type1;
 VARTYPE vt;
 SafeArrayGetVartype(psa, &vt);
 return vt;
にしているからなので、内容的には正しそう。(&H0024 は 定数 VT_RECORD と同じ値)


でも、DLL 側で値を編集するのってどうやるんだろう。
C++ は全然知らない上に、SAFEARRAY の API にも詳しくないんだよなぁ。

とりあえず、見様見真似で書いてみよう。


 LPSAFEARRAY psa = *Type1;
 
 long lb, ub;
 SafeArrayLock(psa);
 SafeArrayGetLBound(psa, 1, &lb); // lb = LBound(Type1, 1)
 SafeArrayGetUBound(psa, 1, &ub); // ub = UBound(Type1, 2)
 SafeArrayUnlock(psa);
 
 Structure1 *p = NULL;
 SafeArrayAccessData(psa, (void**)&p);
 for(long idx = lb; idx <= ub; idx++)
 {
  p[idx].Long1 += 70;
  p[idx].Long2 += 80;
  p[idx].Long3 += 90;
 }
 SafeArrayUnaccessData(psa);
 return ub - lb + 1; // 要素数



C++ は苦手というか勉強したことが無い(C が少し読める程度)ので、
これで正しいのかどうかは全然自信が無いけど、一応コンパイルは通ったようだ。

エラーチェックの処理とか入れてないのが不安だけど、
とりあえず VB6 から呼んでみよう。

 For n = 0 To 9
  oType(n).Long1 = n * 100 + 1
  oType(n).Long2 = n * 100 + 2
  oType(n).Long3 = n * 100 + 3
 Next
 iRet = Test1(oType)

よしよし、oType(2) が { 201, 202, 203 } → { 271, 282, 293 } に書き換わっているな。

次は、VB.NET で呼んでみると…こっちも大丈夫そうだ!

ってことで完成形のお披露目ー★

Imports System.Runtime.InteropServices
Public Module Module1
 <ComVisible(True), Guid("8D60602D-452A-48A1-ACEF-AF148A6E41B8")> _
 Public Structure Structure1
  Dim Long1 As Integer
  Dim Long2 As Integer
  Dim Long3 As Integer
 End Structure

 Declare Function Test1 Lib "TestFunc.dll" _
  (<MarshalAs(UnmanagedType.SafeArray, SafeArraySubType:=VarEnum.VT_RECORD)> ByRef oStructure() As Structure1) As Integer

 Public oType(9) As Structure1
 Sub Main()
  Dim iRet As Integer = -1
  For n = 0 To 9
   oType(n).Long1 = n * 100 + 1
   oType(n).Long2 = n * 100 + 2
   oType(n).Long3 = n * 100 + 3
  Next
  iRet = Test1(oType)
  
 End Sub

End Module
引用返信 編集キー/
■75379 / inTopicNo.24)  Re[10]: VB.netからVC++6.0のへの構造体配列
□投稿者/ とら (11回)-(2015/03/19(Thu) 12:46:31)
私の説明不足により皆様にご心配及びご迷惑をお掛けした事をお詫び申し上げます。


No75358 (774RR さん) に返信

> 俺は、今までの発言の中に SAFEARRAY の実体を取得しているのがどこかは書かれていないと読んでいる。
> 現在既にある VB6 プログラムの動作上、通信バッファに相当する SAFEARRAY の実体を
> 1. VB6 EXE 側が確保して VC DLL に渡し、用済み後 VB6 EXE が処分している
> 2. VC DLL が取得・処分処理をカプセル化していて (malloc/free 相当処理を VC DLL 側で行っていて)
> VB6 EXE 側はメモリの取得・処分の詳細を知らなくてすむようになっている
> どちらだろう。

VB側のEXEにてPublicとして配列を宣言しており、VBのEXE終了時に開放しております。
なのでこのケースですと1という事になります。

> 1. なら通信バッファは VB.NET 側で取得した manage memory を使うことになり、
>  単純に VC DLL に向けて marshal すればいいので簡単だろう。

マーシャリングに関しては魔界の仮面弁士さんにご教授頂いた事も含めて試してみたのですが、
私の力不足により良い結果は得られませんでした。



No75359 (魔界の仮面弁士 さん) に返信
> DLL の改修を行うのはとらさんでしょうか?
> それとも専任者がいるのでしょうか?
VC++の改修も私の方で行う予定です(本来はそもそもVC++側の改修予定はなかったのですが)

> セーフ配列を C 形式配列に置き換える作業を、
> VC++の知識無しで行うのは無理があるかと…。

やはり難しいでしょうか。



> C 配列なので、配列先頭要素のポインタを渡すというだけで。
> 1 つの構造体として渡す、という表現だと語弊があるかな…。

これに関しましてですが「セーフ配列を使わず、配列先頭ポインタと配列長を渡す実装に変更して回避するケース、」
という事を行おうとして先のように実装しようとしたのですが、根本的に勘違いしていたようですか?

色々と調べていたところ、配列の先頭だけを渡して云々といったのがあったので、あのような形になるのかと思いました。


> セーフ配列やマネージ配列の場合は、配列自体が要素数情報を保持していますが、
> C 配列はただのポインタなので、サイズ情報がありません。
> ただし、常に要素数が固定という仕様なのであれば、
> 配列要素数をわざわざ指定しなくても良いでしょう。



>>VC++側は「構造体」と「構造体配列の要素数」を受け取ってから、
> それだと、配列を受け取れませんよね。
> VC++側は構造体ではなく、構造体のポインタを受け取るようにします。
> VB2008 側は「<[In](), Out()> ByVal x() As 構造体, ByVal length As Integer」という感じですね。

> 構造体の値を書き換えるぐらいなら、さほど問題はありません。
> 呼び出し時に OutAttribute をつけておく程度で良いと思います。

> 構造体の値を書き換えるぐらいなら、さほど問題はありません。
> 呼び出し時に OutAttribute をつけておく程度で良いと思います。

> 問題は、配列サイズの変更がありえる場合です。

今回は配列の要素数が固定なので、要素数は渡さず、書き換えも発生するので以下の形にすれば良いのでしょうか。
Declare Function Test1 Lib "TestFunc.DLL" (<InAttribute(), OutAttribute()> ByRef oStructure() As Structure1) As Integer

配列の宣言は
Public udtShareMemory(9) As Structure1


呼ぶ際は
iRet = Test1(aryStructure1)

なお、構造体の宣言は以下のようにしています。
Public Structure Structure1
<MarshalAs(UnmanagedType.I4)> Public aaa As Integer
<MarshalAs(UnmanagedType.I4)> Public bbb As Integer
<MarshalAs(UnmanagedType.I4)> Public ccc As Integer
End Structure


VC++側は下記のようにしてみました。

typedef struct {
int aaa;
int bbb;
int ccc;
} Struct1;


TestFunc_API int __stdcall Test1(Struct1 *aryStruct[])

nRet = FuncTestSub(aryStruct);


TestSub_API int __stdcall FuncTestSub (void *Type1);


しかしVC++側のソースが追えない部分にて落ちている様子

Expression: (L"Buffer is too Smaall" && 0)


うむむ……、VC++側の受け取り方を根本的に勘違いしているのでしょうか。



No75362 (Azulean さん) に返信

ご指摘はごもっともです。
元々の予定ではVC++側に手を入れる予定はなく、VB側だけで何とかしようとしていたのですが、私の力不足により実装が難しく
VC++側に手を入れる方向で再考致しましたが、その時点でVC++に精通した方に依頼すべきではありますね。

しかし、まだ実際に可能かどうかも解っておらず、最低限の技術検証を行ってから実装したいという思いもあるので、
予定の変更やVC++の技術者への依頼もまだ決めかねている状態です。


>>TestSub_API int __stdcall FuncTestSub (void *Type1);
> SafeArrayAccessData で得た構造体配列の先頭ポインタを渡しているだけであれば、SafeArray の要素数が変わることはないと思っています。
> 予想できる振る舞いとしては、内容の書き換えぐらいまでですかね。
> "TestSub_API int __stdcall FuncTestSub (void *Type1)" なら、渡した先で構造体のポインタにキャストしていると予想される。
> 呼び出し元で事前にキャストしていないことから、先頭ポインタを渡すことを前提としているはず。
> であれば、要素数を渡さないこの関数のシグネチャからは要素数は一定以上、事実上固定のサイズと想定していそう。

要素数は固定で内容の書き換えは行っています。

引用返信 編集キー/
■75392 / inTopicNo.25)  Re[9]: VB.netからVC++6.0のへの構造体配列参照渡し
□投稿者/ とら (12回)-(2015/03/19(Thu) 19:01:17)
No75377 (魔界の仮面弁士 さん) に返信

一括表示で2ページ目になっていたため気付くのが遅れました、すみません!

こちらでも同じようにやってみたのですが、タイプライブラリの生成に失敗しましたといったエラーが発生してしまいました。


>  LPSAFEARRAY psa = *Type1;

今回成功したパターンは、VC++で受け取った後にLPSAFEARRAY型に代入しているという事は

    TEST_API int __stdcall Test1(LPSAFEARRAY *Type1)

という元々の状態ではなく

    TEST_API int __stdcall Test1(T_Struct *type1)

の状態で受け取っているという事でしょうか?



> C++ は苦手というか勉強したことが無い(C が少し読める程度)ので、
> これで正しいのかどうかは全然自信が無いけど、一応コンパイルは通ったようだ。
わざわざあれこれ挑戦して頂いたようで大変恐縮です、有難う御座います。



>(1) AssemblyInfo.vb にて、
>   <Assembly: ComVisible(True)>
>   <Assembly: Guid("……")>
>  を指定しておきます。プロジェクトのプロパティの[アプリケーション]-[アセンブリ情報]でも指定可。

>  <ComVisible(True), Guid("8D60602D-452A-48A1-ACEF-AF148A6E41B8")> _

GUIDの部分が少々理解が追い付きませんでしたが、ここに設定する値は

>Guid.NewGuid().ToString("D")等で生成した値を指定

とありましたが、この値はこの関数を実行するたびに変化するのでしょうか


また、

>(2) 構造体側にも、また別の GUID を割り当てておきます。

とありましたが、AssemblyInfo.vbに設定するGUIDと構造体に設定するGUIDは違うものにしておくのですか?



そちらでは結果が出ている事が確認できているところすみませんが、何卒よろしくお願い致します。

引用返信 編集キー/
■75393 / inTopicNo.26)  Re[10]: VB.netからVC++6.0のへの構造体配列参照渡し
□投稿者/ とら (13回)-(2015/03/19(Thu) 19:42:06)
No75392 (とら さん) に追記

すみません、こちらで見直したところ無事成功致しました。

>> LPSAFEARRAY psa = *Type1;
> 今回成功したパターンは、VC++で受け取った後にLPSAFEARRAY型に代入しているという事は
> TEST_API int __stdcall Test1(LPSAFEARRAY *Type1)
> という元々の状態ではなく
> TEST_API int __stdcall Test1(T_Struct *type1)
> の状態で受け取っているという事でしょうか?

こちらは元々の状態、つまりVC++側には手を入れないで良いみたいですね。


> >(1) AssemblyInfo.vb にて、
> >   <Assembly: ComVisible(True)>
> >   <Assembly: Guid("……")>
> >  を指定しておきます。プロジェクトのプロパティの[アプリケーション]-[アセンブリ情報]でも指定可。
>
>> <ComVisible(True), Guid("8D60602D-452A-48A1-ACEF-AF148A6E41B8")> _
>
> GUIDの部分が少々理解が追い付きませんでしたが、ここに設定する値は
>
> >Guid.NewGuid().ToString("D")等で生成した値を指定
>
> とありましたが、この値はこの関数を実行するたびに変化するのでしょうか
>
>
> また、
>
> >(2) 構造体側にも、また別の GUID を割り当てておきます。
>
> とありましたが、AssemblyInfo.vbに設定するGUIDと構造体に設定するGUIDは違うものにしておくのですか?

これは別のGUIDを指定しておけば良いのですね。
GUIDだからという先入観で同じものにするのかと思い込んでおりました、申し訳御座いません。


このたびは大変お世話になりました、そして自分の力不足を痛感致しました。

特に、魔界の仮面弁士さんには尽力を尽くして頂いたようで感謝の極みです。

誠に有難う御座いました。

解決済み
引用返信 編集キー/
■75394 / inTopicNo.27)  Re[10]: VB.netからVC++6.0のへの構造体配列参照渡し
□投稿者/ 魔界の仮面弁士 (254回)-(2015/03/19(Thu) 20:05:29)
No75392 (とら さん) に返信
> 一括表示で2ページ目になっていたため気付くのが遅れました、すみません!
> こちらでも同じようにやってみたのですが、タイプライブラリの生成に失敗しましたといったエラーが発生してしまいました。

型公開は必要ですが、タイプライブラリを明示的に作る必要は無さそうです。

もし、ComVisible を True にしても駄目なようならば、VS を管理者起動してみてください。
レジストリ登録のためには管理者権限が必要なので。


>> LPSAFEARRAY psa = *Type1;
> 今回成功したパターンは、VC++で受け取った後にLPSAFEARRAY型に代入しているという事は
いえ、説明文では C++ 側の関数定義を書いていなかったので、
それが分かるにしただけです。引数自体は LPSAFEARRAY *Type1 のままですね。


>> <ComVisible(True), Guid("8D60602D-452A-48A1-ACEF-AF148A6E41B8")> _
> GUIDの部分が少々理解が追い付きませんでしたが、
GUID は、『ネットワーク上で必ず一意であることが保障される128ビットの数値』です。

ここでは、「クラス」「インターフェイス」「タイプライブラリ」などといった
それぞれの COM オブジェクトに付けられた 128bit の識別番号だと思ってください。


たとえば、ActiveX Data Object のタイプライブリには 00000205-0000-0010-8000-00AA006D2EA4 という
GUID 値(LIBID) が割り当てられていますし、Excel の Application オブジェクトであれば、
00024500-0000-0000-C000-000000000046 という GUID値(CLSID)が割り当てられているといった具合に。


今回は、自作したデータ型なので、既存のGUID値ではなく、新規に用意した GUID 値を割り当てることになります。
もし、構造体が複数あるのなら、それぞれに違う GUID を用意してください。Assmbly 側も然り。



>> Guid.NewGuid().ToString("D")等で生成した値を指定
> とありましたが、この値はこの関数を実行するたびに変化するのでしょうか
はい。生成するたびに変わります。そして、同じ値は二度と生成されません。
「重複しないランダムな値」を生成するものだと思ってください。


> >(2) 構造体側にも、また別の GUID を割り当てておきます。
> とありましたが、AssemblyInfo.vbに設定するGUIDと構造体に設定するGUIDは違うものにしておくのですか?
重複する GUID 値を指定してはいけません。
すべて別の値を使用する必要があります。

複数の箇所で同じ GUID 値を使うことがあるとすれば、たとえば
A.DLL と B.DLL と C.EXE が「同じ構造体」を再定義する場合などです。

もっとも、そういう場合は、共有用のタイプライブラリを用意して、
それを各プロジェクトで参照設定した方が分かりやすいですけれどね。
引用返信 編集キー/
■75398 / inTopicNo.28)  VB.netからVC++6.0のへの構造体配列参照渡し
□投稿者/ とら (14回)-(2015/03/20(Fri) 13:33:23)
2015/03/20(Fri) 13:35:06 編集(投稿者)
No75394 (魔界の仮面弁士 さん) に返信

> 型公開は必要ですが、タイプライブラリを明示的に作る必要は無さそうです。
> 
> もし、ComVisible を True にしても駄目なようならば、VS を管理者起動してみてください。
> レジストリ登録のためには管理者権限が必要なので。

前回ご提示頂いた手法にて正常に構造体配列の受け渡しが可能な事を確認致しました。


自身の認識確認、及び今後参考にされる方が解り易いように手順をまとめ直させて頂きますと、

@ GUIDを2つ生成してメモする
  ・VS付属の「GUIDGEN.EXE」 や デバッグにて 「Guid.NewGuid().ToString("D")」等を実行する

A AssemblyInfo.vb に 下記2つを追記する。
  ・<Assembly: ComVisible(True)>
  ・<Assembly: Guid("【@で取得したGUIDのうち1つ】")>

B 構造体を宣言しているモジュールを Public にする

C 構造体を宣言しているモジュールにて下記をインポートする
  ・System.Runtime.InteropServices

D 構造体の宣言部分上部に下記を追記しCOM公開する
  ・<ComVisible(True), Guid("【@で取得したGUIDのうち1つ(Aで使用していないもの)】")> _

  例:
   <ComVisible(True), Guid("8D60602D-452A-48A1-ACEF-AF148A6E41B8")> _  
   Public Structure Structure1
    Dim Long1 As Integer
    Dim Long2 As Integer
    Dim Long3 As Integer
   End Structure

E Declareの引数指定部分に下記マーシャリングを追記
  ・<MarshalAs(UnmanagedType.SafeArray, SafeArraySubType:=VarEnum.VT_RECORD)>

  例:
    Declare Function Test1 Lib "TestFunc.dll" _
    (<MarshalAs(UnmanagedType.SafeArray, SafeArraySubType:=VarEnum.VT_RECORD)> ByRef oStructure() As Structure1) As Integer

F 後は従来通りにVC++DLLを呼び出す



> GUID は、『ネットワーク上で必ず一意であることが保障される128ビットの数値』です。

他で生成されたGUIDと一致する確率が1/2^61とかなんとか、天文学的数字ってやつですね。
Globally (世界的に) Unique (固有の) Identifier (識別子)ってまんまの意味の略語だったのか。


> 今回は、自作したデータ型なので、既存のGUID値ではなく、新規に用意した GUID 値を割り当てることになります。
> もし、構造体が複数あるのなら、それぞれに違う GUID を用意してください。Assmbly 側も然り。

つまり1つの構造体配列をやり取りするのに2つのGUIDを生成する必要があるが、

> 複数の箇所で同じ GUID 値を使うことがあるとすれば、たとえば
> A.DLL と B.DLL と C.EXE が「同じ構造体」を再定義する場合などです。

同じ構造体であれば別の個所で使用する分には新たに別のGUIDを生成する必要はないが、

> もっとも、そういう場合は、共有用のタイプライブラリを用意して、
> それを各プロジェクトで参照設定した方が分かりやすいですけれどね。

確かにごもっともです。



以上、大変お世話になりました。

解決済み
引用返信 編集キー/

<前の20件
トピック内ページ移動 / << 0 | 1 >>

このトピックに書きこむ

過去ログには書き込み不可

管理者用

- Child Tree -