DOBON.NET DOBON.NETプログラミング掲示板過去ログ

C#とC++で作ったDLLのメモリについて

環境/言語:[WindowsXP, C#, .NET Framework 3.5]
分類:[.NET]

皆さんこんにちは、前回は非常に有効なアドバイスを
いただきまして、誠にありがとうございました。
今回は、C#でのメモリについてお聞きしたいことがあり、投稿させていただきました。

*やりたいこと*
C++で作ったDLLに、C#アプリケーションからポインタ配列(IntPtr[])を渡して
DLL内に作られたメモリ空間をC#側で扱いたいのです。

*やってみたこと*
DLLImportしたい関数は、Hoge( LPVOID* lpBuff, int size );
という単純にlpBuff[]をDLLに渡し、DLL内でメモリを確保して
それぞれのメモリをi個分lpBuffに格納するものです。
lpBuff[i]個
lpBuff[0] = 512kbytesサイズのメモリ
lpBuff[1] = 512kbytesサイズのメモリ
...
lpBuff[i] = 512kbytesサイズのメモリ
というポインタのポインタが帰ってくることを想定しています。
CやC++なら、LPVOID* lpMemをそのまま渡して扱うことができますが、
C#での宣言ではこんな形にしました。
[DLLImport("hoge.dll")]
Hoge( out IntPtr[] buff, int size );

private IntPtr[] buff;
buff = new IntPtr[3];
int size;
Hoge( out buff, size );

こんな形で使っているのですが、
さて、取りえたIntPtrのメモリポインタを、
どうやってC#のbyte配列にコピーしたものか?と悩んでおります。

そこで過去ログや他サイトを参考にいろいろ考えてみたのですが、
いまいちよく分かりません...
LPVOID*を要求する関数でこけてしまっている感じなのです。
上記の関数を呼ぶとアプリケーションごと落ちてしまうので、
おそらくDLLへちゃんとC#の変数がわたっていないように思えます。
C++からこの関数を使うと問題なく動作することが分かっているのですが、
C#ではどのように記述するかご教示願えれば幸いです。
> *やりたいこと*
> C++で作ったDLLに、C#アプリケーションからポインタ配列(IntPtr[])を渡して
> DLL内に作られたメモリ空間をC#側で扱いたいのです。

  メモリの管理方式が違いますが、やってやれないことはない
  のですが、結果として問題が出るかも・・・

  C++ CLI(CLRの表記)で、C++側をマネージドに作れば、C#側
  からは引数を何も気にせず渡せるので、後はC++ CLI側でう
  まく動作するように書けばOK!〜

  C++も.NETクラスも両方使えるからC++ CLI便利ヨ!
  それにC#側は普通に参照設定するだけで使えますのでDllImport
  しなくてよいし・・・

以上。参考まで
配列は参照型です。out は参照渡しです。参照型、参照渡しはともにポインタとしてマーシャリングされるので、out IntPtr[] は IntPtr のポインタのポインタを渡すことになります。
IntPtr 自体もポインタ(void*)ってことになるので、void*** が渡されることになります。C++ 側の LPVOID* とは型があいません。
void** にするには IntPtr[] か out IntPtr にすることになりますが、受け取りたいのは配列なので前者の宣言が必要です。
また、できるだけパフォーマンスを稼ぐため、マーシャリングする際に blittable な型の配列は呼び出すときにアンマネージなメモリに値をコピーするだけで、呼び出し先から返ってくるときにはアンマネージなメモリからコピーしません。つまり、呼び出し先の変更を受け取りません。
この動作を変更するのが In 属性と Out 属性です。呼び出し時のコピーが不要で呼び出し先からのコピーが必要なら Out 属性のみを指定します。

> さて、取りえたIntPtrのメモリポインタを、
> どうやってC#のbyte配列にコピーしたものか?と悩んでおります。
Marshal.Copy メソッドを使用します。
オショウ さん
Hongliang さん

ものすごい早いご返信ありがとうございます。
試行錯誤する暇さえ与えてくれないとは!やりますね。
冗談はさておいて、非常に有用なアドバイスありがとうございます。

> オショウ さん
CLIでマネージド?というか、ずっとベタCで独学でやっていたものですので、
それってナーニ?おいしいの?状態です。こちらの書き方についてついて
調べてから質問させていただきますね。これからC#というか、
.NETFramework関係のアプリを研究のために作って行く予定というか、
私個人のこだわりの範疇ですが、後輩たちに残せるものを変更しやすいものを
作って行きたいと考えているからです。

> IntPtr 自体もポインタ(void*)ってことになるので、void*** が渡されること
> になります。C++ 側の LPVOID* とは型があいません。

あいたー!IntPtrはそんなそうなんですねっ!ただのintだと思っていた
私がバカでした。というか、リファレンスをちゃんと呼んでいないことを
暴露している気がします。はずかしい。

> void** にするには IntPtr[] か out IntPtr にすることになりますが、受け取り
> たいのは配列なので前者の宣言が必要です。
> この動作を変更するのが In 属性と Out 属性です。呼び出し時のコピーが不要で
> 呼び出し先からのコピーが必要なら Out 属性のみを指定します。

ここで、疑問なのが、IntPtrはout指定がなくても呼び出し側で書き換えた値、
(この場合メモリアドレス値)は呼び出し元でも変更されるのですか?
Cで言う&渡しを意識してoutを使ったのですが、そうなると、
ref/outの意味が良く分からなくなってしまいます...orz
ちなみに、関数宣言を
Hoge( out IntPtr[] buff, int size );から
Hoge( IntPtr[] buff, int size );に書き換えた場合、
きっちり関数が動作しました。...しましたが、C#側(呼び出し側)で
buffを操作しようとしたらエラー...コレってやはりアドレスが
ちゃんと帰ってきていないようです。

もし何かお気づきのことがあればご教示していただければ幸いです。
例えば C++ で引数に int* を取るとき、二つの意味がありますよね。一つは配列を受け取る場合。もう一つは呼び出し元に int を返す場合。
この関数の仮引数を C# で定義する場合、前者が int[] であり後者が out int です。
後者を int[] で定義しても構いません。どのみち int* にマーシャリングされますから。ただ、わざわざ配列を作成する意味は無いでしょう。

> あいたー!IntPtrはそんなそうなんですねっ!ただのintだと思っていた
C++ のエクスポート関数には int なんて使われていないのに、なぜただの int を使ったんですか?

> ここで、疑問なのが、IntPtrはout指定がなくても呼び出し側で書き換えた値、
> (この場合メモリアドレス値)は呼び出し元でも変更されるのですか?
out キーワードと Out 属性は別ものです。OutAttribute を調べてください。

> きっちり関数が動作しました。...しましたが、C#側(呼び出し側)で
> buffを操作しようとしたらエラー...コレってやはりアドレスが
> ちゃんと帰ってきていないようです。
デバッガで確認ぐらいしましょう。
> ちなみに、関数宣言を
> Hoge( out IntPtr[] buff, int size );から
> Hoge( IntPtr[] buff, int size );に書き換えた場合、
> きっちり関数が動作しました。...しましたが、C#側(呼び出し側)で
> buffを操作しようとしたらエラー...コレってやはりアドレスが
> ちゃんと帰ってきていないようです。
>
> もし何かお気づきのことがあればご教示していただければ幸いです。

  メモリ管理方式の相違が主原因かと・・・

  .NET側で取得されたメモリのポインタなりをC側に渡した場合、
  C側がアクセスしようとした時点で.NET側が勝手に移動させてし
  まっている・・・と言う状態。

  http://www37.atwiki.jp/visualstudio/?cmd=word&word=visual%20studio%20%E3%83%A1%E3%83%A2%E3%83%AA%20new&type=normal&page=C%2B%2B%2FCLI%E3%81%A8%E3%81%AF

  http://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.gchandle.addrofpinnedobject(VS.80).aspx

  http://dobon.net/vb/dotnet/vb6/objptr.html

参考まで・・・

以上。
Hongliang さん
オショウ さん

ご返信誠にありがとうございます。

> 例えば C++ で引数に int* を取るとき、二つの意味がありますよね。一つは配列
> を受け取る場合。もう一つは呼び出し元に int を返す場合。
> この関数の仮引数を C# で定義する場合、前者が int[] であり後者が out int

これについては、よく分かりました。
ありがとうございます。

>>あいたー!IntPtrはそんなそうなんですねっ!ただのintだと思っていた
> C++ のエクスポート関数には int なんて使われていないのに、なぜただの int
> を使ったんですか?

あ、いえ、IntPtrの内情はただのintだと思っていただけなので、
intを使っていたわけではありません。ずっとCを使っていて、ポインタの
アドレスはintで格納できることから、IntPtrってただのintなんだろう
と勝手に思っていただけです。紛らわしい書き方をして申し訳ありません。
int*とintとの差だと分かってかえってすっきりしました。

>>ここで、疑問なのが、IntPtrはout指定がなくても呼び出し側で書き換えた値、
>>(この場合メモリアドレス値)は呼び出し元でも変更されるのですか?
> out キーワードと Out 属性は別ものです。OutAttribute を調べてください。

アドバイスありがとうございます。調べてみます。

> .NET側で取得されたメモリのポインタなりをC側に渡した場合、
> C側がアクセスしようとした時点で.NET側が勝手に移動させてし
> まっている・・・と言う状態。

ふむぅ。DLLで作ったメモリをポインタの位置だけをこちらで
覚えておいて、必要なときにDLLからC#に引き出して使いたい
だけなんですが、なかなか難しいですね。
情報のご提供誠に感謝いたします。
> .NET側で取得されたメモリのポインタなりをC側に渡した場合、
> C側がアクセスしようとした時点で.NET側が勝手に移動させてし
> まっている・・・と言う状態。

今回は関係ありません。
引数に配列を定義したばあい、マーシャラは関数呼び出し中は自動的にその配列をピン留めします。
Hongliangさん
オショウさん

メモリについて今回の件でよく分かりました。
本当にありがとうございました。
無事DLLで作られたメモリ空間をC#側で引き出して、
扱うことができました!

実は引き出したメモリポインタを他のDLLへ引き渡すとき
エラーが出ていまして、C#の読み直し、DLLのソースを引っ張り出してきて
読み直し、もーわからんと思っていたら、

...DLLのEntryPoint名が違っていたのは内緒だっ!

ともあれ、C#で扱えることができました。
たくさんの有用な情報ありがとうございます。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板