- PR -

VB.NETからvoidポインタを引数にとるDLL関数を呼び出す方法

投稿者投稿内容
masataka
会議室デビュー日: 2006/02/17
投稿数: 13
投稿日時: 2006-02-17 20:11
VB.NET2003でVC++6.0で作成されたDLLを使用する処理を作成しています。
このDLL内の関数にvoidポインタを引数にとる関数があります。
(VB6.0では、この関数の Declare宣言で「As Any」を指定していました。)

この関数に対応するため、Declare宣言で <MarshalAs(UnmanagedType.AsAny)> を使用しているのですが、関数から戻った時に関数内で変更された内容が反映されません。
(関数ないで値を変更しているのですが、関数呼び出し前と同じ値のままになっています。)
関数呼び出しの前後で Marshal.StructureToPtr()/Marshal.PtrToStructure() を使用して自前でマーシャリングする方法では正常に動作しています。
御存知の方がいらっしゃいましたら教えて下さい。

[環境]
VisualStudio.NET2003、Windows2000

[詳細]
・DLLの構造体宣言
<StructLayout(LayoutKind.Sequential, Pack:=4, CharSet:=CharSet.Auto)> _
Structure DLLSTRUCT2
Dim dummy As Integer
End Structure

・DLLの関数宣言
Declare Function fnDllProc2B Lib "DllProc" Alias "_fnDllProc2@4" (ByVal ptr As IntPtr) As Integer
Declare Function fnDllProc2C Lib "DllProc" Alias "_fnDllProc2@4" (<MarshalAs(UnmanagedType.AsAny)> ByVal ptr As Object) As Integer

・処理
' 明示的にマーシャリング処理の呼び出し(正常)
Dim dpr2b As DLLSTRUCT2
Dim p_dpr2b As IntPtr
dpr2b.dummy = 100006
p_dpr2b = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(DLLSTRUCT2)))
Marshal.StructureToPtr(dpr2b, p_dpr2b, False)
fnDllProc2B(p_dpr2b)
dpr2b = CType(Marshal.PtrToStructure(p_dpr2b, GetType(DLLSTRUCT2)), DLLSTRUCT2)
Marshal.FreeHGlobal(p_dpr2b)

' <MarshalAs(UnmanagedType.AsAny)>を使用して暗黙的にマーシャリング。
' DLLの関数には構造体が正しく渡っているが、関数内で変更された dpr2c が
' 正しく反映されていない。
' (関数呼び出し前の値のままになっている。)
Dim dpr2c As DLLSTRUCT2
dpr2c.array0.dummy = 100008
dpr2c.array1.dummy = 100009
fnDllProc2C(dpr2c)

ところで、Marshal.AllocCoTaskMem() と Marshal.AllocHGlobal() の違いは何でしょうか。上記のようにマーシャリングに使用するのはどちらの方がいいでしょうか?

以上、よろしくお願いいたします。
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-02-17 22:15
構造体のポインタを渡したいのなら、ByRefで宣言すればいいです。

Declare Function fnDllProc2C Lib "DllProc" (ByRef ptr As DLLSTRUCT2) As Integer

ただ難点はこの場合NULLポインタを渡せないってところですが。
NULLも渡しうる場合、大抵はByValなIntPtrで定義したオーバーロードも宣言します。

この問題を克服するための一つの手段は、DLLSTRUCTをClass(参照型)とし、StructLayout属性でSequentialを指定することによって、(ByValで)このオブジェクトのポインタが渡されるようになります。
この場合はNothingがNULLポインタを指すようになります。
ただ、値渡しができなくなる点など、気を付ける問題は色々出てきます。

引用:

ところで、Marshal.AllocCoTaskMem() と Marshal.AllocHGlobal() の違いは何でしょうか。上記のようにマーシャリングに使用するのはどちらの方がいいでしょうか?


