Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Taming the C Monster: Haskell FFI Techniques

Taming the C Monster: Haskell FFI Techniques

Haskell has a powerful foreign function interface (FFI) for interfacing with C libraries. Haskell is a great language for building libraries and tools, but interoperability requirements or time constraints can make the FFI a compelling option.

Binding to a non-trivial C library presents several challenges including C idioms, memory management, error handling and more. This presentation will address a selection of these concerns, using hs-notmuch, a binding to the “notmuch” mail indexer, as a case study. We will discuss:

- FFI basics and tools to assist binding authors
- working with “double pointer”-style constructors
- working with iterators; how to do lazy iteration
- how to use Haskell’s garbage collector to manage lifecycles of external objects, and “gotchas” encountered
- using types to enforce correct use of unsafe APIs
- performance considerations (including profiling results)

The presentation will conclude with a mention of some important FFI concepts that were not covered (e.g. callbacks) and a look at how hs-notmuch is being used in the real world.

Attendees will leave this presentation confident to write Haskell bindings to many kinds of C libraries. Developers familiar with C will get the most out of this talk (because there will be limited time to explain C idioms, memory management, etc). To varying degrees, most of the concepts and techniques discussed will apply to other languages’ FFIs.

Fraser Tweedale

July 16, 2018
Tweet

More Decks by Fraser Tweedale

Other Decks in Programming

