我将在FUZxxl的帖子中详细阐述我的评论。
您发布的示例都可以使用
FFI
实现。一旦使用FFI导出函数,您就可以像已经想到的那样将程序编译为DLL。
.NET的设计初衷是能够轻松地与C、C++、COM等进行接口交互。这意味着一旦您能够将函数编译为DLL,您就可以(相对)容易地从.NET调用它。正如我之前在您链接的另一篇文章中提到的那样,请记住在导出函数时指定的调用约定。.NET的标准是
stdcall
,而Haskell
FFI
的大多数示例使用
ccall
。
到目前为止,我发现FFI导出的唯一限制是多态类型或未完全应用的类型。例如,除了kind
*
以外的任何内容(您无法导出
Maybe
,但您可以导出
Maybe Int
)。
我编写了一个
Hs2lib工具,可以自动覆盖和导出您示例中的任何函数。它还具有生成
unsafe
C#代码的选项,这使得它几乎可以“即插即用”。我选择使用不安全的代码是因为它更容易处理指针,从而使数据结构的编组更容易。
为了完整起见,我将详细介绍该工具如何处理您的示例以及我计划如何处理多态类型。
在导出高阶函数时,函数需要略作更改。高阶参数需要成为 FunPtr 的元素。基本上它们被视为显式函数指针(或 c# 中的委托),这是典型的命令式语言中处理高阶函数的方式。
假设我们将 Int
转换为 CInt
,则 double 类型会发生变化。
(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
"wrapper" 函数允许我们创建一个 FunPtr
,而 "dynamic" 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
那就意味着只要应用程序参数有可存储实例,类型本身也可以导出。由于
Maybe a
被定义为具有多态参数的
Just a
,因此在创建结构体时会丢失某些类型信息。结构体将包含一个
void*
参数,您必须手动将其转换为正确的类型。在我看来,另一种方法太过繁琐,这是创建专门的结构体,例如 struct MaybeInt。但是,从普通模块生成的专用结构的数量可能会迅速增加(稍后可能会将其作为标志添加)。为了简化这种信息损失,我的工具将导出在函数中找到的任何
Haddock
文档作为生成的包含文件中的注释。它还将在注释中放置原始的 Haskell 类型签名。然后 IDE 将其作为其智能感知的一部分呈现(代码完成)。与所有这些示例一样,我忽略了 .NET 方面的代码,如果您对此感兴趣,可以查看
Hs2lib 的输出。还有一些其他需要特殊处理的类型,特别是列表和元组。
- 由于我们要与不受管理的语言进行接口交互,而数组的大小在这些语言中并不是隐含已知的,因此列表需要传递从中编组的数组的大小。同样地,当我们返回列表时,还需要返回列表的大小。
元组是特殊的内置类型,为了导出它们,我们必须先将它们映射到“普通”数据类型,并导出这些类型。在该工具中,这一点一直持续到8元组。
多态类型的问题在于,例如map :: (a -> b) -> [a] -> [b],无法确定a和b的大小。也就是说,由于我们不知道它们是什么,因此无法为参数和返回值预留空间。我计划通过允许您为a和b指定可能的值并为这些类型创建专门的包装函数来支持此功能。 另一方面,在命令式语言中,我将使用重载来向用户呈现您选择的类型。
关于类,Haskell的开放世界假设通常是一个问题(例如,可以随时添加实例)。但是,在编译时只有一个静态已知的实例列表可用。我打算提供一个选项,该选项将使用这些列表自动导出尽可能多的专门实例。例如,导出
(+)
在编译时为所有已知的
Num
实例导出一个专门的函数(例如
Int
,
Double
等)。
该工具也相当信任。由于我无法真正检查代码的纯度,因此我始终相信程序员是诚实的。例如,您不会将具有副作用的函数传递给期望纯函数的函数。请诚实并将高阶参数标记为不纯以避免问题。
希望这可以帮助您,并希望这不会太长。
更新:最近我发现了一个相当大的陷阱。我们必须记住,.NET中的String类型是不可变的。因此,当 marshaller 将其发送到 Haskell 代码时,我们在那里得到的 CWString 是原始字符串的副本。我们必须释放它。当 C# 中执行 GC 时,它不会影响 CWString,这是一个副本。
然而,问题在于当我们在 Haskell 代码中释放它时,我们不能使用 freeCWString。该指针未使用 C(msvcrt.dll)的 alloc 进行分配。有三种解决方法(我知道的)。
- 在调用Haskell函数时,请在C#代码中使用char*,而不是String。这样,当您调用返回时,您就有了要释放的指针,或者使用fixed初始化函数。
- 在Haskell中导入CoTaskMemFree并释放指针。
- 请使用StringBuilder而不是String。我不太确定这一点,但想法是由于StringBuilder被实现为本地指针,Marshaller只需将此指针传递给您的Haskell代码(该代码也可以更新它)。当调用返回后执行GC时,StringBuilder应该被释放。