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

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

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

Re[1]: 構造体配列へのポインタを持つ構造体のマーシャリング


(過去ログ 122 を表示中)

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

■72751 / inTopicNo.1)  構造体配列へのポインタを持つ構造体のマーシャリング
  
□投稿者/ キム (7回)-(2014/07/11(Fri) 16:42:17)

分類:[C#] 

お世話になります。

C言語で作成されたDLL内の関数をC#から呼び出したいと考えています。
C#のバージョンは2008、.NET Framework は3.5です。

C言語DLL側

    typedef struct
    {
        int X;
        int Y;
    } MyPoint;

    typedef struct
    {
        MyPoint* pPoints;
        int Count;
    } MyData;

    int __stdcall MyAddPoints(MyData data);

MyData内の MyPoint* pPoints は、Count個の要素を持つMyPoint型配列の先頭アドレスです。
この構造体をどのようにマーシャリングして関数を呼び出せばよいのか悩んでいます。

C#側は下記のようにしました。
MyData構造体内に別の構造体配列の先頭アドレスを示すメンバーがあるので、
呼び出し前にMarshal.AllocCoTaskMemでメモリを確保して、構造体配列の要素を
Marshal.StructureToPtrで1つずつコピーしています。
そして呼出し後には確保したメモリをMarshal.FreeCoTaskMemで解放しています。

これで呼び出しは成功したのですが、非常に無駄な処理をしているのではないか
と思います。

もっとスマートな方法をご教示いただけると助かります。
#優先順位は、パフォーマンス、可読性の順です。

よろしくお願いします。

    [StructLayout(LayoutKind.Sequential)]
    struct MyPoint
    {
        public int X;
        public int Y;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct MyData
    {
        public IntPtr Points;
        public int Count;
    }

    [DllImport("My.dll")]
    private static extern int MyAddPoints(MyData data);

    // 呼び出しラッパー
    bool AddPoints(List<MyPoint> points)
    {
        int elemSize = Marshal.SizeOf(typeof(MyPoint));

        // アンマネージドメモリにMyPoint構造体配列を確保
        MyData data = new MyData();
        data.Count = points.Count;
        data.Points = Marshal.AllocCoTaskMem(elemSize * data.Count);

        // アンマネージドメモリ上のMyPoint構造体配列に値をコピー
        IntPtr ptr = data.Points;
        foreach (MyPoint mp in points)
        {
            Marshal.StructureToPtr(mp, ptr, false);
            ptr = new IntPtr(ptr.ToInt32() + elemSize);
        }

        // 呼び出し
        int result = MyAddPoints(data);

        // アンマネージドメモリ解放
        Marshal.FreeCoTaskMem(data.Points);

        return result == 0;
    }

引用返信 編集キー/
■72756 / inTopicNo.2)  Re[1]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ Cプログラマ (1回)-(2014/07/12(Sat) 23:19:12)
C#にはあまり詳しくないですが、Cのプログラマとして疑問を書いておきます。
// アンマネージドメモリ上のMyPoint構造体配列に値をコピー
IntPtr ptr = data.Points;
foreach (MyPoint mp in points)
  {
     Marshal.StructureToPtr(mp, ptr, false);
     ptr = new IntPtr(ptr.ToInt32() + elemSize);
  }
これは明らかにヘンです。これでC側から受け取ることを期待するなら
typedef struct
  {
      MyPoint** pPoints;
      int Count;
  } MyData;
のはずですが、それでもCountで渡されてくるものが期待と違うと思います。
C言語のポインタについて無知のように見えます。C#で全面的に記述するこ
とをお勧めします。

引用返信 編集キー/
■72767 / inTopicNo.3)  Re[2]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ キム (8回)-(2014/07/14(Mon) 10:57:15)
No72756 (Cプログラマ さん) に返信
お返事ありがとうございます。

> C言語のポインタについて無知のように見えます。C#で全面的に記述するこ
> とをお勧めします。

言葉足らずでごめんなさい、既存のC言語で記述されたDLLを呼び出すので、C#で全面的に記述することは出来ないのです。
また、C言語は使ったことはないですが、C++言語は普段使っており、ポインタについては無知ではないつもりです。