CoTaskMemはCOMの通信のために用意されたものなので、多少なりと単純にメモリを確保するHGlobalよりはリッチな実装になっていると考えればいいかと。
P/Invokeにおいて、こちらでAlloc/Free両方する分には、あまり意識する必要もないと思いますが。
masataka
会議室デビュー日: 2006/02/17
投稿数: 13
投稿日時: 2006-02-17 22:54
早速の回答ありがとうございます。

引用:

Hongliangさんの書き込み (2006-02-17 22:15) より:
構造体のポインタを渡したいのなら、ByRefで宣言すればいいです。

Declare Function fnDllProc2C Lib "DllProc" (ByRef ptr As DLLSTRUCT2) As Integer



関数の引数は void* なので、いろいろな構造体を渡す必要があります。
(数+種類の構造体を渡します。)
質問では、単純化するために省略してしまったのですが、本来は構造体の種類を渡す引数があります。
尚、質問には記述していなかったのですが、ご指摘の方法でなら問題になっている「関数から戻った時に関数内で変更された内容が反映されない」現象が起きない事を確認しています。
構造体毎に関数をオーバーライドさせればできそうかとも思ったのですが、数+種類もあるとメンテナンスの面でも手間の面でもあまりやりたくありませんでしたので今回の質問をしました。分かり難くててすみません。

引用:

CoTaskMemはCOMの通信のために用意されたものなので、多少なりと単純にメモリを確保するHGlobalよりはリッチな実装になっていると考えればいいかと。
P/Invokeにおいて、こちらでAlloc/Free両方する分には、あまり意識する必要もないと思いますが。


HGlobalの方を使うようにします。
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-02-17 23:57
.NET の場合、構造体を扱う場合はボクシング/アンボクシングが発生したときに値のコピーが行われるので、そういう用途でVB.NETやC#を使うのはお勧めしないのですが。

Objectを受け取りDllImport先で操作し結果を返す、と言う場合、
コード:
<MarshalAs(UnmanagedType.AsAny), In, Out> ByVal ptr As Object


と言う風に In 属性と Out 属性も必要になります。
// 参照型の場合自動的にin/outのはずなんですが、AsAnyの場合なぜかinだけになりますね。

もう一つの問題は初めに言ったボクシングによる値コピーの問題です。
引数がObjectですから、渡すときにボクシングが発生し、与えた構造体とは別のインスタンスがアンマネージドに渡されることになります。
これを回避するには、利用者が明示的にObjectに事前にボクシングしておき、関数呼び出しが終わってからアンボクシングを行う必要があります。
コード:
Dim a As StructA
a.Hoge = 234
Dim o As Object = a
DllFunc(o)
a = DirectCast(a, StructA)


いちいち利用者にこれを要求するのは、オーバーロードを定義するよりもよほどメンテナンス性その他で問題があると思います。
もちろんし忘れたら呼び出し先での変更を受け取れず、やっかいなバグの元になるでしょう。

使用する構造体を全てClassで定義する(StructLayout属性を付加して)、と言う手もあります。このときはボクシング/アンボクシングが無いためごく自然に書けます。
誤ってStructureが混じってしまった場合はやっかいなバグになりそうですが。

次善の策としては、ラッパメソッドを用意するというのもありですね。
コード:
Shared Function DllProc(ByVal obj As Object, ByRef result As Integer) As Object
    result = DllFunc(obj)
    Return obj
End Function


これなら利用者も返値を代入し直す必要がある点に気付きやすいでしょう。

// 個人的にはそんなのはManaged C++にさせるようにしますけど。
masataka
会議室デビュー日: 2006/02/17
投稿数: 13
投稿日時: 2006-02-18 10:08
利用者側がややこしくなるのはた避けたいので、最初に御提案頂いた方法で全構造体分のオーバーロードを定義する事にした方がよさそうですね。

引用:

Hongliangさんの書き込み (2006-02-17 23:57) より:
// 個人的にはそんなのはManaged C++にさせるようにしますけど。


とはどういうことでしょうか?
(Managed C++でラッパーを作成するのでしょうか?)

すみませんが教えて下さい。
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-02-18 12:43
引用:

引用:

// 個人的にはそんなのはManaged C++にさせるようにしますけど。


