ここで、FUZxxl の投稿に対する私のコメントについて詳しく説明します。
あなたが投稿した例はすべてを使用して可能FFI
です。FFI を使用して関数をエクスポートしたら、プログラムを DLL にコンパイルすることが既にわかっているようにできます。
.NET は、C、C++、COM などと簡単にインターフェイスできるように設計されています。これは、関数を DLL にコンパイルできれば、.NET から (比較的) 簡単に呼び出すことができることを意味します。リンク先の他の投稿で前に述べたように、関数をエクスポートするときに指定する呼び出し規約に注意してください。.NET の標準は ですがstdcall
、(ほとんどの) Haskell の例はFFI
を使用してエクスポートしccall
ます。
これまでのところ、FFI によってエクスポートできるものについて私が見つけた唯一の制限はpolymorphic types
、または完全に適用されていないタイプです。たとえば、種類以外のもの*
(エクスポートはできませんMaybe
が、たとえばエクスポートできMaybe Int
ます)。
あなたの例にある関数を自動的にカバーしてエクスポートするツールHs2libを作成しました。unsafe
また、ほとんど「プラグ アンド プレイ」になる C# コードを生成するオプションもあります。私がアンセーフ コードを選択した理由は、ポインタの処理が容易であり、データ構造のマーシャリングが容易になるからです。
完全にするために、ツールがあなたの例をどのように処理するか、およびポリモーフィック型の処理をどのように計画するかについて詳しく説明します。
高次関数をエクスポートする場合、関数を少し変更する必要があります。高次の引数はFunPtrの要素になる必要があります。基本的に、それらは明示的な関数ポインター (または C# のデリゲート) として扱われます。これは、命令型言語でより高い順序性が通常どのように行われるかです。double の型に変換する
と仮定すると、から変換されますInt
CInt
(Int -> Int) -> Int -> Int
の中へ
FunPtr (CInt -> CInt) -> CInt -> IO CInt
これらの型は、それ自体doubleA
ではなくエクスポートされるラッパー関数 (この場合)用に生成されdouble
ます。ラッパー関数は、エクスポートされた値と元の関数の予期される入力値との間でマップします。FunPtr
a の構築は純粋な操作ではないため、IO が必要です。
覚えておくべきことの 1 つは、a を構築または逆参照する唯一の方法は、FunPtr
GHC にこのためのスタブを作成するように指示する import を静的に作成することです。
foreign import stdcall "wrapper" mkFunPtr :: (Cint -> CInt) -> IO (FunPtr (CInt -> CInt))
foreign import stdcall "dynamic" dynFunPtr :: FunPtr (CInt -> CInt) -> CInt -> CInt
「ラッパー」関数を使用すると を作成できFunPtr
、「ダイナミック」 FunPtr
関数を使用すると、 1 つを参照できます。
C# では、入力を a として宣言しIntPtr
、Marshaller
ヘルパー関数Marshal.GetDelegateForFunctionPointerを使用して呼び出し可能な関数ポインターを作成するか、逆関数を使用しIntPtr
て関数ポインターから a を作成します。
また、FunPtr に引数として渡される関数の呼び出し規則は、引数が渡される関数の呼び出し規則と一致する必要があることに注意してください。つまり、 に渡す&foo
には、同じ呼び出し規則がbar
必要です。foo
bar
ユーザーデータ型のエクスポートは、実際には非常に簡単です。エクスポートする必要があるすべてのデータ型に対して、この型のStorableインスタンスを作成する必要があります。このインスタンスは、GHC がこの型をエクスポート/インポートできるようにするために必要なマーシャリング情報を指定します。とりわけ、型の値をポインターに読み書きする方法とともに、型のsize
andを定義する必要があります。alignment
このタスクには部分的にHsc2hsを使用します (したがって、ファイル内の C マクロ)。
newtypes
またはコンストラクターが1 つdatatypes
だけの場合は簡単です。これらの型を構築/破棄するときに可能な代替手段が 1 つしかないため、これらはフラットな構造体になります。複数のコンストラクターを持つ型は共用体 ( C#で属性が設定された構造体) になります。ただし、どの構造が使用されているかを識別するために列挙型も含める必要があります。Layout
Explicit
一般に、データ型は次のようにSingle
定義されます。
data Single = Single { sint :: Int
, schar :: Char
}
Storable
次のインスタンスを作成します
instance Storable Single where
sizeOf _ = 8
alignment _ = #alignment Single_t
poke ptr (Single a1 a2) = do
a1x <- toNative a1 :: IO CInt
(#poke Single_t, sint) ptr a1x
a2x <- toNative a2 :: IO CWchar
(#poke Single_t, schar) ptr a2x
peek ptr = do
a1' <- (#peek Single_t, sint) ptr :: IO CInt
a2' <- (#peek Single_t, schar) ptr :: IO CWchar
x1 <- fromNative a1' :: IO Int
x2 <- fromNative a2' :: IO Char
return $ Single x1 x2
そしてC構造体
typedef struct Single Single_t;
struct Single {
int sint;
wchar_t schar;
} ;
関数は
、複数のコンストラクタを持つデータ型の Whilefoo :: Int -> Single
としてエクスポートされますfoo :: CInt -> Ptr Single
data Multi = Demi { mints :: [Int]
, mstring :: String
}
| Semi { semi :: [Single]
}
次の C コードを生成します。
enum ListMulti {cMultiDemi, cMultiSemi};
typedef struct Multi Multi_t;
typedef struct Demi Demi_t;
typedef struct Semi Semi_t;
struct Multi {
enum ListMulti tag;
union MultiUnion* elt;
} ;
struct Demi {
int* mints;
int mints_Size;
wchar_t* mstring;
} ;
struct Semi {
Single_t** semi;
int semi_Size;
} ;
union MultiUnion {
struct Demi var_Demi;
struct Semi var_Semi;
} ;
インスタンスは比較的単純で、Storable
C 構造体定義から簡単に従う必要があります。
私の依存関係トレーサーは、タイプとMaybe Int
タイプの両方への依存関係を出力します。これは、頭のインスタンスを生成すると、次のようになることを意味しますInt
Maybe
Storable
Maybe Int
instance Storable Int => Storable (Maybe Int) where
つまり、アプリケーションの引数に Storable インスタンスがある限り、型自体もエクスポートできます。
Maybe a
はポリモーフィックな引数を持つと定義されているためJust a
、構造体を作成すると、一部の型情報が失われます。構造体には引数が含まれてvoid*
おり、手動で正しい型に変換する必要があります。私の意見では、特殊な構造体も作成するという別の方法は面倒でした。例: struct MaybeInt. しかし、通常のモジュールから生成できる特殊な構造の量は、この方法で急速に爆発する可能性があります。(後でこれをフラグとして追加する可能性があります)。
この情報の損失を軽減するために、このツールはHaddock
、生成されたインクルードのコメントとして、関数で見つかったすべてのドキュメントをエクスポートします。また、元の Haskell タイプの署名もコメントに配置されます。IDE は、これらを Intellisense (コード補完) の一部として提示します。
これらすべての例と同様に、.NET 側のコードは省略しました。興味がある場合は、Hs2libの出力を表示してください。
特別な処理が必要なタイプが他にもいくつかあります。特にLists
とTuples
.
- 配列のサイズが暗黙のうちにわからないアンマネージ言語とやり取りしているため、マーシャリング元の配列のサイズをリストに渡す必要があります。逆に、リストを返すときは、リストのサイズも返す必要があります。
タプルは特殊な組み込み型です。それらをエクスポートするには、最初にそれらを「通常の」データ型にマップし、それらをエクスポートする必要があります。ツールでは、これは 8 タプルまで行われます。
ポリモーフィック型の問題e.g. map :: (a -> b) -> [a] -> [b]
はsize
、a
とb
が不明であることです。つまり、引数と戻り値が何であるかがわからないため、これらのスペースを予約する方法はありません。a
可能な値を指定しb
、これらの型に特化したラッパー関数を作成できるようにすることで、これをサポートする予定です。もう一方のサイズでは、overloading
選択した型をユーザーに提示するために使用する命令型言語です。
クラスに関しては、通常、Haskell のオープン ワールドの前提が問題になります (たとえば、インスタンスはいつでも追加できます)。ただし、コンパイル時には、静的に既知のインスタンスのリストのみが利用可能です。これらのリストを使用して、できるだけ多くの特殊なインスタンスを自動的にエクスポートするオプションを提供するつもりです。たとえば exportは、コンパイル時にすべての既知のインスタンス (たとえば、など)(+)
の特殊な関数をエクスポートします。Num
Int
Double
このツールはまた、かなり信頼しています。コードの純粋性を実際に検査することはできないので、プログラマーが正直であると常に信じています。たとえば、純粋な関数を期待する関数に副作用のある関数を渡さないでください。問題を避けるために、正直に言って、高次の議論を不純なものとしてマークしてください。
これがお役に立てば幸いです。これが長すぎなかったことを願っています。
更新: 最近発見した大きな落とし穴があります。.NET の String 型は不変であることを覚えておく必要があります。したがって、マーシャラーがそれを Haskell コードに送信すると、そこで得られる CWString は元のコピーです。これを解放しなければなりません。GC が C# で実行される場合、コピーである CWString には影響しません。
ただし問題は、Haskell コードでこれを解放すると、freeCWString を使用できないことです。ポインターは、C (msvcrt.dll) の割り当てでは割り当てられませんでした。これを解決するには、(私が知っている) 3 つの方法があります。
- Haskell 関数を呼び出すときは、C# コードで String の代わりに char* を使用します。次に、 return を呼び出すときに free へのポインターを取得するか、fixedを使用して関数を初期化します。
- Haskell でCoTaskMemFreeをインポートし、Haskell でポインターを解放します
- String の代わりに StringBuilder を使用します。これについては完全にはわかりませんが、StringBuilder はネイティブ ポインターとして実装されているため、マーシャラーはこのポインターを Haskell コードに渡すだけです (更新も可能です)。呼び出しが返された後に GC が実行されると、StringBuilder が解放されます。