packages feed

fb-util-0.1.0.1: Util/FFI.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wno-star-is-type #-}
module Util.FFI (
  FFIError, ffiErrorMessage, call,
  List(..), FFIFun(..), Tuple(..), invoke,

  unsafeWithForeignPtr,
) where

import Foreign hiding (with, withMany)
import Foreign.C
#if __GLASGOW_HASKELL__ >= 902
import GHC.ForeignPtr
#endif
import System.IO.Unsafe (unsafePerformIO)
import Control.Exception
import Control.Monad

#if __GLASGOW_HASKELL__ < 902
unsafeWithForeignPtr :: ForeignPtr a -> (Ptr a -> IO b) -> IO b
unsafeWithForeignPtr = withForeignPtr
#endif

foreign import ccall unsafe "&hs_ffi_free_error" hs_ffi_free_error
  :: FunPtr (CString -> IO ())

newtype FFIError = FFIError (ForeignPtr CChar)

ffiErrorMessage :: FFIError -> String
ffiErrorMessage (FFIError fp) =
  unsafePerformIO $ unsafeWithForeignPtr fp peekCString

instance Show FFIError where
  show = ffiErrorMessage

instance Exception FFIError

-- | Call an FFI function that returns 'nullPtr' if it succeeds, or a
-- 'CString' containing the error message if it fails.  On failure the
-- error is thrown as an 'FFIError' exception.
{-# INLINE call #-}
call :: IO CString -> IO ()
call f = do
  p <- f
  when (p /= nullPtr) $ callError p

callError :: CString -> IO ()
callError p = do
  fp <- newForeignPtr hs_ffi_free_error p
  throwIO $ FFIError fp

infixr 5 :>

data List xs where
  Nil :: List '[]
  (:>) :: x -> List xs -> List (x ': xs)

class Tuple xs where
  type Tup xs
  tuple :: List xs -> Tup xs

instance Tuple '[] where
  type Tup '[] = ()
  tuple Nil = ()

instance Tuple '[a] where
  type Tup '[a] = a
  tuple (a :> _) = a

instance Tuple [a,b] where
  type Tup [a,b] = (a,b)
  tuple (a :> b :> _) = (a,b)

instance Tuple [a,b,c] where
  type Tup [a,b,c] = (a,b,c)
  tuple (a :> b :> c :> _) = (a,b,c)

instance Tuple [a,b,c,d] where
  type Tup [a,b,c,d] = (a,b,c,d)
  tuple (a :> b :> c :> d :> _) = (a,b,c,d)

instance Tuple [a,b,c,d,e] where
  type Tup [a,b,c,d,e] = (a,b,c,d,e)
  tuple (a :> b :> c :> d :> e :> _) = (a,b,c,d,e)

instance Tuple [a,b,c,d,e,f] where
  type Tup [a,b,c,d,e,f] = (a,b,c,d,e,f)
  tuple (a :> b :> c :> d :> e :> f :> _) = (a,b,c,d,e,f)

instance Tuple [a,b,c,d,e,f,g] where
  type Tup [a,b,c,d,e,f,g] = (a,b,c,d,e,f,g)
  tuple (a :> b :> c :> d :> e :> f :> g :> _) = (a,b,c,d,e,f,g)

instance Tuple [a,b,c,d,e,f,g,h] where
  type Tup [a,b,c,d,e,f,g,h] = (a,b,c,d,e,f,g,h)
  tuple (a :> b :> c :> d :> e :> f :> g :> h :> _) = (a,b,c,d,e,f,g,h)

instance Tuple [a,b,c,d,e,f,g,h,i] where
  type Tup [a,b,c,d,e,f,g,h,i] = (a,b,c,d,e,f,g,h,i)
  tuple (a :> b :> c :> d :> e :> f :> g :> h :> i :> _) = (a,b,c,d,e,f,g,h,i)

class FFIFun f where
  type Res f :: [*]
  invoke' :: f -> IO (List (Res f))

instance FFIFun (IO CString) where
  type Res (IO CString) = '[]
  invoke' f = do
    call f
    return Nil

instance FFIFun (IO ()) where
  type Res (IO ()) = '[]
  invoke' f = do
    f
    return Nil

-- | For output parameter 'Ptr a' this will 'alloca' memory (unitialized)
-- and then proceed to the next parameter.  On returning this prepends
-- the result into x.  This is only safe if the invoked function
-- always fills in this output parameter pointer before returning.
instance (Storable a, FFIFun f) => FFIFun (Ptr a -> f) where
  type Res (Ptr a -> f) = a ': Res f
  invoke' f = alloca $ \p -> do
    xs <- invoke' (f p)
    x <- peek p
    return $ x :> xs

-- | Call an FFI function with automatic handling of return values and
-- errors.
--
-- For example, if we have a function with one input and two outputs,
-- optionally returning an error as a `CString`:
--
-- > foreign import ccall unsafe json_encode
-- >   :: Ptr Dyn -> Ptr (Ptr Word8) -> Ptr CSize -> IO CString
--
-- Then we can call it like this:
--
-- >   (bytes, size) <- invoke $ json_encode dyn
--
-- where @bytes :: Ptr Word8@ and @size :: CSize@.
--
-- Note: this is only safe if the invoked function always fills in
-- output parameter pointers before returning.
invoke :: (FFIFun f, Tuple (Res f)) => f -> IO (Tup (Res f))
invoke = fmap tuple . invoke'