このアーカイブは同期化されません。 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 などの構造体が登場する。定義を見てみよう。
構造体に関する定義があるが、
パッキングに関しては明記されていない。
ヘッダファイルは 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 として定数が使える。
さて、長くなったので、続きは明日にしよう。