我会在这里详细说明我对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#中的委托),这是命令式语言中通常完成的更高级的命令。
假设我们转换Int
成CInt
双的类型是从
(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的参数传递的函数的调用约定必须与传递参数的函数的调用约定相匹配。换句话说,通过&foo
到bar
要求foo
和bar
具有相同的调用约定。
导出用户数据类型其实是相当简单的。对于需要导出的每种数据类型,必须为此类型创建一个Storable实例。此实例指定GHC为了能够导出/导入此类型而需要的编组信息。除此之外,您需要定义类型的size
和alignment
以及如何读取/写入类型值的指针。我为这个任务部分使用了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
虽然具有多个构造一个数据类型
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
的依赖。这意味着,为Maybe Int
产生Storable
实例时头部看起来像
instance Storable Int => Storable (Maybe Int) where
也就是说,aslong因为有该类型本身也可以导出应用程序的参数可存储实例。
由于Maybe a
被定义为具有多态性参数Just a
,因此在创建结构时,某些类型信息会丢失。该结构将包含一个void*
参数,您必须手动将其转换为正确的类型。我认为替代方案过于繁琐,也是为了创建专业化的结构。例如。 struct MaybeInt。但是可以通过普通模块生成的专用结构的数量可以很快爆炸。 (稍后可能会将此添加为标志)。
为了缓解这种信息丢失,我的工具将导出任何发现该函数的文档作为生成包含中的注释。它也将在注释中放置原始的Haskell类型签名。然后IDE将把它们作为其Intellisense(代码竞争)的一部分呈现出来。
与所有的这些例子中,我为中省略的东西.NET端的代码,如果你有兴趣,你可以只查看Hs2lib输出。
还有一些其他类型需要特殊处理。特别是Lists
和Tuples
。
- 列出需要获得通过数组从马歇尔的大小从,因为我们与其中数组的大小并不隐含已知的非托管语言接口。当我们返回一个列表时,我们还需要返回列表的大小。
元组是特殊构建的类型,为了导出它们,我们必须先将它们映射到“正常”数据类型,然后导出它们。在这个工具中,直到8元组完成。
问题与多态类型e.g. map :: (a -> b) -> [a] -> [b]
是,a
的size
和b
不知道。也就是说,没有办法为参数和返回值保留空间,因为我们不知道它们是什么。我计划通过允许您指定a
和b
的可能值并为这些类型创建专门的包装函数来支持此操作。另一方面,在命令式语言中,我会使用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。
我觉得这几乎涵盖了一切。我爱你们/加尔斯! :) –
非常欢迎你:)如果你有任何问题随时问:) – Phyx