> IntPtr ptr = data.Points;
> foreach (MyPoint mp in points)
>   {
>      Marshal.StructureToPtr(mp, ptr, false);
>      ptr = new IntPtr(ptr.ToInt32() + elemSize);
>   }
> これは明らかにヘンです。これでC側から受け取ることを期待するなら
> typedef struct
>   {
>       MyPoint** pPoints;
>       int Count;
>   } MyData;
> のはずですが、それでもCountで渡されてくるものが期待と違うと思います。

ご指摘の部分はMyPoint配列の内容をコピーしている部分であり、MyDataの型とは関係ないように思います。
(配列用メモリを確保してMyPointポインタを設定しているのは最初の部分です)
Countも配列の要素数なので、C言語側の仕様にあっていると思います。
コピー処理がおかしくてメモリを壊しているというご指摘でしょうか?
せっかくのお返事をうまく理解できずにごめんなさい。

引用返信 編集キー/
■72768 / inTopicNo.4)  Re[3]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ とっちゃん (245回)-(2014/07/14(Mon) 14:37:34)
とっちゃん さんの Web サイト
No72767 (キム さん) に返信


No72756 (Cプログラマ さん) に返信
コードとしては間違ってませんよ(ただし、違うバグはありましたけど)。
32ビットコードで動かしてる分には現行コードでも意図した動作になっているはずです。
ということで、怪しいと引っ張った部分にC風味な動作コメントを入れておきました

>IntPtr ptr = data.Points;
>foreach (MyPoint mp in points)
> {
> Marshal.StructureToPtr(mp, ptr, false); // memcpy( ptr, mp, sizeof(mp) ); に相当
> ptr = new IntPtr(ptr.ToInt32() + elemSize); // ポインタを「int型(32bit値)」に変換して、構造体サイズ分を足して、再び「ポインタ型整数」を生成する
> }
という動きになっています。
わかりにくいかもしれませんが...メモリを確保したりということはしていません。


が、それとは別に重大な潜在バグがここには潜んでいます。

それは、コメントにも書いたように int 型にキャストしていること。
IntPtr は、ポインタ値を格納可能な整数型という特殊な数値型です。
Windows SDK の中には、C 向けの同様の型があります(INT_PTRなど)。

さらに、IntPtrは値型なので、new IntPtr() のような形は必須ではありません(やってはいけないわけではない)。
そのまま値を操作する演算子を呼び出せます。


具体的にはどうするか?というと

ptr += elemSize;

とするだけです。
普通に、ポインタと同じ扱い(基本的な部分で)が可能です。

もしかしたらこんなまどろっこしいことをせずとも、points.ToArray() あたりで済んでしまうかもしれませんが
その辺については詳しくないので省略。


それと、サンプルコードでは、一方的に渡すだけになっているようですが、逆のパターンでも同様です。
ptr をステップさせる部分は、+= で十分です。

最適解を求めるなら、場合によっては C++/CLI を使って、双方のメリットを最大限享受したままのほうが
最速コードを導き出せる場合もあります。

どんなコードなのかわからないので何とも言えませんが、そういう選択肢も一応ありということで。。。

引用返信 編集キー/
■72769 / inTopicNo.5)  Re[4]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ キム (9回)-(2014/07/14(Mon) 16:14:33)
No72768 (とっちゃん さん) に返信

ご丁寧なお返事ありがとうございます。
基本構成は間違っていないとのことで少し安心しました。

> が、それとは別に重大な潜在バグがここには潜んでいます。

> ptr += elemSize;
>
> とするだけです。
> 普通に、ポインタと同じ扱い(基本的な部分で)が可能です。

はい、私も単純にオフセットを加算したかったのですが、 IntPtr構造体にオフセット加算のメソッド&演算子
が追加されたのは4.0からで、私の使っている3.5ではエラーとなってしまうのです。
そこで仕方なくint(Int32)を受け取るコンストラクタを使ったのでした。

また、ご指摘のお陰で、クライアントに『対象OSは32bit限定とする』と確認することが出来ました。
これで、intにキャストしても安心と思います。
ありがとうございます。

