2011-07-03 41 views
27

如何使用和调用从C#(dllimport的),如高阶类型签名Haskell函数...使用高阶的Haskell类型在C#

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#中的“未知”哈斯克尔的类型,所以我至少可以通过在他们身边,不C#知道任何特定类型的?我需要知道的最重要的功能是传递类型类(如Monad或Arrow)。

我已经知道how to compile a Haskell library to DLL并在C#中使用,但仅限于一阶函数。我也知道Stackoverflow - Call a Haskell function in .NET,Why isn't GHC available for .NEThs-dotnet,其中我没有找到任何文档和示例(对于C#到Haskell方向)。

回答

18

我会在这里详细说明我对FUZxxl的帖子的评论。
您发布的示例都可以使用FFI。一旦你使用FFI导出你的函数,你可以像你已经想出将该程序编译成DLL一样。 NET的设计意图是能够轻松地与C,C++,COM等接口。这意味着,一旦你能够将你的函数编译成一个DLL,你可以称它(相对)简单来自.NET。正如我之前在其他文章中提到的那样,您已经链接到了,请记住您在导出函数时指定了哪种调用约定。 .NET中的标准是stdcall,而使用ccall导出的Haskell FFI(大多数)示例。

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

我已经编写了一个工具Hs2lib,可以自动覆盖和导出示例中的任何函数。它也可以生成unsafe C#代码,这使得它几乎可以“即插即用”。我选择不安全的代码的原因是因为处理指针更容易,这反过来又使得对数据结构进行编组更容易。

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

  • 高阶函数

当输出高阶函数,需要稍微改变的功能。高阶参数需要成为FunPtr的元素。基本上它们被视为显式函数指针(或c#中的委托),这是命令式语言中通常完成的更高级的命令。
假设我们转换IntCInt双的类型是从

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

转化到

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

这些类型为(在此情况下doubleA)包装函数产生,其代替导出的double本身。包装函数映射导出的值和原始函数的预期输入值之间的映射关系。 IO是必需的,因为构建FunPtr不是纯粹的操作。
有一点要记住的是,构建或取消引用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#我们声明输入作为IntPtr,然后使用Marshaller辅助函数Marshal.GetDelegateForFunctionPointer创建一个函数指针,我们可以调用,或反函数来创建从一个函数指针一个IntPtr

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

  • 用户数据类型

导出用户数据类型其实是相当简单的。对于需要导出的每种数据类型,必须为此类型创建一个Storable实例。此实例指定GHC为了能够导出/导入此类型而需要的编组信息。除此之外,您需要定义类型的sizealignment以及如何读取/写入类型值的指针。我为这个任务部分使用了Hsc2hs(因此文件中的C宏)。

newtypesdatatypes只用一个构造很容易。这些变成了一个扁平的结构,因为在构造/破坏这些类型时只有一种可能的选择。具有多个构造函数的类型变为联合(在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 虽然具有多个构造一个数据类型

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的依赖。这意味着,为Maybe Int产生Storable实例时头部看起来像

instance Storable Int => Storable (Maybe Int) where 

也就是说,aslong因为有该类型本身也可以导出应用程序的参数可存储实例。

由于Maybe a被定义为具有多态性参数Just a,因此在创建结构时,某些类型信息会丢失。该结构将包含一个void*参数,您必须手动将其转换为正确的类型。我认为替代方案过于繁琐,也是为了创建专业化的结构。例如。 struct MaybeInt。但是可以通过普通模块生成的专用结构的数量可以很快爆炸。 (稍后可能会将此添加为标志)。

为了缓解这种信息丢失,我的工具将导出任何发现该函数的文档作为生成包含中的注释。它也将在注释中放置原始的Haskell类型签名。然后IDE将把它们作为其Intellisense(代码竞争)的一部分呈现出来。

与所有的这些例子中,我为中省略的东西.NET端的代码,如果你有兴趣,你可以只查看Hs2lib输出。

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

  1. 列出需要获得通过数组从马歇尔的大小从,因为我们与其中数组的大小并不隐含已知的非托管语言接口。当我们返回一个列表时,我们还需要返回列表的大小。
  2. 元组是特殊构建的类型,为了导出它们,我们必须先将它们映射到“正常”数据类型,然后导出它们。在这个工具中,直到8元组完成。

    • 态类型

问题与多态类型e.g. map :: (a -> b) -> [a] -> [b]是,asizeb不知道。也就是说,没有办法为参数和返回值保留空间,因为我们不知道它们是什么。我计划通过允许您指定ab的可能值并为这些类型创建专门的包装函数来支持此操作。另一方面,在命令式语言中,我会使用overloading来向用户展示您选择的类型。对于类,Haskell的开放世界假设通常是一个问题(例如,一个实例可以随时添加)。但是,在编译时,只有一个静态已知的实例列表可用。我打算提供一个选项,可以使用这些列表自动导出尽可能多的专用实例。例如export (+)在编译时为所有已知的Num实例导出专用函数(例如,Int,Double等)。

该工具也相当值得信赖。由于我不能真正检查代码的纯度,我总是相信程序员是诚实的。例如。你不会将一个具有副作用的函数传递给一个需要纯函数的函数。坦白地说,把这个更高级的论点标记为不纯以避免问题。

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

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

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

  • 调用Haskell函数时,在C#代码中使用char *而不是String。当您致电退货时,您可以使指针自由,或使用fixed初始化该功能。
  • 在Haskell中导入CoTaskMemFree并释放Haskell中的指针
  • 使用StringBuilder而不是String。我不完全知道这一个,但这个想法是,既然StringBuilder的是作为本机的指针来实现,现Marshaller只是通过这个指针到您的Haskell代码(也可顺便说一句更新)。在调用返回后执行GC时,应该释放StringBuilder。
+0

我觉得这几乎涵盖了一切。我爱你们/加尔斯! :) –

+0

非常欢迎你:)如果你有任何问题随时问:) – Phyx

4

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

经过一些测试后,我认为通常不可能通过FFI输出带有类型参数的高阶函数和函数。 [引文需要]

+0

这绝对是可能的,因为 - 如在帖子中提到的 - 我做的了,但只用一阶功能。所有的教程和例子也限于一阶函数。 –

+0

@lambdor我误解你的问题。您链接到博客中已经使用了外贸出口结构。如果您看到我提供的链接,您将会看到,GHC会生成一个包含那些导出函数原型的存根'.c'-File。您可以尝试使用高阶类型进行一些实验。 – fuz

+0

@lambdor经过一些测试后,FFI似乎只允许“简单”类型。即:没有类型变量,没有高阶函数,没有ADT。 – fuz

3

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