Transcript

  1. why FFI? want to do $THING in Haskell there exists

    a C library for $THING interoperability / bug-compatibility performance / timing-critical code
  2. C FFI {-# LANGUAGE ForeignFunctionInterface #-} import Foreign.C.Types foreign import

    ccall "math.h sin" c_sin :: CDouble -> CDouble main :: IO () main = print $ c_sin 1.0
  3. c2hs file extension: .chs more features than hsc2hs automatic generation

    of foreign import declarations library ... build-tools: c2hs >= 0.19.1
  4. c2hs - example ... result <- notmuch_database_open path 1 ptr

    ... foreign import ccall "Notmuch/Binding.chs.h notmuch_database_open" notmuch_database_open :: CString -> CInt -> Ptr (Ptr Database) -> IO CInt
  5. Foreign.Ptr data Ptr a nullPtr :: Ptr a plusPtr ::

    Ptr a -> Int -> Ptr b castPtr :: Ptr a -> Ptr b
  6. Foreign.ForeignPtr data ForeignPtr a type FinalizerPtr a = FunPtr (Ptr

    a -> IO ()) newForeignPtr :: FinalizerPtr a -> Ptr a -> IO (ForeignPtr a) withForeignPtr :: ForeignPtr a -> (Ptr a -> IO b) -> IO b
  7. Foreign.C.String type CString = Ptr CChar peekCString :: CString ->

    IO String withCString :: String -> (CString -> IO a) -> IO a
  8. Foreign.Storable class Storable a where peek :: Ptr a ->

    IO a ... instance Storable (Ptr a) -- Foreign.Marshal.Alloc alloca :: Storable a => (Ptr a -> IO b) -> IO b
  9. enum types typedef enum _notmuch_status { NOTMUCH_STATUS_SUCCESS = 0, NOTMUCH_STATUS_OUT_OF_MEMORY,

    NOTMUCH_STATUS_READ_ONLY_DATABASE, NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW, ... } notmuch_status_t
  10. enum types data Status = StatusSuccess | StatusOutOfMemory | StatusReadOnlyDatabase

    | StatusUnbalancedFreezeThaw ... deriving (Eq) instance Enum Status where ...
  11. opaque pointer types newtype DatabaseHandle = DatabaseHandle (ForeignPtr DatabaseHandle) withDatabaseHandle

    :: DatabaseHandle -> (Ptr DatabaseHandle -> IO b) -> IO b withDatabaseHandle (DatabaseHandle fptr) = withForeignPtr fptr
  12. double-pointer constructors databaseOpen :: CString -> IO (Either Status DatabaseHandle)

    databaseOpen path = alloca $ \ptr -> do result <- {#call notmuch_database_open #} path 1 ptr case toEnum (fromIntegral result) of StatusSuccess -> Right . DatabaseHandle <$> (peek ptr >>= newForeignPtr_) e -> pure (Left e)
  13. iterator notmuch_tags_t * notmuch_message_get_tags (notmuch_message_t *message); notmuch_bool_t notmuch_tags_valid (notmuch_tags_t *tags);

    const char * notmuch_tags_get (notmuch_tags_t *tags); void notmuch_tags_move_to_next (notmuch_tags_t *tags);
  14. iterator tagsToList :: Tags -> IO [String] tagsToList (Tags ptr)

    = go where go = test ptr >>= \valid -> case valid of 0 -> pure [] _ -> (:) <$> (get ptr >>= mk >>= \x -> next ptr $> x) <*> go test = {#call notmuch_tags_valid #} get = {#call notmuch_tags_get #} next = {#call notmuch_tags_move_to_next #} mk = peekCString
  15. macros #if (__GNUC__ >= 3) #define _TALLOC_TYPEOF(ptr) __typeof__(ptr) #define talloc_steal(ctx,

    ptr) ({ \ _TALLOC_TYPEOF(ptr) __talloc_steal_ret = (_TALLOC_TYPEOF(ptr)) \ _talloc_steal_loc((ctx), (ptr), __location__); \ __talloc_steal_ret; }) #else /* __GNUC__ >= 3 */ #define _TALLOC_TYPEOF(ptr) void * #define talloc_steal(ctx, ptr) \ (_TALLOC_TYPEOF(ptr)) _talloc_steal_loc((ctx), (ptr), __location__) #endif /* __GNUC__ >= 3 */ void *_talloc_steal_loc( const void *new_ctx, const void *ptr, const char *location);
  16. external object lifecycles query_create :: DatabaseHandle -> String -> IO

    (Query a) query_create db s = withCString s $ \s’ -> withDatabaseHandle db $ \db’ -> {#call notmuch_query_create #} db’ s’ >>= fmap Query . newForeignPtr query_destroy foreign import ccall "&notmuch_query_destroy" query_destroy :: FinalizerPtr Query
  17. external object lifecycles query_create :: DatabaseHandle -> String -> IO

    (Query a) query_create db s = withCString s $ \s’ -> withDatabaseHandle db $ \db’ -> {#call notmuch_query_create #} db’ s’ >>= fmap Query . newForeignPtr query_destroy foreign import ccall "&notmuch_query_destroy" query_destroy :: FunPtr (Ptr Query -> IO ())
  18. read-only mode {-# LANGUAGE DataKinds #-} newtype Database (a ::

    DatabaseMode) = Database DatabaseHandle withDatabase :: Database a -> (Ptr DatabaseHandle -> IO b) -> IO b withDatabase (Database dbh) = withDatabaseHandle dbh data Message (a :: DatabaseMode) = Message MessageHandle
  19. read-only mode type RW = ’DatabaseModeReadWrite -- convenient alias messageAddTag

    :: Message RW -> Tag -> IO () messageAddTag msg tag = void $ withMessage msg $ tagUseAsCString tag . {#call notmuch_message_add_tag #}
  20. locking /* can return NOTMUCH_STATUS_READ_ONLY_DATABASE */ notmuch_status_t notmuch_message_freeze (notmuch_message_t *message);

    /* can return NOTMUCH_STATUS_READ_ONLY_DATABASE or NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW */ notmuch_status_t notmuch_message_thaw (notmuch_message_t *message);
  21. locking {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeFamilies #-} {-#

    LANGUAGE TypeOperators #-} import GHC.TypeLits data Message (n :: Nat) (a :: DatabaseMode) = Message MessageHandle messageAddTag :: Message n RW -> Tag -> IO () messageAddTag msg tag = void $ withMessage msg $ tagUseAsCString tag . {#call notmuch_message_add_tag #}
  22. locking messageFreeze :: Message n RW -> IO (Message (n

    + 1) RW) messageFreeze msg = withMessage msg {#call notmuch_message_freeze #} $> coerce msg messageThaw :: (1 <= n) => Message n RW -> IO (Message (n - 1) RW) message_thaw msg = withMessage msg {#call notmuch_message_thaw #} $> coerce msg
  23. unsafe {#call unsafe notmuch_messages_valid #} foreign import ccall unsafe "notmuch.h

    notmuch_messages_valid" notmuch_messages_valid :: Messages -> IO CInt
  24. unsafe Before: total time = 6.53 secs (6530 ticks @

    1000 us, 1 processor) total alloc = 260,249,536 bytes (excludes profiling overheads) After: total time = 3.73 secs (3728 ticks @ 1000 us, 1 processor) total alloc = 260,249,536 bytes (excludes profiling overheads)
  25. lazy iteration messagesToList :: Messages -> IO [Message n a]

    messagesToList (Messages ptr) = go where go = test ptr >>= \valid -> case valid of 0 -> pure [] _ -> (:) <$> (get ptr >>= mk >>= \x -> next ptr $> x) <*> go
  26. lazy iteration import System.IO.Unsafe (unsafeInterleaveIO) messagesToList :: Messages -> IO

    [Message n a] messagesToList (Messages ptr) = go where go = test ptr >>= \valid -> case valid of 0 -> pure [] _ -> (:) <$> (get ptr >>= mk >>= \x -> next ptr $> x) <*> unsafeInterleaveIO go
  27. lazy iteration (search *, take 10, count tags) Before: total

    time = 1.79 secs (1795 ticks @ 1000 us, 1 processor) total alloc = 59,500,568 bytes (excludes profiling overheads) After: total time = 0.07 secs (68 ticks @ 1000 us, 1 processor) total alloc = 79,960 bytes (excludes profiling overheads)
  28. lazy iteration (search *, count tags) Before: 68,431,240 bytes maximum

    residency (9 sample(s)) total time = 8.37 secs (8370 ticks @ 1000 us, 1 processor) total alloc = 218,627,008 bytes (excludes profiling overheads) After: 40,965,384 bytes maximum residency (8 sample(s)) total time = 7.59 secs (7586 ticks @ 1000 us, 1 processor) total alloc = 257,666,440 bytes (excludes profiling overheads)
  29. things that weren’t covered foreign export (callbacks) (un)marshalling C structs

    other FFIs (JVM, JavaScript, . . . ) other performance tips (avoiding unnecessary copies, interning, . . . )
  30. Questions? Except where otherwise noted this work is licensed under

    http://creativecommons.org/licenses/by/4.0/ https://speakerdeck.com/frasertweedale @hackuador purebred-mua/hs-notmuch