GUID を引数に取る Win32 API の宣言と使用方法

GUID 限定と言う訳ではないのですが.例えば,PowerEnumerate と言う関数は C では以下のような宣言になっています.

DWORD WINAPI PowerEnumerate(
    __in_opt   HKEY RootPowerKey,
    __in_opt   const GUID *SchemeGuid,
    __in_opt   const GUID *SubGroupOfPowerSettingsGuid,
    __in       POWER_DATA_ACCESSOR AccessFlags,
    __in       ULONG Index,
    __out_opt  UCHAR *Buffer,
    __inout    DWORD *BufferSize
);

これを C# から使用しようとすると DllImport を用いて宣言する必要があるのですが,const GUID* をどうするかで問題になってきます.

ref Guid を使用する方法

C# には GUID を扱うための Guid 構造体 が用意されていますので,これを使おうとすると以下のような宣言になります.

public enum POWER_DATA_ACCESSOR {
    ACCESS_SCHEME               = 16,
    ACCESS_SUBGROUP             = 17,
    ACCESS_INDIVIDUAL_SETTING   = 18
}

[DllImport("PowrProf.dll")]
public static extern UInt32 PowerEnumerate(
    IntPtr RootPowerKey,
    ref Guid SchemeGuid,
    ref Guid SubGroupOfPowerettingsGuid,
    POWER_DATA_ACCESSOR AccessFlags,
    UInt32 Index,
    ref Guid Buffer,
    ref UInt32 BufferSize
);

この宣言の場合,使用する側は Guid オブジェクトを生成して ref 付きで渡すだけで良いので楽なのですが,「NULL が指定できない」と言う問題があります.SchemeGuid, SubGroupOfPowerettingsGuid の 2つの引数は,指定したり指定しなかったりと取得したい内容によって頻繁に変わってしまうので,こう言ったケースでは ref Guid で宣言する事はできません.

IntPtr で宣言して呼び出し側で変換する方法

「Guid を指定するときも指定しないときもある」のような場合は,宣言を IntPtr で行って呼び出し側で頑張ると言う方法を取る必要があります.

[DllImport("PowrProf.dll")]
public static extern UInt32 PowerEnumerate(
    IntPtr RootPowerKey,
    IntPtr SchemeGuid,
    IntPtr SubGroupOfPowerettingsGuid,
    POWER_DATA_ACCESSOR AccessFlags,
    UInt32 Index,
    ref Guid Buffer,
    ref UInt32 BufferSize
);

// サブグループの GUID を取得してみる.
// この場合,SchemeGuid には値を設定して SubGroupOfPowerettingsGuid は NULL.
void f(Guid scheme) {
    byte[] data = new byte[Marshal.SizeOf(scheme)];
    GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
    try {
        Marshal.StructureToPtr(scheme, handle.AddrOfPinnedObject(), true);
        
        Guid dest = Guid.Empty;
        UInt32 index = 0;
        UInt32 size = (uint)Marshal.SizeOf(dest);
        UInt32 status = PowerEnumerate(
            IntPtr.Zero,
            handle.AddrOfPinnedObject(),          // GUID を指定する場合
            IntPtr.Zero,                          // NULL の場合
            POWER_DATA_ACCESSOR.ACCESS_SUBGROUP,  // サブグループの GUID を取得する
            index,
            ref dest,
            ref size
        );
        
        // ... 必要な処理 ...
    }
    finally {
        handle.Free();
    }
}

上記は,GCHandle を使ってピン止めして Win32 API に渡すと言う方法です.構造体からポインタに変換する方法はそれ以外にもいくつかあるようです(参考:2009-05-12).

新たにクラスを定義する方法

IntPtr で宣言して呼び出し側で頑張ると言う方法はやはりめんどくさいので,http://d.hatena.ne.jp/xcaqhbaj/20110118/1295359360 で StructLayout 属性を使ってクラスを定義し,そのクラスを使用すると言う方法が紹介されていました.

// Guid の代わりに使用する
[StructLayout(LayoutKind.Sequential)]
public class RefGuid {
    public int Data1;
    public short Data2;
    public short Data3;
    [MarshalAs(UnmanagedType.U1, SizeConst = 8)]
    public byte Data4;
}

[DllImport("PowrProf.dll")]
public static extern UInt32 PowerEnumerate(
    IntPtr RootPowerKey,
    RefGuid SchemeGuid,
    RefGuid SubGroupOfPowerettingsGuid,
    POWER_DATA_ACCESSOR AccessFlags,
    UInt32 Index,
    RefGuid Buffer,
    ref UInt32 BufferSize
);

// RefGuid を使用する場合
void f(RefGuid scheme) {
    RefGuid dest = new RefGuid();
    UInt32 index = 0;
    UInt32 size = (uint)Marshal.SizeOf(dest);
    UInt32 status = PowerEnumerate(
        IntPtr.Zero,
        scheme,                               // GUID を指定する場合
        null,                                 // NULL の場合
        POWER_DATA_ACCESSOR.ACCESS_SUBGROUP,  // サブグループの GUID を取得する
        index,
        dest,
        ref size
    );
    
    // ... 必要な処理 ...
}

呼び出し側が随分と楽になりました(・・・が,Data4 の 2バイト目以降を参照する方法が分かりませんでした).

追記

UnmanagedType.ByValArray はクラス内で使用すると無視されてしまいます。配列を使いたいのは山々ですが、調べてみても、どうも使う方法がなさそうであります。仕方がないので

[StructLayout(LayoutKind.Sequential)]
public class Guid
{
    public int Data1;
    public short Data2;
    public short Data3;
    public long Data4;

    public override string ToString()
    {
        byte[] bytes = BitConverter.GetBytes(Data4);

        return String.Format("{0:x8}-{1:x4}-{2:x4}-{3:x2}{4:x2}-{5:x2}{6:x2}{7:x2}{8:x2}{9:x2}{10:x2}",
            Data1, Data2, Data3,
            bytes[0], bytes[1], bytes[2], bytes[3],
            bytes[4], bytes[5], bytes[6], bytes[7]);
    }
}

にします。

http://d.hatena.ne.jp/xcaqhbaj/20110617/1308285988

感想としては,呼び出し側がどういった値を取るのか決まっている場合には ref Guid と IntPtr を場合に応じて使い分ける(必ず値を指定する箇所には ref Guid,必ず NULL の箇所には IntPtr)と言う方法が良いようです.そうではない場合は,状況や各人の好みに応じて上記のような方法を取る必要があります.