C#でUnmanagedな構造体へのポインタを
FFTWのDLLをC#から利用しようとWrapperを書いているときに思いついた。
Cで書かれたDLL内の関数に構造体へのポインタが使われていると、C#でインポートするにはものすごく困ってしまう。たとえばFFTWにでてくるこのような関数だ
fftw_plan fftw_plan_dft_1d(int n, fftw_complex *in, fftw_complex *out, int sign, unsigned flags);
fftw_complex*はdouble[]を使うことで代用できるので解決される。問題はfftw_planという型で、これはfftw_plan_sという構造体へのポインタ型となっている。
Cを良く知っている人ならば型名の後に*をつければいいのでは?と言うだろうが、C#はそうは問屋がおろさない。C#ではunsafeという括弧をつけたブロック内では*が付いたポインタを確かに利用することができる。しかし、それはbyteやdoubleなどの元からある値型に限られ、自分で書いた構造体に対してはコンパイルが通らない。
IntPtrを用いる
そこで妥協案として、普通ポインタの部分はIntPtrというポインタを記憶する型を当てることが多い。すなわちC#側では
[DllImport("libfftw3-3.dll", EntryPoint = "fftw_plan_dft_1d")] public static extern IntPtr PlanDFT1D(int Length, double[] In, double[] Out, int Sign, uint Flags);
確かにこれでコンパイルは通る。。。しかし、IntPtrが指すメモリ領域のデータを読み書きするには
[StructLayout(LayoutKind.Sequential)] struct plan_s { public IntPtr adt; public opcnt ops; public double pcost; public wakefulness wakefulness; /* used for debugging only */ public int could_prune_now_p; }
などと構造体を宣言してMarshalクラスを毎回呼んでデータを読み書きをするかGCHandleを使うかなどしないといけない。
改善策はどうやら二つあるようだ。
同じ構造を持つクラスを用意する
1つ目の方法は構造体と同じメンバーと構造を持つクラスを書いてあげる。
たとえば先ほどのplan_sならば
[StructLayout(LayoutKind.Sequential)] class plan { public IntPtr adt; public opcnt ops; public double pcost; public wakefulness wakefulness; /* used for debugging only */ public int could_prune_now_p; }
とする。そしてインポート部は
[DllImport("libfftw3-3.dll", EntryPoint = "fftw_plan_dft_1d")] public static extern plan PlanDFT1D(int Length, double[] In, double[] Out, int Sign, uint Flags);
とする。これでうまくいくようだ。そこでさらに内部の変数IntPtr adtに対しても同じ事を行ったのだが、AccessViolationExceptionが発生してしまう・・・一体なぜ??
よくわからないのでこの方法は諦めることにした。
構造体へのポインタ型を作る汎用型を作る
IntPtrを拡張した値型を作れないだろうかとひらめいた。つまりこのような汎用型を作る。
[StructLayout(LayoutKind.Sequential)] struct IntPtr<Structure> where Structure : struct { public readonly IntPtr Pointer; public Structure Value { get { return (Structure)Marshal.PtrToStructure(this.Pointer, typeof(Structure)); } set { Marshal.StructureToPtr(value, this.Pointer, true); } } }
そしてインポートは
[DllImport("libfftw3-3.dll", EntryPoint = "fftw_plan_dft_1d")] public static extern IntPtr<plan_s> PlanDFT1D(int Length, double[] In, double[] Out, int Sign, uint Flags);
とすれば戻り値の型は値型plan_sへのポインタを記録する値型になる!(面白いのはポインタなのに値型になっている事。よく考えれば、ポインタと言うのは所詮数値なのだから実体は値型なのである。)
これを使ってFFTWをC#から使うことができた。落ち着いたらここに書くかもしれない。
ちなみにこの型名をつけるのに悩んだ(笑) Pointer<>だと長いし、p<>だとなんかC#っぽくないし、_<>だとC++/CLIのかの失敗例のようにかっこ悪いし…と悩みぬいた挙句ベースのIntPtrをパクルことにした。
ただ、やっぱりclassを使ってかけないのだろうかと思ってしまう。そのほうがスマートだと思うから…