我有Haskell代码需要与C库接口,如下所示:
// MyObject.h
typedef struct MyObject *MyObject;
MyObject newMyObject(void);
void myObjectDoStuff(MyObject myObject);
//...
void freeMyObject(MyObject myObject);
原始FFI代码使用unsafePerformIO
将所有这些函数包装为纯函数。这导致了错误和不一致,因为操作的顺序未定义。
我正在寻找的是在Haskell中处理对象而不诉诸IO
中的所有内容的一般方法。什么是好的是我可以做的事情:
myPureFunction :: String -> Int
-- create object, call methods, call destructor, return results
有没有很好的方法来实现这个目标?
答案 0 :(得分:6)
我们的想法是不断传递每个组件的接力棒,以强制按顺序评估每个组件。这基本上就是状态monad(IO
实际上是一个奇怪的状态monad。有点)。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.State
data Baton = Baton -- Hide the constructor!
newtype CLib a = CLib {runCLib :: State Baton a} deriving Monad
然后你就把操作串起来了。将它们注入CLib
monad将意味着它们被排序。从本质上讲,你是以一种更加不安全的方式伪造自己的IO
,因为你可以逃脱。
然后,您必须确保将construct
和destruct
添加到所有CLib
个链的末尾。这可以通过导出类似
clib :: CLib a -> a
clib m = runCLib $ construct >> m >> destruct
要跳过的最后一个大箍就是确保当unsafePerformIO
construct
中的任何内容时,它实际上会得到评估。
IO
中证明了这一点。而不是整个精心设计的过程,只是
construct :: IO Object
destruct :: IO ()
runClib :: (Object -> IO a) -> a
runClib = unsafePerformIO $ construct >>= m >> destruct
如果您不想使用名称IO
:
newtype CLib a = {runCLib :: IO a} deriving (Functor, Applicative, Monad)
答案 1 :(得分:2)
我的最终解决方案。它可能有一些我没有考虑过的微妙错误,但它是目前唯一符合所有原始标准的解决方案:
不幸的是,实现有点复杂。
E.g。
// Stack.h
typedef struct Stack *Stack;
Stack newStack(void);
void pushStack(Stack, int);
int popStack(Stack);
void freeStack(Stack);
c2hs文件:
{-# LANGUAGE ForeignFunctionInterface, GeneralizedNewtypeDeriving #-}
module CStack(StackEnv(), runStack, pushStack, popStack) where
import Foreign.C.Types
import Foreign.Ptr
import Foreign.ForeignPtr
import qualified Foreign.Marshal.Unsafe
import qualified Control.Monad.Reader
#include "Stack.h"
{#pointer Stack foreign newtype#}
newtype StackEnv a = StackEnv
(Control.Monad.Reader.ReaderT (Ptr Stack) IO a)
deriving (Functor, Monad)
runStack :: StackEnv a -> a
runStack (StackEnv (Control.Monad.Reader.ReaderT m))
= Foreign.Marshal.Unsafe.unsafeLocalState $ do
s <- {#call unsafe newStack#}
result <- m s
{#call unsafe freeStack#} s
return result
pushStack :: Int -> StackEnv ()
pushStack x = StackEnv . Control.Monad.Reader.ReaderT $
flip {#call unsafe pushStack as _pushStack#} (fromIntegral x)
popStack :: StackEnv Int
popStack = StackEnv . Control.Monad.Reader.ReaderT $
fmap fromIntegral . {#call unsafe popStack as _popStack#}
测试程序:
-- Main.hs
module Main where
import qualified CStack
main :: IO ()
main = print $ CStack.runStack x where
x :: CStack.StackEnv Int
x = pushStack 42 >> popStack
构建
$ gcc -Wall -Werror -c Stack.c
$ c2hs CStack.chs
$ ghc --make -Wall -Werror Main.hs Stack.o
$ ./Main
42
答案 2 :(得分:0)
免责声明:我从来没有真正使用过来自Haskell的C语言,所以我不是根据这里的经验发言。
但让我想到的是写下这样的东西:
withMyObject :: NFData r => My -> Object -> Constructor -> Params -> (MyObject -> r) -> r
将C ++构造函数/析构函数包装为IO操作。 withMyObject
使用IO对构造函数进行排序,调用用户指定的函数,调用析构函数并返回结果。然后可以unsafePerformIO
整个do
块(与其中已经烹饪的单个操作相反,它不起作用)。您也需要使用deepSeq
(这就是NFData
约束存在的原因),或者懒惰可能会推迟MyObject
的使用,直到它被破坏为止。
这样做的好处是:
MyObject -> r
函数,不需要monad MyObject
,以便在withMyObject
withMyObject
MyObject
1 unsafePerformIO
,因此这是您唯一需要仔细担心的是否有正确的顺序来证明它是否安全。还有一个地方你必须担心确保正确使用析构函数。它基本上是“构造,使用,破坏”模式,其中“使用”步骤的细节被抽象为参数,因此每次需要使用该模式时,您都可以拥有单个实现。
主要缺点是构造MyObject
然后将其传递给几个不相关的函数有点尴尬。您必须将它们捆绑到一个函数中,该函数返回每个原始结果的元组,然后使用withMyObject
。或者,如果您还单独公开构造函数和析构函数的IO
版本,则用户可以选择使用IO
比使包装函数传递给withMyObject
更不尴尬(但随后用户在释放后可能会意外使用MyObject
,或忘记释放它。
1 除非你做一些愚蠢的事情,比如使用id
作为MyObject -> r
函数。据推测,虽然没有NFData MyObject
实例。此类错误往往来自故意滥用而不是偶然的误解。