とはどういうことでしょうか?
(Managed C++でラッパーを作成するのでしょうか?)


そういうことです。
C#/VBからは構造体を意識しないで扱えるような範囲で。
masataka
会議室デビュー日: 2006/02/17
投稿数: 13
投稿日時: 2006-02-18 13:58
もう少し教えて下さい。
Managed C++でラッパーを作成するというのは、VBのラッパメソッドと同じロジックをManaged C++で作成するのでしょうか?

引用:

Hongliangさんの書き込み (2006-02-17 23:57) より:
次善の策としては、ラッパメソッドを用意するというのもありですね。
コード:
Shared Function DllProc(ByVal obj As Object, ByRef result As Integer) As Object
    result = DllFunc(obj)
    Return obj
End Function


これなら利用者も返値を代入し直す必要がある点に気付きやすいでしょう。



そうだとしたら、VBで作成しないで、Managed C++で作成するメリットは何でしょうか?又、この場合にVBからManaged C++で作成したラッパ(DLL?)を使用するにはどうするのでしょうか?(結局はオーバーロードを構造体毎に定義する事になるような気がするのですが)

後、勉強のため、昨日教えて頂いたIn 属性と Out 属性を指定する方法
引用:

Hongliangさんの書き込み (2006-02-17 23:57) より:
Objectを受け取りDllImport先で操作し結果を返す、と言う場合、
コード:
<MarshalAs(UnmanagedType.AsAny), In, Out> ByVal ptr As Object


と言う風に In 属性と Out 属性も必要になります。
// 参照型の場合自動的にin/outのはずなんですが、AsAnyの場合なぜかinだけになりますね。


を試してみたのですが、In 指定のところでコンパイルエラー「キーワードは識別子として有効ではありません。」になってしまいます。なぜでしょうか?
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-02-18 14:40
引用:

masatakaさんの書き込み (2006-02-18 13:58) より:
もう少し教えて下さい。
Managed C++でラッパーを作成するというのは、VBのラッパメソッドと同じロジックをManaged C++で作成するのでしょうか?
引用:

Hongliangさんの書き込み (2006-02-17 23:57) より:
次善の策としては、ラッパメソッドを用意するというのもありですね。
コード:
Shared Function DllProc(ByVal obj As Object, ByRef result As Integer) As Object
    result = DllFunc(obj)
    Return obj
End Function


これなら利用者も返値を代入し直す必要がある点に気付きやすいでしょう。



そうだとしたら、VBで作成しないで、Managed C++で作成するメリットは何でしょうか?又、この場合にVBからManaged C++で作成したラッパ(DLL?)を使用するにはどうするのでしょうか?(結局はオーバーロードを構造体毎に定義する事になるような気がするのですが)


関数呼び出し部分だけをラップするのは全く意味がないでしょう。
ですから、
引用:

C#/VBからは構造体を意識しないで扱えるような範囲で。


と書いたように、各種構造体をC#/VBから隠蔽します。
その関数呼び出しに限定されず、もう少し大きい部品単位でラップするわけです。
ことによっちゃラップ以上の範囲になるかもしれませんが。


引用:

後、勉強のため、昨日教えて頂いたIn 属性と Out 属性を指定する方法
引用:

Hongliangさんの書き込み (2006-02-17 23:57) より:
Objectを受け取りDllImport先で操作し結果を返す、と言う場合、
コード:
<MarshalAs(UnmanagedType.AsAny), In, Out> ByVal ptr As Object


と言う風に In 属性と Out 属性も必要になります。
// 参照型の場合自動的にin/outのはずなんですが、AsAnyの場合なぜかinだけになりますね。


を試してみたのですが、In 指定のところでコンパイルエラー「キーワードは識別子として有効ではありません」になってしまいます。なぜでしょうか?


あ、これは失礼。VB では In は予約語でしたね。
予約語を予約語の用途以外で使う場合、[In] と言う風に書いてください。InAttribute でもいいですけど。

スキルアップ/キャリアアップ(JOB@IT)