> もしかしたらこんなまどろっこしいことをせずとも、points.ToArray() あたりで済んでしまうかもしれませんが
> その辺については詳しくないので省略。

はい、points.ToArray() で一括変換できたら素晴らしいです。
時間をとって試してみます。

クライアント要求が『C#を使うこと』なので、points.ToArray() も含めてもう少し最適化を模索してから
結論を出そうと思います。
結論が出たらご報告して解決としたいと思います。
引用返信 編集キー/
■72770 / inTopicNo.6)  Re[5]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ とっちゃん (246回)-(2014/07/14(Mon) 17:50:21)
とっちゃん さんの Web サイト
No72769 (キム さん) に返信
> はい、私も単純にオフセットを加算したかったのですが、 IntPtr構造体にオフセット加算のメソッド&演算子
> が追加されたのは4.0からで、私の使っている3.5ではエラーとなってしまうのです。
> そこで仕方なくint(Int32)を受け取るコンストラクタを使ったのでした。
>
あ。。。w
.NET 3.5 だと、単純な + 演算子もないですね。


> また、ご指摘のお陰で、クライアントに『対象OSは32bit限定とする』と確認することが出来ました。
> これで、intにキャストしても安心と思います。
> ありがとうございます。
>
えーっと、+= にしてしまったので、いろいろと隠蔽されてしまったのですが
まず、ToInt32 としている時点で、型を意識していないな?ということを感じました。

間接的であれメモリイメージを扱う場合、IntPtrなどのポインタ値型がいくつのサイズになるのかを
常に意識しておくことをお勧めします。

特に、P/Invokeなどを利用したAPI呼び出しで同じようなことを行う場合、32bitなのか64bitなのかでは
かなり影響が出てしまいます。

ちなみに、32bitOSじゃなければ動かないのではなく、プラットフォームターゲットをx86(32bit)にしなければ動かないです。

ただし、CのDLLが32/64の両方のDLLを提供しているという場合は、
呼び出し側も32/64 両方で動くコードを書く必要があります。

今回の場合は、

ptr = (IntPtr)(ptr.ToInt64() + elemSize);

と、より大きな値が扱える Int64 側を使えば、プラットフォームターゲットに依存したバグは消滅します。

ただし、CPUによっては、64bit整数演算が早くない32bitCPUがあります(x64搭載前の古いCPUなど)。
その場合、どの程度演算性能が落ちるかがわかりません。

もし、速度的に満足できないというようなら、環境を見て(IntPtr.Size==sizeof(int) が成立するかしないかで判断可能)
ToInt64 を使うコード、ToInt32を使うコードを呼び分ければいいと思います。

引用返信 編集キー/
■72776 / inTopicNo.7)  Re[6]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ キム (10回)-(2014/07/14(Mon) 20:34:41)
No72770 (とっちゃん さん) に返信
> えーっと、+= にしてしまったので、いろいろと隠蔽されてしまったのですが
> まず、ToInt32 としている時点で、型を意識していないな?ということを感じました。

一応最初にC側DLLは32bitと口頭で聞いていたのですが、それだけでILP32なノリで作ってしまいました。
もっと注意しないとだめですね。気をつけます。

> 間接的であれメモリイメージを扱う場合、IntPtrなどのポインタ値型がいくつのサイズになるのかを
> 常に意識しておくことをお勧めします。
>
> 特に、P/Invokeなどを利用したAPI呼び出しで同じようなことを行う場合、32bitなのか64bitなのかでは
> かなり影響が出てしまいます。
>
> ちなみに、32bitOSじゃなければ動かないのではなく、プラットフォームターゲットをx86(32bit)にしなければ動かないです。

C側DLLが32bit版しか存在しないこと、C#側の構成マネージャーでプラットフォームがx86になっていることを再確認しました。
こんどこそintで大丈夫かなと思います。

でも、

> もし、速度的に満足できないというようなら、環境を見て(IntPtr.Size==sizeof(int) が成立するかしないかで判断可能)
> ToInt64 を使うコード、ToInt32を使うコードを呼び分ければいいと思います。

