27

次のように、C# (DLLImport) の高次型シグネチャを使用して Haskell 関数を使用および呼び出すにはどうすればよいですか?

double :: (Int -> Int) -> Int -> Int -- higher order function

typeClassFunc :: ... -> Maybe Int    -- type classes

data MyData = Foo | Bar              -- user data type
dataFunc :: ... -> MyData

C#で対応する型シグネチャは何ですか?

[DllImport ("libHSDLLTest")]
private static extern ??? foo( ??? );

さらに(簡単かもしれないので):C#内で「未知の」Haskell型を使用するにはどうすればよいですか?C#が特定の型を知らなくても、少なくともそれらを渡すことができますか?私が知っておくべき最も重要な機能は、型クラス (Monad や Arrow など) を渡すことです。

Haskell ライブラリを DLL にコンパイルしてC# 内で使用する方法は既に知っていますが、一次関数についてのみです。Stackoverflow - Call a Haskell function in .NETWhy isn't GHC available for .NET and hs-dotnetについても認識しています。ここでは、ドキュメントとサンプルが見つかりませんでした (C# から Haskell 方向)。

4

3 に答える 3

18

ここで、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 の型に変換する
と仮定すると、から変換されますIntCInt

(Int -> Int) -> Int -> Int

の中へ

FunPtr (CInt -> CInt) -> CInt -> IO CInt

これらの型は、それ自体doubleAではなくエクスポートされるラッパー関数 (この場合)用に生成されdoubleます。ラッパー関数は、エクスポートされた値と元の関数の予期される入力値との間でマップします。FunPtra の構築は純粋な操作ではないため、IO が必要です。
覚えておくべきことの 1 つは、a を構築または逆参照する唯一の方法は、FunPtrGHC にこのためのスタブを作成するように指示する 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 として宣言しIntPtrMarshallerヘルパー関数Marshal.GetDelegateForFunctionPointerを使用して呼び出し可能な関数ポインターを作成するか、逆関数を使用しIntPtrて関数ポインターから a を作成します。

また、FunPtr に引数として渡される関数の呼び出し規則は、引数が渡される関数の呼び出し規則と一致する必要があることに注意してください。つまり、 に渡す&fooには、同じ呼び出し規則がbar必要です。foobar

  • ユーザーのデータ型

ユーザーデータ型のエクスポートは、実際には非常に簡単です。エクスポートする必要があるすべてのデータ型に対して、この型のStorableインスタンスを作成する必要があります。このインスタンスは、GHC がこの型をエクスポート/インポートできるようにするために必要なマーシャリング情報を指定します。とりわけ、型の値をポインターに読み書きする方法とともに、型のsizeandを定義する必要があります。alignmentこのタスクには部分的にHsc2hsを使用します (したがって、ファイル内の C マクロ)。

newtypesまたはコンストラクターが1 つdatatypesだけの場合は簡単です。これらの型を構築/破棄するときに可能な代替手段が 1 つしかないため、これらはフラットな構造体になります。複数のコンストラクターを持つ型は共用体 ( C#で属性が設定された構造体) になります。ただし、どの構造が使用されているかを識別するために列挙型も含める必要があります。LayoutExplicit

一般に、データ型は次のように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;
} ;

インスタンスは比較的単純で、StorableC 構造体定義から簡単に従う必要があります。

  • 応用タイプ

私の依存関係トレーサーは、タイプとMaybe Intタイプの両方への依存関係を出力します。これは、頭のインスタンスを生成すると、次のようになることを意味しますIntMaybeStorableMaybe Int

instance Storable Int => Storable (Maybe Int) where

つまり、アプリケーションの引数に Storable インスタンスがある限り、型自体もエクスポートできます。

Maybe aはポリモーフィックな引数を持つと定義されているためJust a、構造体を作成すると、一部の型情報が失われます。構造体には引数が含まれてvoid*おり、手動で正しい型に変換する必要があります。私の意見では、特殊な構造体も作成するという別の方法は面倒でした。例: struct MaybeInt. しかし、通常のモジュールから生成できる特殊な構造の量は、この方法で急速に爆発する可能性があります。(後でこれをフラグとして追加する可能性があります)。

この情報の損失を軽減するために、このツールはHaddock、生成されたインクルードのコメントとして、関数で見つかったすべてのドキュメントをエクスポートします。また、元の Haskell タイプの署名もコメントに配置されます。IDE は、これらを Intellisense (コード補完) の一部として提示します。

これらすべての例と同様に、.NET 側のコードは省略しました。興味がある場合は、Hs2libの出力を表示してください。

特別な処理が必要なタイプが他にもいくつかあります。特にListsTuples.

  1. 配列のサイズが暗黙のうちにわからないアンマネージ言語とやり取りしているため、マーシャリング元の配列のサイズをリストに渡す必要があります。逆に、リストを返すときは、リストのサイズも返す必要があります。
  2. タプルは特殊な組み込み型です。それらをエクスポートするには、最初にそれらを「通常の」データ型にマップし、それらをエクスポートする必要があります。ツールでは、これは 8 タプルまで行われます。

    • 多形型

ポリモーフィック型の問題e.g. map :: (a -> b) -> [a] -> [b]sizeabが不明であることです。つまり、引数と戻り値が何であるかがわからないため、これらのスペースを予約する方法はありません。a可能な値を指定しb、これらの型に特化したラッパー関数を作成できるようにすることで、これをサポートする予定です。もう一方のサイズでは、overloading選択した型をユーザーに提示するために使用する命令型言語です。

クラスに関しては、通常、Haskell のオープン ワールドの前提が問題になります (たとえば、インスタンスはいつでも追加できます)。ただし、コンパイル時には、静的に既知のインスタンスのリストのみが利用可能です。これらのリストを使用して、できるだけ多くの特殊なインスタンスを自動的にエクスポートするオプションを提供するつもりです。たとえば exportは、コンパイル時にすべての既知のインスタンス (たとえば、など)(+)の特殊な関数をエクスポートします。NumIntDouble

このツールはまた、かなり信頼しています。コードの純粋性を実際に検査することはできないので、プログラマーが正直であると常に信じています。たとえば、純粋な関数を期待する関数に副作用のある関数を渡さないでください。問題を避けるために、正直に言って、高次の議論を不純なものとしてマークしてください。

これがお役に立てば幸いです。これが長すぎなかったことを願っています。

更新: 最近発見した大きな落とし穴があります。.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 が解放されます。
于 2011-07-27T03:29:56.533 に答える
4

FFI経由で関数をエクスポートしようとしましたか? これにより、関数へのより C っぽいインターフェイスを作成できます。Haskell 関数を C# から直接呼び出すことができるとは思えません。詳細については、ドキュメントを参照してください。(上記のリンク)。

いくつかのテストを行った後、FFI を介して高次関数と型パラメーターを持つ関数をエクスポートすることは一般的に不可能であると思います。[引用が必要]

于 2011-07-03T11:51:21.660 に答える
3

わかりました、FUZxxl のおかげで、彼は「未知の型」に対して思いついたソリューションです。IO コンテキスト内のHaskell MVarにデータを格納し、一次関数を使用して C# から Haskell に通信します。これは、少なくとも単純な状況では解決策になる場合があります。

于 2011-07-03T14:18:16.753 に答える