2006 年 6 月 21 日 22 時 28 分

構造体のパッキング


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


シェルのインタフェースには構造体が良く出てくる。
これらは、インタフェースの皮をかぶった API だと
考えてもいい程、低水準の定義が多いのだ。

基本的には、C# では値型の struct で
色々な型を並べた構造を扱うすることができるが、
C 言語の構造体を定義する場合、注意点がある。
それは、パッキングやアラインメントと呼ばれる機構だ。

.NET では「型」と「名前」を使ってアクセスするので、
フィールド定義の順番や型などを気にしないのだが、
シェルの構造体は「メモリレイアウト」であり、
「定義の順序」や「型の大きさ」が大きな意味を持つ。

構造体の先頭から値をバイト単位で詰めていくと、
構造体としてのメモリ効率は良いのだが、
アクセス効率は悪くなることがある。

例えば、Intel の 32 ビットの環境では、
4 バイトの整数値が、4 の倍数のアドレスに配置されないと、
4 の倍数のアドレスに配置された場合よりも
読み書きに余計な時間がかかるのだ。

こういったことを考慮して、
構造体にバイト単位の詰め物を入れることで整列し、
アクセス速度を最適化することが考えられた。
この作業をパッキングと呼び、
C 言語ではコンパイラによって自動的に行われる。

パッキングはバイト数で指定することが一般的であり、
整列される間隔の最大バイト数を指定する。
1, 2, 4, 8, 16, 32 などが使われる。

既定ではパッキングサイズが 8 であり、
2 バイトの型は、2 バイト境界のメモリ位置に、
4 バイトの型は、4 バイト境界のメモリ位置に、
8 バイト以上の型は 8 バイト境界のメモリ位置に整列される。

ちなみに、Windows で定義されている構造体は、
4 バイトのパッキングがなされているものが多い。

さて、IColumnProvider の Initialize メソッドの引数には、
SHCOLUMNINIT などの構造体が登場する。定義を見てみよう。

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/structures/shcolumninit.asp

構造体に関する定義があるが、
パッキングに関しては明記されていない。
ヘッダファイルは shlobj.h と書いてあるので、
include フォルダより shlobj.h を探す。
すると、以下の定義が見つかった。
(必要な部分だけ引用)

    #include <pshpack8.h>

    typedef struct {
        ULONG  dwFlags;
        ULONG  dwReserved;
        WCHAR  wszFolder[MAX_PATH];
    } SHCOLUMNINIT, *LPSHCOLUMNINIT;

    #include <poppack.h>

なんとなく想像はつくと思うが、
#include <pshpack8.h> は、
パッキングのサイズを明示している。
#include <poppack.h> は既定に戻している
SHCOLUMNINIT のパッキングサイズは 8 ということだ。

では、SHCOLUMNINIT を C# に変換してみる。
これらの構造体は IColumnProvider 用なので、
IColumnProvider.cs に定義することにしよう

    [StructLayout(LayoutKind.Sequential,
            Pack=8, CharSet=CharSet.Unicode)]
    public struct SHCOLUMNINIT {
        public uint dwFlags;
        public uint dwReserved;
        [MarshalAs(UnmanagedType.ByValTStr,
                SizeConst=Constants.MAX_PATH)]
        public string wszFolder;
    }

見慣れないものが幾つか出てきたので順に見ていこう。

まずは、構造体の定義。

StructLayoutAttribute は、レイアウトに関する属性だ。
.NET ではこの属性を使って、
API や COM に渡す構造体のレイアウトを決める。

StructLayout のコンストラクタ引数で渡す
LayoutKind.Sequential は、フィールドが
定義した順番にメモリ上に並べられる事を意味し、
Pack フィールドで、パッキングサイズを指定する。

CharSet フィールドは、構造体内の文字列型が、
Unicode か ANSI(Shift_JIS など)かを指定する。

次に、フィールドを見ていこう。

.NET の構造体は値型である以外はクラスと同じなので、
フィールドには public などのアクセス指定が必要だ。

ULONG は 符号なしの 4 バイト整数なので、uint となる。

そして、面白いのが、wszFolder フィールドだ。

WCHAR は、2 バイトの UNICODE 文字型なので、
このフィールドは、UNICODE 文字の配列であるが、
C 言語では配列として使うだけではなく、
文字列を格納するためのバッファとしての使い方がある。

.NET は、文字列のバッファとしての配列を特別扱いし、
.NET の String 型として扱えるように記述できる。
これのおかげで非常にプログラミングが楽になるのだ。

しかし、String 型は参照型(クラス)なので、
単に String 型のフィールドを定義しただけでは、
文字列へのポインタとして扱われてしまう。

そこで、MarshalAsAttribute 属性を指定し、
マーシャリング(データの変換)規則を指定する。

MarshalAs のコンストラクタ引数で渡す
UnmanagedType.ByValTStr は、フィールドが
構造体に埋め込まれた文字列配列である事を示す。

UnmanagedType.ByValTStr の場合、
C 構造体上では配列なので、要素数の定義が必要だ。
これは、SizeConst フィールドで明示している。

MAX_PATH は windef.h で定義されている定数であり、
パスの長さを格納するためバッファの最大長だ。
これは非常に多くの場所で利用されている。

.NET では、名前空間に直接定数定義ができないので、
定数用のクラスを作って定義することにしよう。

プロジェクトの Interop フォルダ内に、
Constants.cs を追加し、MAX_PATH を定義する。

    using System;

    namespace LoaferShellEx.Interop {

        public sealed class Constants {

            // 一般的な定数
            public const int MAX_PATH = 260;

            // クラスのインスタンスは作成させない
            private Constants() {
            }

        }
    }

これで、Constants.MAX_PATH として定数が使える。

さて、長くなったので、続きは明日にしよう。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.