教えていただいた方法は、実行コストも掛からないし、より安全になるので使わさせていただこうと思います。
ありがとうございます。
引用返信 編集キー/
■72778 / inTopicNo.8)  Re[7]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ Atata!! (1回)-(2014/07/14(Mon) 21:19:17)
Atata!!です。


本当に関数の呼び出しが提示されている例の通りであるならば、
C#側は以下のように記述することができます。

[DllImport("My.dll")]
private static extern int MyAddPoints(MyPoint[] points, int count);

この場合、points.ToArray()の結果を渡すだけで良いことになりますが、
実際のところはどうでしょうか?

引用返信 編集キー/
■72779 / inTopicNo.9)  Re[8]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ キム (11回)-(2014/07/15(Tue) 13:16:00)
No72778 (Atata!! さん) に返信

お返事ありがとうございます。

> 本当に関数の呼び出しが提示されている例の通りであるならば、
> C#側は以下のように記述することができます。
>
> [DllImport("My.dll")]
> private static extern int MyAddPoints(MyPoint[] points, int count);

__stdcall規約では構造体を値渡しするのと、各メンバーを引数に分けて渡すのとでスタック上の並びが
同じになるということでしょうか?
だとしたら、難しい構造体のマーシャリングに悩まず素直に渡せるので嬉しいですね。

ただ、今回提示した構造体はミニマムケースとして余計なメンバーを省略したもので、
実際の構造体は提示したメンバー以外に値型のメンバーが10個ほどあります。
きっと、これらを引数にするにはアライメントなども考慮しなければならないですね。

今回は期日も迫っているので、検討課題とさせてください。
要調査項目としてしっかり記録したので、機会があったら活用したいです。
素敵なアイディアありがとうございます。
引用返信 編集キー/
■72796 / inTopicNo.10)  Re[9]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ Atata!! (2回)-(2014/07/16(Wed) 20:35:20)
基本的にその認識で問題ないと私は思います。


で、元々の本題である構造体配列をマーシャリングして
そのアンマネージポインタを得る方法について記載します。
実際に速度は計測していませんが、感覚的に早そうな順です。


1.unsafeを有効にしてマネージ構造体配列の先頭ポインタを得る方法
ILレベルでは2と同等かもしれませんが、最もわかりやすい書き方だと思います。

bool AddPoints(List<MyPoint> points)
{
    unsafe
    {
        fixed (MyPoint* p = points.ToArray())
        {
            MyData data = new MyData();
            data.Count = points.Count;
            data.Points = (IntPtr)p;

            int result = MyAddPoints(data);

            return result == 0;
        }
    }
}


2.Marshal.UnsafeAddrOfPinnedArrayElementを使用する方法
VB.NETでも使用できるので私が最もよく使用する方法です。

bool AddPoints(List<MyPoint> points)
{
    MyPoint[] arr = points.ToArray();
    GCHandle h = GCHandle.Alloc(arr, GCHandleType.Pinned);
    try
    {
        MyData data = new MyData();
        data.Count = points.Count;
        data.Points = Marshal.UnsafeAddrOfPinnedArrayElement(arr, 0);

        int result = MyAddPoints(data);

        return result == 0;
    }
    finally
    {
        h.Free();
    }
}


3.APIでメモリをコピーする方法
状況によっては使います。

[DllImport("KERNEL32")]
private static extern int RtlMoveMemory(IntPtr dst, MyPoint[] src, int size);

bool AddPoints(List<MyPoint> points)
{
    int elemSize = Marshal.SizeOf(typeof(MyPoint));

    MyData data = new MyData();
    data.Count = points.Count;
    data.Points = Marshal.AllocCoTaskMem(elemSize * data.Count);
    try
    {
        RtlMoveMemory(data.Points, points.ToArray(), elemSize * data.Count);

        int result = MyAddPoints(data);

        return result == 0;
    }
    finally
    {
        Marshal.FreeCoTaskMem(data.Points);
    }
}


4.Marshal.Copyでメモリをコピーする方法
これを使う場合、最初からマネージ側の型をMarshal.Copyしやすい感じで実装すると思います。

