在C#中使用高阶Haskell类型

Ger*_*ger 27 c# haskell ffi

如何使用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
Run Code Online (Sandbox Code Playgroud)

C#中相应的类型签名是什么?

[DllImport ("libHSDLLTest")]
private static extern ??? foo( ??? );
Run Code Online (Sandbox Code Playgroud)

另外(因为它可能更容易):我如何在C#中使用"未知"Haskell类型,所以我至少可以传递它们,而不知道任何特定类型的C#?我需要知道的最重要的功能是传递类型类(如Monad或Arrow).

我已经知道如何将Haskell库编译为DLL并在C#中使用,但仅适用于一阶函数.我也知道Stackoverflow - 在.NET中调用Haskell函数,为什么GHC不能用于.NEThs-dotnet,我没有找到任何文档和样本(用于C#到Haskell的方向).

Phy*_*hyx 18

我将在此详细阐述我对FUZxxl帖子的评论.
您发布的示例都可以使用FFI.使用FFI导出函数后,您已经想到将程序编译为DLL.

.NET的设计旨在能够轻松地与C,C++,COM等进行交互.这意味着一旦您能够将函数编译为DLL,就可以从.NET(相对)轻松地调用它.正如我之前在其他帖子中提到的那样,您已经链接到了,请记住在导出函数时指定的调用约定..NET中的标准是stdcall(大多数)Haskell FFI导出使用的例子ccall.

到目前为止,我发现FFI可以导出的唯一限制是polymorphic types,或者没有完全应用的类型.例如,除了种类以外的任何东西*(你不能导出,Maybe但你可以导出Maybe Int例如).

我编写了一个工具Hs2lib,可以自动覆盖和导出示例中的任何功能.它还可以选择生成unsafeC#代码,使其非常"即插即用".我选择不安全代码的原因是因为它更容易处理指针,这反过来使得更容易对数据结构进行编组.

为了完整,我将详细说明该工具如何处理您的示例以及我计划如何处理多态类型.

  • 高阶函数

导出高阶函数时,需要稍微更改函数.高阶参数需要成为FunPtr的元素.基本上它们被视为显式函数指针(或c#中的委托),这是通常在命令式语言中完成更高阶的有序性.
假设我们转换IntCIntdouble的类型转换而来

(Int -> Int) -> Int -> Int
Run Code Online (Sandbox Code Playgroud)

FunPtr (CInt -> CInt) -> CInt -> IO CInt
Run Code Online (Sandbox Code Playgroud)

这些类型是为包装函数(doubleA在本例中)生成的,而不是double自身导出.包装函数在导出值和原始函数的预期输入值之间进行映射.需要IO是因为构造a FunPtr不是纯粹的操作.
要记住的一件事是,构建或取消引用a的唯一方法FunPtr是静态创建导入,指示GHC为此创建存根.

foreign import stdcall "wrapper" mkFunPtr  :: (Cint -> CInt) -> IO (FunPtr (CInt -> CInt))
foreign import stdcall "dynamic" dynFunPtr :: FunPtr (CInt -> CInt) -> CInt -> CInt
Run Code Online (Sandbox Code Playgroud)

"包装"功能允许我们创建一个FunPtr"动态" FunPtr允许人们尊重的.

在C#中,我们将输入声明为a IntPtr,然后使用Marshaller辅助函数Marshal.GetDelegateForFunctionPointer来创建我们可以调用的函数指针,或者使用反函数来创建IntPtr函数指针.

还要记住,函数的调用约定作为参数传递给FunPtr必须与传递参数的函数的调用约定相匹配.换句话说,传递&foobar需要foobar具有相同的调用约定.

  • 用户数据类型

导出用户数据类型实际上非常简单.对于需要导出的每种数据类型,必须为此类型创建一个可存储的实例.此实例指定GHC需要的编组信息,以便能够导出/导入此类型.除其他事项外,您还需要定义类型sizealignment类型,以及如何读取/写入指针类型的值.我部分使用Hsc2hs执行此任务(因此文件中的C宏).

newtypesdatatypes只使用一个构造函数很容易.这些变成了扁平结构,因为在构造/破坏这些类型时只有一种可能的替代方案.具有多个构造函数的类型成为联合(具有在C#中Layout设置的属性的结构Explicit).但是,我们还需要包含一个枚举来识别正在使用的构造.

一般来说,数据类型Single定义为

data Single = Single  { sint   ::  Int
                      , schar  ::  Char
                      }
Run Code Online (Sandbox Code Playgroud)

创建以下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
Run Code Online (Sandbox Code Playgroud)

和C结构

typedef struct Single Single_t;

struct Single {
     int sint;
     wchar_t schar;
} ;
Run Code Online (Sandbox Code Playgroud)

该函数foo :: Int -> Single将导出为foo :: CInt -> Ptr Single 具有多个构造函数的数据类型

data Multi  = Demi  {  mints    ::  [Int]
                    ,  mstring  ::  String
                    }
            | Semi  {  semi :: [Single]
                    }
Run Code Online (Sandbox Code Playgroud)

生成以下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;
} ;
Run Code Online (Sandbox Code Playgroud)

Storable实例相对简单,应该更容易从C结构定义.

  • 应用类型

我的依赖关系跟踪器会为Maybe Int类型Int和类型的依赖类型发出Maybe.这意味着,在StorableMaybe Int头部生成实例时看起来像

instance Storable Int => Storable (Maybe Int) where
Run Code Online (Sandbox Code Playgroud)

也就是说,只要应用程序的参数有一个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不知道的.也就是说,没有办法为参数保留空间并返回值,因为我们不知道它们是什么.我打算让你为指定的可能值来支持这个ab并为这些类型创建专门的包装函数.在另一个大小上,在命令式语言中,我将用于overloading向用户呈现您选择的类型.

至于类,Haskell的开放世界假设通常是一个问题(例如,可以随时添加实例).但是,在编译时,只有一个静态已知的实例列表可用.我打算提供一个选项,使用这些列表自动导出尽可能多的专用实例.例如,出口(+)出口专用功能的所有已知Num在编译时实例(例如Int,Double等).

该工具也相当信任.由于我无法真正检查代码的纯度,我始终相信程序员是诚实的.例如,您不会将具有副作用的函数传递给期望纯函数的函数.说实话,并将更高阶的论点标记为不可避免的问题.

我希望这会有所帮助,我希望这不会太久.

更新:我最近发现了一些大问题.我们必须记住,.NET中的String类型是不可变的.所以当marshaller将它发送给Haskell代码时,我们得到的CWString是原始的副本.我们必须释放这个.在C#中执行GC时,它不会影响CWString,这是一个副本.

但问题是,当我们在Haskell代码中释放它时,我们不能使用freeCWString.指针未分配C(msvcrt.dll)的alloc.有三种方法(我知道)可以解决这个问题.

  • 在调用Haskell函数时,在C#代码中使用char*而不是String.然后,当您调用return时,您可以指向free,或者使用fixed来初始化函数.
  • 在Haskell中导入CoTaskMemFree并释放Haskell中的指针
  • 使用StringBuilder而不是String.我不完全确定这个,但是我的想法是,因为StringBuilder是作为本机指针实现的,所以Marshaller只是将这个指针传递给你的Haskell代码(也可以通过btw更新它).在调用返回后执行GC时,应释放StringBuilder.