FFI Haskell回调函数与状态

7
我的问题是如何编写友好的Haskell接口,以模拟可以从C代码调用的回调。回调在此处进行处理(HaskellWiki),但我认为这个问题比该链接中的示例更复杂。
假设我们有需要回调的C代码,头文件如下:
typedef int CallbackType(char* input, char* output, int outputMaxSize, void* userData)

int execution(CallbackType* caller);

在这种情况下,函数execution接受一个回调函数,并将其用于处理新数据,本质上就是一个闭包。回调函数需要输入字符串、已分配大小为outputMaxSize的输出缓冲区和userData指针,该指针可以在回调函数内部以任何方式转换。
我们在Haskell中也会做类似的事情,当我们传递带有MVars的闭包时,我们仍然可以进行通信。因此,在编写外部接口时,我们希望保持这种类型。
具体来说,以下是FFI代码的示例:
type Callback = CString -> CString -> CInt -> Ptr () -> IO CInt

foreign import ccall safe "wrapper"
    wrap_callBack :: Callback -> IO (FunPtr Callback)

foreign import ccall safe "execution"
    execute :: FunPtr Callback -> IO CInt 

用户应该能够完成这种操作,但是使用type Ptr ()编写回调函数的方式显得界面比较差。相反,我们更喜欢使用感觉更自然的MVars来替代它。因此,我们想要编写一个函数:

myCallback :: String -> Int -> MVar a -> (Int, String)
myCallback input maxOutLength data = ...

为了转换成C语言,我们需要一个如下所示的函数:
```c // 伪代码 int toC(int x) { // 转换逻辑 } ```
castCallback :: ( String -> Int -> MVar a -> (Int, String) )
             -> ( CString -> CString -> CInt -> Ptr () -> IO CInt )

main = wrap_callBack (castCallback myCallback) >>= execute

在这种情况下,castCallback 的实现大部分不难, 将字符串转换为 cstring,Int 转换为 CInt,并复制输出字符串。
然而,困难的部分是解决 MVar 到 Ptr 的映射,这并不一定可存储。
我的问题是,在 Haskell 中编写回调代码的最佳方法是什么,同时仍然可以进行通信。

我绝不是FFI专家,但我的理解是C程序员使用void *技巧是因为他们没有真正的闭包。在Haskell中,我们有真正的闭包--所以只需完全忽略Haskell接口中的void *参数,并通过部分应用封闭任何本地数据(例如IORefMVar)。 - Daniel Wagner
啊!明白了。我会试一下的。我想那可能是绑定在做的事情,但我没有注意到。谢谢回复! - Anil Vaitla
@tigger,我之前也用过DanielWagner建议的技巧,在Haskell中从C进行同步回调 - 通过应用MVar参数获取部分函数,并让C函数使用MVar的数据回调它。如果您的MVar更复杂,则可以使用可存储向量或可存储实例将数据从C传递到MVar。将指向可存储实例的Ptr传递给C。这里有一个示例:http://hpaste.org/63702 - Sal
太好了!是的,出于某种原因,我不知道是否可以使用FFI来完成这个操作。感谢您的帮助。我会尝试一下! - Anil Vaitla
是的,这个方法很好用!你们中有没有人可以发表一个答案,这样我就可以接受它了吗? - Anil Vaitla
@tigger,好的,我刚刚发布了答案。 - Sal
1个回答

10
如果你想要访问像 MVar 这样的Haskell结构,但没有一个库函数可以把它转换为指针表示(也就是不应该被传递给C),那么你需要进行部分函数应用。
部分函数应用的技巧是构建一个已经应用了 MVar 的部分函数,并将指向该函数的指针传递给 C。C 将使用该指针回调该函数,并将对象放入 MVar 中。下面是一个示例代码(所有代码都源自我以前做过的东西 - 我为这里的示例修改了它,但未测试修改):
-- this is the function that C will call back
syncWithC :: MVar CInt -> CInt -> IO () 
syncWithC m x = do 
              putMVar m x
              return ()

foreign import ccall "wrapper"
  syncWithCWrap :: (CInt -> IO ()) -> IO (FunPtr (CInt  -> IO ()))

main = do
    m <- newEmptyMVar
    -- create a partial function with mvar m already applied. Pass to C. C will back with CInt
    f <- syncWithCWrap $ syncWithC m

如果您的MVar对象更加复杂,则需要构建该MVar对象的Storable实例(如果不存在)。例如,如果我想使用一个由Int对数组组成的MVar,那么首先需要定义Int对的Storable实例(SV是Storable Vector,MSV是Storable Mutable Vector):

data VCInt2 = IV2 {-# UNPACK #-} !CInt
                  {-# UNPACK #-} !CInt

instance SV.Storable VCInt2 where
  sizeOf _ = sizeOf (undefined :: CInt) * 2
  alignment _ = alignment (undefined :: CInt)
  peek p = do
             a <- peekElemOff q 0
             b <- peekElemOff q 1
             return (IV2 a b)
    where q = castPtr p
  {-# INLINE peek #-}
  poke p (IV2 a b) = do
             pokeElemOff q 0 a
             pokeElemOff q 1 b
    where q = castPtr p
  {-# INLINE poke #-}

现在,你可以将指向vector的指针传递给C,让它更新vector,并使用不带参数的void函数回调(因为C已经正在填充vector)。这也通过在Haskell和C之间共享内存来避免昂贵的数据编组。

-- a "wrapper" import is a converter for converting a Haskell function to a foreign function pointer
foreign import ccall "wrapper"
  syncWithCWrap :: IO () -> IO (FunPtr (IO ()))


-- call syncWithCWrap on syncWithC with both arguments applied
-- the result is a function with no arguments. Pass the function, and 
-- pointer to x to C. Have C fill in x first, and then call back syncWithC 
-- with no arguments
syncWithC :: MVar (SV.Vector VCInt2) -> MSV.IOVector VCInt2 -> IO ()
syncWithC m1 x = do
              SV.unsafeFreeze x >>= putMVar m1
              return ()

在 C 语言中,您需要声明一个结构体以使 VCInt2 知道如何解析它:

/** Haskell Storable Vector element with two int members **/
typedef struct vcint2{
  int a;
  int b;
} vcint2;

因此,在 C 侧,您正在传递 vcint2 指针以用于 MVar 对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接