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 - 在 .NET 中调用 Haskell 函数为什么 GHC 不适用于 .NEThs-dotnet,我没有找到任何文档和示例(从 C# 到 Haskell 方向)。

4

3 回答 3

18

我将在这里详细说明我对 FUZxxl 帖子的评论。
您发布的示例都可以使用FFI. 使用 FFI 导出函数后,您可以将程序编译为 DLL。

.NET 的设计目的是能够轻松地与 C、C++、COM 等接口。这意味着一旦您能够将函数编译为 DLL,就可以(相对)轻松地从 .NET 调用它。正如我之前在您链接到的另一篇文章中提到的,请记住您在导出函数时指定的调用约定。.NET 中的标准是 .NET stdcall,而(大多数)HaskellFFI导出示例使用ccall.

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

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

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

  • 高阶函数

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

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

进入

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

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

foreign import stdcall "wrapper" mkFunPtr  :: (Cint -> CInt) -> IO (FunPtr (CInt -> CInt))
foreign import stdcall "dynamic" dynFunPtr :: FunPtr (CInt -> CInt) -> CInt -> CInt

包装器”功能允许我们创建一个FunPtr,而“动态” FunPtr功能允许我们尊重一个。

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

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

  • 用户数据类型

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

newtypes或者datatypes只有一个构造函数很容易。这些成为平面结构,因为在构造/破坏这些类型时只有一种可能的选择。具有多个构造函数的类型成为联合(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;
} ;

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

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类型IntMaybe. 这意味着,当为头部生成Storable实例时,看起来像Maybe Int

instance Storable Int => Storable (Maybe Int) where

也就是说,只要应用程序的参数有一个 Storable 实例,该类型本身也可以被导出。

由于Maybe a被定义为具有多态参数Just a,因此在创建结构时,一些类型信息会丢失。结构将包含一个void*参数,您必须手动将其转换为正确的类型。在我看来,替代方案太麻烦了,那就是创建专门的结构。例如结构MaybeInt。但是,可以从普通模块生成的专用结构的数量会以这种方式迅速爆炸。(稍后可能会将其添加为标志)。

为了减轻这种信息的丢失,我的工具将导出Haddock为该函数找到的任何文档作为生成的包含中的注释。它还将原始的 Haskell 类型签名也放在注释中。然后,IDE 会将这些作为其 Intellisense(代码完成)的一部分呈现。

与所有这些示例一样,我省略了 .NET 方面的代码,如果您对此感兴趣,可以查看Hs2lib的输出。

还有一些其他类型需要特殊处理。特别是ListsTuples

  1. 列表需要传递从中编组的数组的大小,因为我们正在与非托管语言交互,其中数组的大小不是隐式知道的。反之,当我们返回一个列表时,我们也需要返回列表的大小。
  2. 元组是特殊的内置类型,为了导出它们,我们必须首先将它们映射到“普通”数据类型,然后导出它们。在该工具中,直到 8 元组为止。

    • 多态类型

多态类型的问题e.g. map :: (a -> b) -> [a] -> [b]在于sizeofabare 不知道。也就是说,没有办法为参数和返回值保留空间,因为我们不知道它们是什么。我计划通过允许您为这些类型指定可能的值ab为这些类型创建专门的包装函数来支持这一点。另一方面,我将使用命令式语言overloading向用户展示您选择的类型。

至于类,Haskell 的开放世界假设通常是一个问题(例如,可以随时添加实例)。然而,在编译时,只有一个静态已知的实例列表可用。我打算提供一个选项,可以使用这些列表自动导出尽可能多的专用实例。例如 export在编译时(+)为所有已知实例导出一个专门的函数(例如,等)。NumIntDouble

该工具也相当值得信赖。由于我无法真正检查代码的纯度,我始终相信程序员是诚实的。例如,您不会将具有副作用的函数传递给需要纯函数的函数。诚实并将高阶论证标记为不纯,以避免出现问题。

我希望这会有所帮助,我希望这不会太长。

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

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

  • 调用 Haskell 函数时,在 C# 代码中使用 char* 而不是 String。然后,当您调用 return 或使用fixed初始化函数时,您将获得指向 free 的指针。
  • import CoTaskMemFree in Haskell and free the pointer in Haskell
  • use StringBuilder instead of String. I'm not entirely sure about this one, but the idea is that since StringBuilder is implemented as a native pointer, the Marshaller just passes this pointer to your Haskell code (which can also update it btw). When GC is performed after the call returns, the StringBuilder should be freed.
于 2011-07-27T03:29:56.533 回答
4

您是否尝试通过FFI导出函数?这使您可以为函数创建更多 C 风格的接口。我怀疑是否可以直接从 C# 调用 Haskell 函数。有关更多信息,请参阅文档。(上面的链接)。

在做了一些测试之后,我认为一般情况下,通过 FFI 导出高阶函数和带类型参数的函数是不可能的。[需要引用]

于 2011-07-03T11:51:21.660 回答
3

好的,感谢 FUZxxl,这是他为“未知类型”提出的解决方案。将数据存储在 IO 上下文中的 Haskell MVar中,并使用一阶函数从 C# 与 Haskell 通信。这可能是至少对于简单情况的解决方案。

于 2011-07-03T14:18:16.753 回答