Skip to content

Repository files navigation

binding-combinators

Write high-level Haskell wrappers over low-level FFI bindings, declaratively.

Wrapping a C function by hand is repetitive: allocate a buffer, pass a Ptr, thread an out-parameter, read it back, check a status code, free whatever C handed back. This library gives you combinators for saying what each C argument is for, and does the marshalling from that description.

int strncmp(const char *str1, const char *str2, size_t n);
c_strncmp :: PtrConst CChar -> PtrConst CChar -> CSize -> IO CInt

hsStrncmp :: String -> ByteString -> IO Int
hsStrncmp = toHighLevel c_strncmp
          $ input  withCStringIn        -- const char *str1
          $ input2 useAsByteStringLenIn -- str2, n
          $ resultPure fromIntegral

That reads top to bottom like an annotated C prototype: one input per Haskell argument, marshalled into the C argument or arguments it fills, closed by a conversion of the return value. The approach is inspired by c2hs, but with no custom syntax and no code generation step. A spec is an ordinary Haskell value, so you can name it, reuse it, and let the type checker check it.

c_strncmp is the low-level binding, which this library does not produce. You either write it yourself or generate it with hs-bindgen, Well-Typed's C-to-Haskell binding generator; PtrConst is hs-bindgen's read-only pointer, one of the types generated bindings are written in.

What it costs

Nothing at runtime. Every combinator is INLINE, and at a finished binding all three type indices are concrete, so every class method resolves to a known instance. Put a spec and a hand-written wrapper for the same function in one module and GHC produces one worker that both names cast to:

-- double frexp(double x, int *exp);
hsFrexpSpec :: Double -> IO (Int, Double)
hsFrexpSpec = toHighLevel c_frexp
            $ input  (scalar realToFrac)
            $ output (unmarshalOutPure fromIntegral)
            $ resultPure (\e m -> (e, realToFrac m))

hsFrexpHand :: Double -> IO (Int, Double)
hsFrexpHand x = alloca $ \p -> do
  m <- c_frexp (realToFrac x) p
  e <- peek p
  pure (fromIntegral e, realToFrac m)
$ ghc -O -ddump-simpl -dsuppress-all
hsFrexpHand1 = \ x eta -> $wallocaBytesAligned 4# 4# (\p s -> ... frexp ...) eta
hsFrexpHand  = hsFrexpHand1 `cast` <Co:8> :: ...
hsFrexpSpec  = hsFrexpHand1 `cast` <Co:8> :: ...

How it works shows the same for inputs and for a spec using every combinator at once.

What a real one looks like

git_commit_create takes ten C arguments. The spec exposes six, and the four it hides are visible in the text (Write.hs, from the libgit2 example):

commitCreate :: Repository -> Text -> Signature -> Signature -> Text -> Tree -> IO Oid
commitCreate = toHighLevel C.git_commit_create
             $ output oidOut                   -- git_oid *id (out)
             $ input  handleIn                 -- git_repository *repo
             $ input  textIn                   -- const char *update_ref
             $ input  (asArgumentC sigMarshal) -- const git_signature *author
             $ input  (asArgumentC sigMarshal) -- const git_signature *committer
             $ fixed  nullConst                -- const char *message_encoding = NULL
             $ input  textIn                   -- const char *message
             $ input  handleInC                -- const git_tree *tree
             $ fixed  0                        -- size_t parent_count = 0
             $ fixed  nullPtr                  -- const git_commit **parents = NULL
             $ checkedStatus

The high-level arguments arrive in C's order, since each input adds its argument where C takes it. To expose a different order, name the arguments and apply them yourself.

Conventions repeated across a library get named once. libgit2 has ten handle types whose constructors all fill a git_X **out and return a status; one newHandle combinator covers every one of them, and Writing a combinator builds it.

Installing

Requires GHC 9.2 or later. Tested on 9.2 through 9.14.

Not on Hackage yet. It depends on hs-bindgen-runtime, which is not on Hackage either, so for now both come from git. In your cabal.project:

source-repository-package
  type:     git
  location: https://github.com/well-typed/binding-combinators
  tag:      <commit>

source-repository-package
  type:     git
  location: https://github.com/well-typed/hs-bindgen
  tag:      <commit>
  subdir:   hs-bindgen-runtime

hs-bindgen-runtime supplies the types generated bindings are written in, which the marshallers are defined against. The generator itself is not a dependency, which is why this library is versioned separately.

Scope

The bindings do not have to be generated, but how far you get over a hand-written foreign import depends on the types it uses. Scalars, plain Ptrs and callbacks have defaults and behave as they do over generated bindings. The richer marshallers assume the generated vocabulary: const pointers arrive as PtrConst, flexible array members as IncompleteArray, and by-value structs are marshalled through the ReadRaw and WriteRaw instances hs-bindgen emits per struct.

Some things this deliberately does not do. It does not read headers, so it generates nothing and extracts no constants or layouts; that is hs-bindgen's job, or hsc2hs's. It does not write C, so a wrapper that needs a snippet of C is a job for inline-c. And a spec is a straight line from the first C argument to the result, so a wrapper that has to branch between calls is ordinary IO code that happens to call two bindings.

Wrapping a handful of functions is probably not worth a vocabulary. The library pays off where a convention repeats.

Documentation

  • Your first binding goes from an empty file to a working wrapper in one module, imports included.
  • Writing a spec teaches the library: combinators, auto, and the typed-hole workflow.
  • Writing a struct marshaller covers the two halves of a struct and the three adapters that drop one into a spec.
  • Writing a combinator abstracts over a C library's own conventions.
  • How it works explains the machinery: the type signatures, Marshal and Unmarshaller, how each combinator is derived, and what a finished spec compiles to.
  • The Haddock for Binding.Combinators is the per-combinator reference.
  • examples/ wraps three real C libraries.

Caveats

Each of these is a place where the types will not save you. The Haddock linked from each carries the detail.

  • auto and autoInputs take everything that is left, so both come last. (Binding.Combinators.Auto)
  • auto assembles results positionally, so it cannot tell two components of the same type apart. (autoResult)
  • Each default picks a policy and nothing checks it. Numeric conversion is lossy and silent, the string result defaults borrow, and funPtrIn frees the FunPtr when the call returns. (Binding.Combinators.Defaults)
  • toHighLevelPure is unsafePerformIO. Sound only when the call really is a function of its inputs, and only with a non-throwing closer. (Binding.Combinators.Result)

Status

Pre-release (alpha). The API is still settling.

Owned by Well-Typed LLP and Anduril Industries. BSD-3-Clause.

About

Combinators for constructing high-level FFI bindings from low-level bindings

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages