size_tのマーシャリング

以下のようなCの構造体について、P/InvokeのためにC#で同等の構造体を定義するとします。

typedef struct Hoge
{
    size_t size;
    int x;
}Hoge;

32bit環境に限れば、size_t は普通 unsigned int ですから、C#では以下のようになります。

[StructLayout(LayoutKind.Sequential)]
struct Hoge
{
    public uint size;
    public int x;
} 

しかし、もし64bit環境なら、size_t は64bit値です。だいたい以下のような定義になっています。

typedef unsigned __int64 size_t;  // Visual C++
typedef unsigned long size_t;     // gcc

すなわちC#でいうところの ulong です。

よって、上記のC#のHoge構造体は64bit環境では問題となります。64bitではsizeより後にあるフィールドのアドレスがすべてずれていくので、ひどいことになります。この解決策をいくつか考えてみます。

1. 条件コンパイル

C#でも #if のようなプリプロセッサが使えるので、これを使ってsize_tのところの定義を2通り用意します。

なお、C#では32bitか64bitかを判定できるようなマクロは定義されていないようですので、プロジェクトのプロパティか若しくはコンパイラ(csc.exe)に渡す引数で、自分で"X64"のようなシンボルを定義しておきます。

これでx64プラットフォームの時は X64 というシンボルが定義されます。これを利用し以下のような構造体の定義とします。

[StructLayout(LayoutKind.Sequential)]
struct Hoge
{
#if X64
    public ulong size;
#else
    public uint size;
#endif
    public int x;
} 

.NET Frameworkでプラットフォーム依存のコードを書くのはどうかとも思いますが、そもそもP/Invokeで外部のネイティブDLLを呼びだしている以上、いくらかプラットフォーム依存になるのは避けられません。そのDLLが32bitのものであれば.NETでもx86として呼びだすしかありません。よってやむを得ないと思います。

2. IntPtrとして定義する

よりスマートな解決ができる方法です。(少なくともVC++gccでは)32bit・64bitどちらの環境でも、sizeof(size_t) は sizeof(void*)、すなわちポインタのサイズと同じです。(違う環境ってあるんでしょうか?あったら教えてください。)

これを利用し、IntPtrでフィールドを定義します。IntPtrはC#でのintやcharのように環境によらず固定のサイズではなく、C/C++のように環境によりサイズが変化する構造体ですから、まさにC#版size_tのように扱えてしまいます。

[StructLayout(LayoutKind.Sequential)]
struct Hoge
{
    public IntPtr size;
    public int x;
} 

IntPtrはsize_tのようなそのままの数値ではなく「ポインタ」ですが、結局ポインタもメモリ内の番地を表す数値に変わりはありませんので変換はすぐにできます。具体的にはToInt32ToInt64メソッドを使うことでintやlongの値が得られます。

x86とx64でプラットフォームにより処理を分けたい際は、IntPtr.Sizeでポインタサイズを取得できるので、これで条件分けします。

void Foo(Hoge hoge)
{
    // 32bit
    if(IntPtr.Size == 4)
    {
        uint size = (uint)hoge.size.ToInt32();
        .....
    }
    // 64bit (IntPtr.Size == 8)
    else  
    {
        ulong size = (ulong)hoge.size.ToInt64();
        .....
    }
}


リファレンス: