带状态的 FFI Haskell 回调

发布于 2025-01-05 19:04:09 字数 1554 浏览 1 评论 0原文

我的问题是关于如何编写友好的 Haskell 接口来模拟可以从 C 代码调用的回调。回调在这里解决(HaskellWiki),但是,我相信这个问题比该链接中的示例更复杂。

假设我们有需要回调的 C 代码,并且标头如下所示:

typedef int CallbackType(char* input, char* output, int outputMaxSize, void* userData)

int execution(CallbackType* caller);

在这种情况下,函数 execution 接受一个回调函数,并将使用它来处理新数据,本质上是一个闭包。回调需要一个输入字符串、一个已分配大小 outputMaxSize 的输出缓冲区和 userData 指针,但可以在回调内部进行强制转换。

当我们使用 MVar 传递闭包时,我们在 haskell 中做了类似的事情,因此我们仍然可以进行通信。因此,当我们编写Foreign接口时,我们希望保留这种类型。

具体来说,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 

用户应该能够执行此类操作,但感觉界面很差,因为 他们需要编写类型为 Ptr() 的回调。相反,我们想用 MVar 替换它 感觉更自然。所以我们想编写一个函数:

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

为了转换为 C,我们希望有一个如下函数:

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 中编写回调代码的最佳方法是什么,并且仍然可以与之通信。

My question is about how to write friendly Haskell Interfaces that model callbacks which can be invoked from C code. Callbacks are addressed here (HaskellWiki), however, I believe this question is more complex than the example from that link.

Suppose we have C code, requiring callbacks and the header looks like the following:

typedef int CallbackType(char* input, char* output, int outputMaxSize, void* userData)

int execution(CallbackType* caller);

In this case the function execution takes a callback function and will use that to process new data, essentially a closure. The call back expects an input string, an output buffer which has been allocated with size outputMaxSize and the userData pointer, which can be casted however inside the callback.

We do similar things in haskell, when we pass around closures with MVars, so we can still communicate. Therefore when we write the Foreign interface, we'd like to keep this sort of type.

Specifically here is what the FFI Code might look like:

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 

Users should be able to do this sort of thing, but it feels like a poor interface since
they need to write callbacks with type Ptr (). Rather we'd like to replace this with MVars
which feel more natural. So we'd like to write a function:

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

In order to convert to C, we'd like to have a function like:

castCallback :: ( String -> Int -> MVar a -> (Int, String) )
             -> ( CString -> CString -> CInt -> Ptr () -> IO CInt )

main = wrap_callBack (castCallback myCallback) >>= execute

In this case castCallback is for the most part not hard to implement,
convert string -> cstring, Int -> CInt, and copy over the output string.

The hard part however is resolving the MVar to Ptr, which is not necessarily storable.

My Question is what is the best way to go about writing callback code in Haskell, which can still be communicated with.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

活雷疯 2025-01-12 19:04:09

如果你想访问像 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 实例(SVStorable Vector code>,MSVStorable 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 #-}

现在,您只需将指向向量的指针传递给 C,让它更新向量,然后回调 void 函数没有参数(因为 C 已经是填充向量)。这还可以通过在 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< /code> MVar 对象的指针。

If you want to access a Haskell structure like MVar which doesn't have a library function to convert it to a pointer representation (meaning it is not supposed to be passed to C), then you need to do partial function application.

In the partial function application, the trick is to build a partial function with MVar already applied, and pass the pointer to that function to C. C will then call it back with the object to put in MVar. An example code below (all the code below is derived from something I did before - I modified it for examples here but haven't tested the modifications):

-- 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

What if your MVar object is more complex? Then you need to build a Storable instance of the MVar object if it doesn't exist. For example, if I want to use an MVar with array of pair of Ints, then first define a Storable instance of Int pairs (SV is Storable Vector, MSV is 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 #-}

Now, you can just pass a pointer to the vector to C, have it update the vector, and call back the void function with no arguments (since C is already filling the vector). This also avoid expensive data marshalling by sharing memory between Haskell and 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 ()

On C side, you will need a struct declaration for VCInt2 so that it knows how to parse it:

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

So, on C side, you are passing it vcint2 pointer for MVar object.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文