异步C通过FFI回调到Haskell代码

sir*_*usa 7 haskell asynchronous callback ffi

我目前正在尝试将部分Windows API移植到Haskell.其中一个功能是SetConsoleCtrlHandler注册一个处理程序回调,只要从控制台输入接收到Ctrl + C或Ctrl + Break,就会调用它.所以这里有一个小样本程序:

{-# LANGUAGE CPP #-}
{-# LANGUAGE ForeignFunctionInterface #-}
module Main where

import Control.Monad
import GHC.ConsoleHandler
import Foreign
import System.Win32.Process
import System.Win32.Types

foreign import ccall "wrapper"
    makeHandlerPtr :: (DWORD -> IO BOOL) -> IO (FunPtr (DWORD -> IO BOOL))

foreign import stdcall "windows.h SetConsoleCtrlHandler"
    c_SetConsoleCtrlHandler :: FunPtr (DWORD -> IO BOOL) -> BOOL -> IO BOOL

main :: IO ()
main = unsafeCtrl

unsafeCtrl :: IO ()
unsafeCtrl = do
    ptrHandler <- makeHandlerPtr $ \d -> do
        putStrLn $ "received event " ++ show d
        return True
    c_SetConsoleCtrlHandler ptrHandler True

    forM_ [1..50] $ \_ -> sleep 100
    freeHaskellFunPtr ptrHandler

safeCtrl :: IO ()
safeCtrl = do
    installHandler $ Catch $ \e -> do
        putStrLn $ "received event " ++ show e

    forM_ [1..50] $ \_ -> sleep 100
Run Code Online (Sandbox Code Playgroud)

如果您通过编译上述程序ghc --make AsyncCallback.hs并运行该程序,则一旦收到控件事件,它就会崩溃,并出现以下错误:

$ AsyncCallback
AsyncCallback.exe: schedule: re-entered unsafely.
   Perhaps a 'foreign import unsafe' should be 'safe'?
Run Code Online (Sandbox Code Playgroud)

但是,使用-threaded添加的选项进行编译似乎工作正常:

$ AsyncCallback
received event 0
received event 1
received event 0
Run Code Online (Sandbox Code Playgroud)

不知道为什么这会使它工作,因为我没有在这里明确使用线程,我无法看到GHC将隐式启动新线程的位置.

早些时候在#haskell上问过这个问题,有人指出从C回调中异步调用Haskell函数本质上是不安全的,上面的例子或多或少巧合.我也被指出GHC.ConsoleHandler.installHandler这似乎是一个安全的包装SetConsoleCtrlHandler,并运行上述程序main = safeCtrl确实工作正常.我试图理解installHandler(C端和Haskell端)的实现,但我不太明白.

这里的整个问题似乎是回调是异步发出的,这样当回调试图运行Haskell代码时,Haskell RTS没有"准备好".所以我想知道

  1. GHC的实施如何installHandler运作?使实施工作和我的版本失败的关键部分是什么?
  2. 是否有其他选项/一般模式如何从C回调中异步调用Haskell代码?在这种特殊情况下,我可以切换到GHC实施.但是我可能会遇到类似的C函数,这些函数异步调用回Haskell代码,我必须自己写一个安全的绑定.