SendInputをAnyCpuビルド(C#)で使おうとするとハマる件

 構造体/共用体のアライメントの問題で動かない。

元(Win32API)の定義:

typedef struct tagINPUT {
    DWORD   type;

    union
    {
        MOUSEINPUT      mi;
        KEYBDINPUT      ki;
        HARDWAREINPUT   hi;
    };
} INPUT, *PINPUT, FAR* LPINPUT;

良く見かける(C#の)サンプル定義:

[StructLayout(LayoutKind.Explicit)]
private struct INPUT
{
    [FieldOffset(0)] public int type;
    [FieldOffset(4)] public MOUSEINPUT mi;
    [FieldOffset(4)] public KEYBDINPUT ki;
    [FieldOffset(4)] public HARDWAREINPUT hi;
};

 これをAnyCpuビルドで構築すると、x64環境では正常動作しない。なぜならば、「struct tagINPUT」はpackedではないので、x64環境ではunion部分のオフセットは「8」になる。FieldOffset(4)→FieldOffset(8)にすれば、x64環境で動作するがx86では動作しなくなる。(x86では試してはいないけど)

 x86ビルドにするのが現実的な解決方法だと思う。AnyCpuはリスクの割にメリットがない。ただし「x64環境では大容量メモリ(2G超)を利用したいし、x86/x64を別々にビルドしたくない」という需要もある。(だからClickShotはAnyCpuに戻した)

 色々と調べた結果、以下の定義で使うのが良さそうだった。

[StructLayout(LayoutKind.Sequential)]
private struct INPUT
{
    public int type;
    public INPUT_UNION u;
};

[StructLayout(LayoutKind.Explicit)]
private struct INPUT_UNION
{
    [FieldOffset(0)] public MOUSEINPUT mi;
    [FieldOffset(0)] public KEYBDINPUT ki;
    [FieldOffset(0)] public HARDWAREINPUT hi;
};

 「xxx.mi.dwFlags = ~」としていた所を「xxx.u.mi.dwFlags = ~」にする必要があるが、悪くないと思う。
 他の方法としては、x86/x64で別々の定義(INPUT_x64とか)を作成し、実行時に使い分ける方法もあった。この場合は、そのまま使うと「if(IntPtr.Size==4)~」が多発して可読性が低下するので、もう一段ラップクラスを用意しないと使えないと思う。

 あと、基本的な事としてint(Int32)とIntPtrの区別ができている等の基本はちゃんと押える事、この辺り適当なサンプルは多い。(自分も人の事はいえないが・・・)

この記事へのコメント

VSさん
2017年10月31日 20:20
Visual Studio 2017ですけど、VC++でpackを特に指定せずにx86とx64両方で構造体をsizeofしたり各構造体メンバーのアドレスを取ってみたりしましたが、x64にしたからと言ってpack(8)になることはなかったですよ?
ありがとう
2018年10月12日 00:53
助かりました。VS2013ですがこれで解決しました…。

この記事へのトラックバック