Passing haskell functions to a c function as function pointers

Hi everyone, I was working on a hobby project to get used to haskells FFI, I wanted to create a function that allows custom defined handlers for errors and for recieved responses, so I wrote the needed c code using c function pointers

foreign import ccall "ListenTcpsocketv4" cListenTcpIP4 :: Int -> CString -> Int -> Int -> Int -> (Int -> CString -> IO ()) -> (Int -> IO ()) -> IO Int

however I got into issues to to the functions passed as arguments not being marshelled, I tried making them of type FunPtr but then couldn’t pass a haskell function to this c call, any help?

You might want to look at my old bindings to libtelnet: ~jack/libtelnet-haskell - Haskell bindings to libtelnet. - sourcehut git

The things that seem most important to you:

  1. Declare the imported function using primitives and a callback type that closely matches the C API:

    type TelnetEventHandlerT = Ptr T.TelnetT -> Ptr T.EventT -> Ptr () -> IO ()
    foreign import ccall "libtelnet.h telnet_init"
      cTelnetInit
        :: Ptr T.TelnetTeloptT -- ^ @const telnet_telopt_t *telopts@
        -> FunPtr TelnetEventHandlerT -- ^ @telnet_event_handler_t eh@
        -> CUChar -- ^ @unsigned char flags@
        -> Ptr () -- ^ @void *user_data@
        -> IO (Ptr T.TelnetT)
    
  2. Declare a foreign import "wrapper" to turn the Haskell function into a FunPtr:

    foreign import ccall "wrapper"
      wrapEventHandler :: TelnetEventHandlerT -> IO (FunPtr TelnetEventHandlerT)
    
  3. Provide a mid-level wrapper function that marshals the FFI-level arguments into an actual call into foreign code. Note the use of newForeignPtr to clean up the wrapper FunPtr as well as other stuff we had to create:

    telnetInit
      :: [T.TelnetTeloptT]
      -> TelnetEventHandlerT
      -> [T.Flag]
      -> IO (ForeignPtr T.TelnetT)
    telnetInit options handler flags = do
      optionsA <- newArray0 (T.TelnetTeloptT (-1) iacNull iacNull) options
      handlerP <- wrapEventHandler handler
      let flagsC = foldr ((.|.) . T.unFlag) 0 flags
      telnet <- cTelnetInit optionsA handlerP flagsC nullPtr
      when (telnet == nullPtr) $ throwIO T.NullTelnetPtr
    
      newForeignPtr telnet $ do
        cTelnetFree telnet
        freeHaskellFunPtr handlerP
        free optionsA
    
  4. Provide functions and types so that setup is ergonomic for the library user:

    type EventHandler = TelnetPtr -> Event -> IO ()
    data Event = Received ByteString | Send ByteString | -- and many others
    -- basically a big case-match that packs CString into ByteString etc.
    convertEventT :: FFI.EventT -> IO Event
    
    convertEventHandler :: EventHandler -> F.TelnetEventHandlerT
    convertEventHandler f telnetP eventP _ =
      peek eventP >>= convertEventT >>= f telnetP
    
    -- | Main init function for the library
    telnetInit :: [OptionSpec] -> [Flag] -> EventHandler -> IO Telnet
    telnetInit options flags handler =
        FFI.telnetInit options' (convertEventHandler handler) flags
      where
        options' = map f options
        f (OptionSpec opt us him) =
          let us' = if us then iacWill else iacWont
              him' = if him then iacDo else iacDont
          in T.TelnetTeloptT (fromIntegral $ unOption opt) us' him'
    
  5. Provide a typeclass to generalise over both Ptr TelnetT and ForeignPtr TelnetT (does something like this exist in base?):

    -- | The pointer you get back from 'telnetInit' is a 'ForeignPtr'
    -- because it carries around its finalizers, but the pointer that gets
    -- passed into your 'EventHandler' is a bare 'Ptr' because it's being
    -- passed in from C. This class lets us generalise across both types.
    class HasTelnetPtr t where
      withTelnetPtr :: t -> (TelnetPtr -> IO a) -> IO a
    
    -- | Unwrap with 'withForeignPtr'.
    instance HasTelnetPtr Telnet where
      withTelnetPtr = withForeignPtr
    
    -- | No unwrapping needed.
    instance HasTelnetPtr TelnetPtr where
      withTelnetPtr t f = f t
    
  6. Provide wrappers over FFI functions that can use either from of the telnet_t pointer:

    telnetRecv :: HasTelnetPtr t => t -> ByteString -> IO ()
    telnetRecv t bs = withTelnetPtr t $ \telnetP -> FFI.telnetRecv telnetP bs
    
2 Likes

Sorry if this is a rather silly question but in this case is wrapper something given by the compiler? does wrapper need to be defined on its own? if so how would the wrapper function look?

foreign import "wrapper" is specified by the Haskell report:

Dynamic wrapper.
The type of a wrapper stub has to be of the form ft -> IO (FunPtr ft), where ft may be any foreign type.

As an example, consider

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

The stub factory mkCallback turns any Haskell computation of type IO () into a C function pointer that can be passed to C routines, which can call back into the Haskell context by invoking the referenced function.

1 Like