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 fromIntegralThat 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.
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.
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
$ checkedStatusThe 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.
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-runtimehs-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.
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.
- 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,
MarshalandUnmarshaller, how each combinator is derived, and what a finished spec compiles to. - The Haddock for
Binding.Combinatorsis the per-combinator reference. examples/wraps three real C libraries.
Each of these is a place where the types will not save you. The Haddock linked from each carries the detail.
autoandautoInputstake everything that is left, so both come last. (Binding.Combinators.Auto)autoassembles 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
funPtrInfrees theFunPtrwhen the call returns. (Binding.Combinators.Defaults) toHighLevelPureisunsafePerformIO. Sound only when the call really is a function of its inputs, and only with a non-throwing closer. (Binding.Combinators.Result)
Pre-release (alpha). The API is still settling.
Owned by Well-Typed LLP and Anduril Industries. BSD-3-Clause.