トップ回答者
LayoutKind.Explicitを指定した構造体およびSystem.Char型をメンバに持つ構造体のレイアウトについて

質問
-
いつもお世話になります。
unsafeを使用したC言語との相互運用のコーディングをしていたところ、
「LayoutKind.Explicitを指定した構造体およびSystem.Char型をメンバに持つ構造体」の
メンバの配置順が期待していた結果と異なってしまい、受け渡した値が不正になってしました。
(Marshal.OffsetOfメソッドで取得すると期待通りでした。)回避方法はわかったのですが、なぜそのような結果になってしまうのか
MSDNを参照してもわからなかったため、お知恵を拝借したく質問させて頂きます。以下がその再現コードです。
コンパイル/動作環境はWindows 7 x64 上の.Net3.5と.Net5で、ターゲットがx86です。using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Explicit)] public struct LARGE_INTEGER { [FieldOffset(0)] public uint lowpart; [FieldOffset(4)] public int highpart; [FieldOffset(0)] public long QuadPart; }
/* 以下のC言語で定義した構造体に対応する
#include <PshPack4.h>
struct A
{
LARGE_INTEGER li;
LONGLONG i;
WCHAR c;
};
#include <PopPack.h> */ [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct A { //System.Int64に変更すればMarshalクラスの結果と一致する public LARGE_INTEGER li; public long i; //System.UInt16に変更すればMarshalクラスの結果と一致する。
//System.Charがblittableではないからか? public char c; } class Program { static void Main(string[] args) { { A a = new A(); Console.WriteLine("Marshal.SizeOf(A): {0}", Marshal.SizeOf(typeof(A))); Console.WriteLine("Marshal.OffsetOf(li): {0}", Marshal.OffsetOf(typeof(A), "li")); Console.WriteLine("Marshal.OffsetOf(i): {0}", Marshal.OffsetOf(typeof(A), "i")); Console.WriteLine("Marshal.OffsetOf(c): {0}", Marshal.OffsetOf(typeof(A), "c")); unsafe { A* p = &a; Console.WriteLine("sizeof(A): {0}", sizeof(A)); Console.WriteLine("offsetof(li): {0}", new IntPtr(&p->li).ToInt64() - new IntPtr(p).ToInt64()); Console.WriteLine("offsetof(i): {0}", new IntPtr(&p->i).ToInt64() - new IntPtr(p).ToInt64()); Console.WriteLine("offsetof(c): {0}", new IntPtr(&p->c).ToInt64() - new IntPtr(p).ToInt64()); } } } } /* 実行結果
Marshal.SizeOf: 20
Marshal.OffsetOf(li): 0
Marshal.OffsetOf(i): 8
Marshal.OffsetOf(c): 16
sizeof(A): 20
offsetof(li): 12
offsetof(i): 0
offsetof(c): 8
*/
以上よろしくお願いいたします。
- 編集済み udaken 2011年9月7日 16:39
回答
-
最初はDllImportやMarshal.PtrToStructure()用ということで、K. Takaokaさんと同じ考えでいましたが、StructLayoutAttributeクラスの説明に
一般に、共通言語ランタイムがマネージ メモリ内のクラスまたは構造体のデータ フィールドの物理的なレイアウトを制御します。
とあったり、LayoutKind.Explicitでフィールドの位置が指定できたりということから、マネージメモリのレイアウトを指定するものなのかなと思うようにもなりました。
そうなるとCharSet.Ansiで位置がずれるのはよくわかりません。文字コード変換が必要だからずれてるからだとは思いますが、その根拠となる記述は見つけられませんでした。
- 回答としてマーク udaken 2011年9月17日 13:05
すべての返信
-
あーcharはStructLayoutAttribute.CharSetの影響を受けますね。blittableではないけど、CharSet.Unicodeならblittable相当なのかなぁ?
[StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Unicode)] public struct A { public LARGE_INTEGER li; public long i; public char c; }
-
佐祐理様
早々にご回答頂いていたのに返信が遅くなり申し訳ありません。
ご指摘の通り、CharSet.Unicodeをつければうまく行きますね。
sizeof(char)=2となったのでunsafeコンテクストでは常にUnicodeで扱うだろう、と早合点してました。質問に記載していたコードですと、CharSet.Ansiを指定した場合と同じ結果のようですね。
となると新たな疑問が湧いてくるのですが、
というC言語の構造体に対応するC#の構造体として、#include <PshPack4.h> struct A { LARGE_INTEGER li; LONGLONG i; CHAR c; }; #include <PopPack.h>
[StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Ansi)] public struct A { public LARGE_INTEGER li; public long i; public char c; }
という定義をした場合、メンバの配置順が記述した順番でなくなるのはなぜなのでしょうか?
(非blittable型の動作なので微妙な部分であるとは思っているのですが、LayoutKind.Sequentialを指定しているのに記述した順番に配置されない点が納得できないんです・・・)以上よろしくお願いいたします。
-
私が勘違いしていたらすいません。
構造体レイアウトの定義は、マーシャリングした先のメモリレイアウトを指定するものであって、マネジドレイアウトには直接は影響しないのではないですか? 確認方法が間違っているだけで、.NET Framework 側はきちんと動いてるのでは?と思いました。
実際に Marshal.OffsetOf() は正しい値を返しているし、マーシャラを通して作成したメモリイメージも正しいものになっていますよね? unsafe コードではマネージド構造体を操作することになるので、マーシャラ経由前の格納位置が違っていても誰も何も困らないはずです。
- 編集済み K. Takaoka 2011年9月13日 4:05
-
最初はDllImportやMarshal.PtrToStructure()用ということで、K. Takaokaさんと同じ考えでいましたが、StructLayoutAttributeクラスの説明に
一般に、共通言語ランタイムがマネージ メモリ内のクラスまたは構造体のデータ フィールドの物理的なレイアウトを制御します。
とあったり、LayoutKind.Explicitでフィールドの位置が指定できたりということから、マネージメモリのレイアウトを指定するものなのかなと思うようにもなりました。
そうなるとCharSet.Ansiで位置がずれるのはよくわかりません。文字コード変換が必要だからずれてるからだとは思いますが、その根拠となる記述は見つけられませんでした。
- 回答としてマーク udaken 2011年9月17日 13:05
-
外池と申します。
LayoutKind.Explicitだと、一応、意のままに動いてくれるようですが、LayoutKind.Sequential の時の挙動が、ドキュメントに書かれていない(あるいは別のドキュメントに書かれている)何かによって左右されるようですね。
http://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.layoutkind(v=VS.100).aspx
によれば、「プログラムの中で宣言する順序」とは書かれておらず、アンマネージメモリにエクスポートされる時の何らかの規則が絡んでいるようです。また、
http://msdn.microsoft.com/en-us/library/ms993883.aspx
によれば、構造体が入れ子になっている場合は、C++/CLIで書くほうが良いような示唆がなされています。
で・・・、今回のお困りのプログラムに関する特定の解決方法ですが、
外側の構造体もLayoutKind.Explicitで、変数ごとに位置を明示してやる、というのはいかがでしょうか? 試しに走らせてみたところ、大丈夫のようです。
追記:あ、解決はされていましたね。ごめんなさい。
追記2:通読していませんが、役に立ちそうな情報がありました。
http://blogs.msdn.com/b/jaredpar/archive/2005/07/11/437584.aspx
(ホームページを再開しました)
- 編集済み 外池 2011年9月13日 6:11
-
K. Takaoka様
構造体レイアウトの定義は、マーシャリングした先のメモリレイアウトを指定するものであって、マネジドレイアウトには直接は影響しないのではないですか? 確認方法が間違っているだけで、.NET Framework 側はきちんと動いてるのでは?と思いました。
私も佐祐理様と同様にStructLayoutAttributeは.NET Framework側のレイアウトを指定できるものと考えています。
のような.NET Frameworkにとっては意味のない構造体を定義した場合、[StructLayout(LayoutKind.Sequential, Size = 128)] struct B {}
Marshal.SizeOf(typeof(B))とsizeof(B)は両方共128を返すからです。実際に Marshal.OffsetOf() は正しい値を返しているし、マーシャラを通して作成したメモリイメージも正しいものになっていますよね? unsafe コードではマネージド構造体を操作することになるので、マーシャラ経由前の格納位置が違っていても誰も何も困らないはずです。
unsafeコードで相互運用しなければ、確かに困らないと思います。
ご指摘を受けて改めてMSDNを見返してみると、unsafeコード(でのポインタ)で相互運用できる、という説明は無いようでした。
実際、例示していた構造体Aの配列をGCHandle .Alloc メソッド (Object, GCHandleType)で固定してアドレスを取得しようとすると、
例外 System.ArgumentException
「オブジェクトに、プリミティブでないか、または blittable でないデータが含まれています。」
が発生するため、「.NET FrameworkとしてはCharSetを指定していてもレイアウトは保証していない」と取れます。そこで私の理解をまとめますと、
- blittable型のみを含んでいる構造体であれば、StructLayoutAttributeによる指定でマネージメモリのレイアウトをC言語(アンマネージ)と同様にすることができる
- 非blittable型を含んでいる構造体のレイアウトはStructLayoutAttribute通りになっている保証はない
(なぜか配置順が入れ替わることがある)
非blittableな値型(charやbool?)を含んでいれば、unsafeコンテクストでマネージメモリポインタを取得できるが、相互運用できるものではない
という、結論になりました。
ドキュメント化されていない推測での理解になりますが、
問題点があれば後学のためにご指摘いただけると幸いです。 -
外池様
こちらには「型のメンバがマネージ型定義に示されているのと同じ順序で、アンマネージ メモリ内にレイアウトされることを示します。」と記載があります。(マーシャリングの動作を説明しているページですが)
http://msdn.microsoft.com/ja-jp/library/0t2cwe11%28VS.90%29.aspx
C++/CLIの件については、全文を読んだわけではありませんが技術的な制約ではなく、実用面からC++/CLIを推奨する、という内容だと理解しました。(その点はもちろん同意します。)
ただ、今回はC#でコーディングした場合について知りたいと思っていました。
追記2のURLは参考になりました。ありがとうございました。