bool AddPoints(List<MyPoint> points)
{
    int elemSize = Marshal.SizeOf(typeof(MyPoint));

    MyData data = new MyData();
    data.Count = points.Count;
    data.Points = Marshal.AllocCoTaskMem(elemSize * data.Count);
    try
    {
        int[] arr = new int[data.Count * 2];
        for (int i = 0; i < points.Count; i++)
        {
            arr[i * 2 + 0] = points[i].X;
            arr[i * 2 + 1] = points[i].Y;
        }
        Marshal.Copy(arr, 0, data.Points, arr.Length);

        int result = MyAddPoints(data);

        return result == 0;
    }
    finally
    {
        Marshal.FreeCoTaskMem(data.Points);
    }
}


ワンライナー派なら以下のような書き方でも良いかもしれません。
var arr = Array.ConvertAll(points.ToArray(), new Converter<MyPoint, long>(
    delegate(MyPoint point) { 
        return point.X + point.Y * ((long)UInt32.MaxValue + 1);
    }));
Marshal.Copy(arr, 0, data.Points, arr.Length);


5.型を動的に生成する方法

サンプルを書こうと思いましたが、めんどくさくなったのでやめました。
参考になるサイトを見つけたのでリンクを載せておきます。
http://sayurin.blogspot.jp/2008/11/ptrtostructurearray.html

引用返信 編集キー/
■72827 / inTopicNo.11)  Re[10]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ キム (12回)-(2014/07/22(Tue) 16:48:13)
No72796 (Atata!! さん) に返信

お返事ありがとうございます。
応答が遅くなってしまってごめんなさい。

unsafeは恐くて今まで敬遠していたのですが、ポインタが直に書けることで
とてもすっきりしますね。
今の実装方法からの変更が容易で、明示的なアンマネージメモリの確保/解放が不要、しかも高速と
良いこと尽くめな気がします。
次回のバージョンアップで書き換えようと思います。

Marshal.UnsafeAddrOfPinnedArrayElementは調査から漏れていてまったく知らなかったので、とても勉強になりました。
自分の中で使える選択肢が増えて嬉しいです。

他にもいろいろなパターンを教えていただきありがとうございます。
引用返信 編集キー/
■72828 / inTopicNo.12)  Re[1]: 構造体配列へのポインタを持つ構造体のマーシャリング
□投稿者/ キム (13回)-(2014/07/22(Tue) 17:01:15)
皆様のお陰で理解が深まりました。
ここまでの内容をまとめて解決としたいと思います。

まず、定義についてです。

・構造体内に別構造体の配列を持つような構造体を引数で渡す場合、記述が難しくなる。
・可能であれば、C側の宣言をC#から呼びやすい形にすると良い。
・C側の構造を変更できない場合は、C#側の宣言を工夫することですっきり書ける可能性がある。
 __stdcall規約では構造体を値渡しするのと、各メンバーを引数に分けて渡すのとでスタック上の
 並びが同じになるので、各メンバーを個別の引数に展開できる。


次に、構造体内に別構造体の配列を持つような構造体を引数で渡す手法についてです。

・C++/CLIの使用が許される場合は、C++/CLIで記述するのが最大の成果を得る早道。
 (マネージド/アンマネージド双方のメリットを最大限享受できる)
・C#でもunsafeな記述が許される場合は、シンプルで高速な記述が出来る可能性が高い。
 (ポインタの記述が出来るので、C#側がC側に沿った素直な記述に出来、尚且つ高速)
・IntPtrによるポインタ演算を行う場合、.NET4.0以降であれば += 演算子が使えるので問題なく記述できる。
 一方、.NET3.5までは一旦整数型に変換してから演算する必要があるので、ポインタサイズに注意する。
・私が最初に提示したコードでも、(ポインタサイズを考慮していない点以外は)問題ないが、やはり遅いと思われる。
 状況に応じて手法を選択すると良い。

各手法については、Atata!!さんがわかりやすいサンプルをご提示くださっているので、そちらをご参照ください。
解決済み
引用返信 編集キー/


トピック内ページ移動 / << 0 >>

このトピックに書きこむ

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

管理者用

- Child Tree -