最近真面目に作っている WinQuickLook は Win32 API と COM の塊なので、P/Invoke で割とはまりました。
特に文字コード周りが致命的だったので、簡単に内容をまとめておきます。
DllImport のデフォルト文字コードは ANSI
DllImport をデフォルトのまま使うと文字コードが ANSI になるので、Unicode がデフォルトの NT 系 Windows では Unicode <-> ANSI 変換が必要になるので無駄な処理となります。なので、最低でも CharSet.Auto を明示的に指定しましょう。
例としてシェルからファイルの情報を取得する SHGetFileInfo API の P/Invoke 定義を書きます。
SHGetFileInfo function (Windows)
必要な部分だけ MSDN から持ってきました。単純な 1 つの構造体と 1 つの API ですが、NT 系の Windows では ANSI と Unicode 版の両方が実装されています。
typedef struct _SHFILEINFO { HICON hIcon; int iIcon; DWORD dwAttributes; TCHAR szDisplayName[MAX_PATH]; TCHAR szTypeName[80]; } SHFILEINFO; DWORD_PTR SHGetFileInfo( _In_ LPCTSTR pszPath, DWORD dwFileAttributes, _Inout_ SHFILEINFO *psfi, UINT cbFileInfo, UINT uFlags );
ANSI として使う場合は何も考えずに string にマッピングすれば良いですが、ANSI と Unicode の両方で使えるようにするためには、多少 CharSet などの設定を追加する必要があります。
C++ の定義から C# の P/Invoke 定義を作成すると以下のようになります。
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct SHFILEINFO { public IntPtr hIcon; public int iIcon; public uint dwAttributes; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szDisplayName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] public string szTypeName; } [DllImport("shell32.dll", CharSet = CharSet.Auto)] public static extern IntPtr SHGetFileInfo( [In, MarshalAs(UnmanagedType.LPTStr)] string pszPath, uint dwFileAttributes, [In, Out] ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags );
TCHAR の配列は MarshalAs 属性で UnmanagedType.ByValTStr と SizeConst を明示的に指定します。
そして重要なのが StructLayout での CharSet 指定です。当然ながら TCHAR 配列の場合はポインタと異なり、ANSI と Unicode でバイト数が変わってくるので、明示的に Auto を付けます。
STRRET を string に変換すると文字化けする時がある
COM を使って IShellFolder.GetDisplayNameOf メソッドを呼び出して取得した STRRET を StrRetToBuf で string に変換すると、Unicode の合成文字っぽい部分が文字化けしてしまいました。
STRRET は union を使って Unicode と ANSI の両方を返せるようになっているのですが、何故か COM のくせに GetDisplayNameOf で ANSI を返すことがあるみたいです。
更に StrRetToBuf 関数を DllImport のデフォルトで定義すると、先程のように ANSI 版が使われるようなので文字化けが発生することがあるようです。
typedef struct _STRRET { UINT uType; union { LPWSTR pOleStr; UINT uOffset; CHAR cStr[MAX_PATH]; }; } STRRET, *LPSTRRET;
ちなみに OLECHAR は Unicode です。COM では基本的に全て Unicode が使われています。
この STRRET 構造体と StrRetToBuf 関数を C# で定義すると以下のようになります。
[StructLayout(LayoutKind.Explicit, Size = 264)] public struct STRRET { [FieldOffset(0)] public uint uType; [FieldOffset(4)] public IntPtr pOleStr; [FieldOffset(4)] public uint uOffset; [FieldOffset(4)] public IntPtr cStr; } [DllImport("shlwapi.dll", CharSet = CharSet.Auto)] public static extern void StrRetToBuf( [In, Out] ref STRRET pstr, [In] IntPtr pidl, [Out, MarshalAs(UnmanagedType.LPTStr)] StringBuilder pszBuf, uint cchBuf ); [DllImport("shlwapi.dll")] public static extern void StrRetToBSTR( [In, Out] ref STRRET pstr, [In] IntPtr pidl, [Out, MarshalAs(UnmanagedType.BStr)] out string pbstr );
STRRET から文字列に変換する関数として、他にも BSTR に変換してくれる StrRetToBSTR 関数があるので、こっちを使うという手もあります。BSTR も Unicode なので MarshalAs で指定するだけです。