In this tutorial, we use hs-bindgen
to automatically generate Haskell
bindings for libpcap
, an interface to various kernel packet capture
mechanisms. Further, we use the generated Haskell bindings to print the list of
network devices available on the local machine. We use the Nix package
manager to manage installation of hs-bindgen
and other system
dependencies.
First, we generate bindings using the hs-bindgen
client with binary name
hs-bindgen-cli
. The client generates a set of modules exposing a Haskell
interface to the translated C header files.
We compile our program, linking the resulting object files to the shared
libpcap
library which needs to be available. That is, while generating the
bindings only requires the C header files to be available, using the generated
bindings requires a (compiled) implementation of the interface defined in the
C header files.
The hs-bindgen
Template-Haskell interface allows direct inclusion (a'la
#include
) of C header files into our Haskell source code files. We rebuild the
same application developed using the hs-bindgen
client with the
hs-bindgen
Template-Haskell interface.
hs-bindgen
uses libclang
to parse and interpret C header files.
libclang
is a part of the LLVM compiler infrastructure, which we need to
set up and connect to hs-bindgen
.
Nix, the package manager and build system, takes care of setting up the Clang
toolchain, the hs-bindgen
client, and the hs-bindgen
Template-Haskell
interface for us. In particular, this tutorial contains self-contained Nix
Flakes exposing the hs-bindgen
the client, and hs-bindgen
the
Template-Haskell interface, respectively. These Nix Flakes only export outputs
provided by an upstream Nix Flake which we maintain alongside hs-bindgen
.
You should use this upstream Nix Flake directly in your future projects, if you
decide to use the Nix package manager to manage your hs-bindgen
installation.
Install the Nix package manager, enable Nix Flakes, and try to build and run the client with
$ nix run ./pcap-client#hs-bindgen-cli -- --help | head -n 6
hs-bindgen - generate Haskell bindings from C headers
Usage: hs-bindgen [-v|--verbosity INT] [--log-as-info TRACE_ID]
[--log-as-warning TRACE_ID] [--log-as-error TRACE_ID]
[--log-as-error-warnings] [--log-enable-macro-warnings]
[--log-show-time] [--log-show-call-stack] COMMAND
The build uses the default NixOS binary cache, but some dependencies are
hs-bindgen
-specific and compilation will take a few minutes. The
hs-bindgen-cli
package derivation uses the default version of GHC provided by
Nixpkgs, and also takes care of installing the default version of the required
parts of the Clang toolchain.
Note
At the time of writing (October 3, 2025),
- the default version of GHC is 9.8.4;
- the Clang toolchain includes version 19.1.7 of packages
llvmPackages.clang
,llvmPackages.libclang
, andllvmPackages.llvm
.
Tip
- If you are interested in how
hs-bindgen
finds included headers, see thehs-bindgen
manual section on includes. - If you want to analyze how
hs-bindgen
finds the Clang toolchain, see Section System environment of this tutorial. - If you want to use a specific version of GHC or the Clang toolchain, see the relevant section below.
We have prepared a small project that generates bindings for libpcap
and
uses them to list the network devices found on your machine. Change your current
working directory to this sub-project,
$ cd pcap-client
Run the the application
$ nix run .#pcap-client
This should print a list of network devices found on your machine.
Note
We did not check in the generated bindings! The derivation generates the bindings during the build process. That is, you can run the application without manually generating bindings yourself!
A Nix development shell provides access to the Haskell toolchain, the
hs-bindgen
client, the Clang toolchain, and the libpcap
library (header
files and compiled shared object files). The simplified, relevant code from the
Nix Flake is:
pcap-client = haskellPackages.callCabal2nix "pcap-client" ./. { }; # Simplified.
...
devShells.default = haskellPackges.shellFor {
packages = _: [ pcap-client ];
nativeBuildInputs = [
...
# `hs-bindgen` client.
pkgs.hs-bindgen-cli
# Connect `hs-bindgen` to the Clang toolchain and `libpcap`.
pkgs.hsBindgenHook
];
};
Interestingly, hsBindgenHook
picks up libpcap
, which is defined as a
dependency in the Cabal file. Enter the development shell
$ nix develop
Let's analyze the environment set up by hsBindgenHook
:
$ echo $BINDGEN_EXTRA_CLANG_ARGS
...
-isystem /nix/store/0crnzrvmjwvsn2z13v82w71k9nvwafbd-libpcap-1.10.5/include
...
The environment variable BINDGEN_EXTRA_CLANG_ARGS
is used by hs-bindgen
and
forwarded to libclang
. For details, see the hs-bindgen
manual section on
Clang options.
Then, generate bindings with the provided script:
$ ./generate-bindings
The generate-bindings
script is well documented, please have a look at
the different command line flags. In particular, analyze the parse and select
flags which determine the set of translated declarations. In the following, we
will highlight some selected command line flags:
--unique-id
: C does not have explicit namespaces but only maintains separate declaration spaces (e.g,struct foo
vsfoo
). We use a unique identifier to discriminate global C identifiers, ensuring that bindings do not clash. This is also relevant when libraries have common dependencies, and external binding specifications are not used.- Parse and select predicates: Parse predicates determine the
declarations
hs-bindgen
tries to parse and reify; select predicates determine the declarations to translate. --select-by-header-path
: Select all declarations in header files with file paths matching the provided Perl-compatible regular expression. By default,hs-bindgen
selects all declarations in the provided main header file. However, the main headerpcap.h
does not declare anything but only imports sub-headers, so we need to provide this option.--enable-program-slicing
: Do not only select declarations that match the select predicate but all transitive dependencies.
We generated the script using an iterative procedure, adding and removing
command line flags as required. The script should generate several files in
folder ./src/Generated/
that you are encouraged to inspect. In particular, we
separate bindings into modules exposing different binding categories. For
example. ./src/Generated/Pcap.hs
exposes types, whereas
./src/Generated/Pcap/Safe.hs
and ./src/Generated/Pcap/Unsafe.hs
expose
safe
and unsafe
versions of foreign imports. The Safe
and Unsafe
modules export the same identifiers, and the user has to decide on user one of
them, or imported them qualified.
After generating the bindings, compile and run the minimal application using standard commands. We have prepared a Cabal package:
$ cabal build
Have a look at the application code ./app/Pcap.hs
.
Building the project requires the libpcap
shared object files which are
provided by Nix,
$ echo $NIX_CFLAGS_COMPILE
...
-isystem /nix/store/0crnzrvmjwvsn2z13v82w71k9nvwafbd-libpcap-1.10.5/include
...
You can also set the package.<name>.extra-include-dirs
and
package.<name>.extra-lib-dirs
stanzas in your cabal.project
or
cabal.project.local
files.
On my machine, running the program produces the following output:
$ cabal run
List of network devices found on your machine:
- wlp0s20f3
- any
- lo
- enp0s13f0u3u4u4
- nflog
- nfqueue
We use the types and Doxygen comments to create documentation for the generated
bindings. The Haskell pipeline implemented in Nix builds documentation by
default, which can be accessed quite easily. Create a symlink result-doc
to
the documentation:
$ nix build .#pcap-client.doc
On my machine, the path to the documentation is
result-doc/share/doc/pcap-client-0.1.0.0/html/index.html
libpcap
does not provide Doxygen comments, and the documentation only contains
type signatures and location information; but even so, the documentation is
already quite useful.
hs-bindgen
can also create include graphs for you. In particular, we can
create and visualize the include graph for libpcap
. To this end,execute
$ ./generate-include-graph
Include graphs can be tremendously helpful while adapting the command line flags to parse and select the desired declarations.
The Template-Haskell (TH) interface of hs-bindgen
allows direct inclusion
(a'la #include
) of C header files into Haskell source code. Thereby,
hs-bindgen
generates Haskell bindings to the C header files at compile time.
This has the advantage that the user does not need to perform additional
compilation steps, but can directly use the generated bindings. Also, changes to
the C header files directly propagate into the Haskell source code, and there is
no need to manage additional files containing the generated bindings.
In TH mode, it may be harder for you to tune the hs-bindgen
configuration,
especially when translating C libraries that require detailed configuration.
Also, the generated bindings are less readily available, and we need compiler
flags to observe them (see the Section Inspect generated bindings below).
Change your current working directory to the pcap-th
sub-project using the TH
interface of hs-bindgen
,
$ cd pcap-th
Build and run the application,
$ cabal run
The output should be the same list of network devices as before.
The provided Nix development shell is similar to the one from pcap-client
;
however, please note the following workaround to connect Haskell Language Server
with the libpcap
:
devShells = {
default = hpkgs.shellFor {
packages = _: [ pcap-th ];
...
# We need to add the `libpcap` library to `LD_LIBRARY_PATH` manually
# here because otherwise Haskell Language Server does not find it.
# Nix tooling ensures that other parts of the Haskell toolchain
# (e.g., `cabal`, `ghc`) find the shared libraries of dependencies
# without the need to temper with `LD_LIBRARY_PATH`.
shellHook = ''
LD_LIBRARY_PATH="${pkgs.libpcap.lib}/lib''${LD_LIBRARY_PATH:+:''${LD_LIBRARY_PATH}}"
export LD_LIBRARY_PATH
'';
};
};
Enter the provided development shell
$ nix develop
and inspect the application code ./app/Pcap.hs
. The development shell
provides the Haskell Language Server (HLS), and ensures HLS can compile the
project and link to the shared pcap
library.
The TH function generating the hs-bindgen
splice is
let headerHasPcap = BIf $ SelectHeader $ HeaderPathMatches "pcap.h"
isDeprecated = BIf $ SelectDecl DeclDeprecated
hasName = BIf . SelectDecl . DeclNameMatches
isExcluded =
BOr (hasName "pcap_open")
$ BOr (hasName "pcap_createsrcstr")
$ BOr (hasName "pcap_parsesrcstr")
$ BOr (hasName "pcap_findalldevs_ex")
$ BOr (hasName "pcap_setsampling")
(hasName "pcap_remoteact")
selectP = BAnd headerHasPcap
$ BAnd (BNot isDeprecated)
(BNot isExcluded)
cfg :: Config
cfg = def
& #parsePredicate .~ BTrue
& #selectPredicate .~ selectP
& #programSlicing .~ EnableProgramSlicing
cfgTH :: ConfigTH
cfgTH = ConfigTH { safety = Safe }
in withHsBindgen cfg cfgTH $ hashInclude "pcap.h"
Most of this code defines the appropriate parse and select predicates; compare with the respective command line flags of the client example.
Some notes:
- In TH mode, we do not have to set a
unique-id
;hs-bindgen
automatically generates one using TH features (Language.Haskell.TH.location
).
Also in TH mode, hs-bindgen
generates documentation for translated functions,
and HLS can show the automatically generated documentation. For example,
navigate your cursor to pcap_findalldevs
pcap_findalldevs :: Ptr (Ptr Pcap_if_t) -> Ptr CChar -> IO CInt
Defined at /path/to/Pcap.hs:24:1
__C declaration__: pcap_findalldevs
__defined at__: pcap/pcap.h:795:14
__exported by__: pcap.h
Further, we can inspect the code generated during compile time using GHC
options. In particular, we can debug the compiler using ddump-slices
by
adding
{-# OPTIONS_GHC -ddump-splices #-}
to the top of the file. For example, the generated code corresponding to the documentation
of pcap_finalldevs
above is
foreign import ccall safe
"hs_bindgen_hspcap0_1_0_0inplacehspcapbin_172d2c8dfa18cccf" pcap_findalldevs
:: Foreign.Ptr (Foreign.Ptr Pcap_if_t)
-> Foreign.Ptr C.CChar -> IO C.CIn
Tip
Have a look at section about setting the GHC or LLVM toolchain versions.
The Nix Flake wraps the client hs-bindgen-cli
so that it knows where the Clang
toolchain is installed. We use a binary wrapper, and direct inspection of the
environment is cumbersome. However, we can use hs-bindgen-cli
itself to report
the system environment it is picking up:
nix run .#hs-bindgen-cli -- info libclang -v4
For example,
[Info ] [HsBindgen] [extra-clang-args] Picked up evironment variable BINDGEN_EXTRA_CLANG_ARGS; parsed 'libclang' arguments: ["-B/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0/lib/gcc/x86_64-unknown-linux-gnu/14.3.0","--gcc-toolchain=/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0","-B/nix/store/10mkp77lmqz8x2awd8hzv6pf7f7rkf6d-clang-19.1.7-lib/lib","-nostdlibinc","-resource-dir=/nix/store/fbfcll570w9vimfbh41f9b4rrwnp33f3-clang-wrapper-19.1.7/resource-root","-idirafter","/nix/store/gf3wh0x0rzb1dkx0wx1jvmipydwfzzd5-glibc-2.40-66-dev/include","-fmacro-prefix-map=/nix/store/gf3wh0x0rzb1dkx0wx1jvmipydwfzzd5-glibc-2.40-66-dev/include=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-2.40-66-dev/include","-frandom-seed=76bkkqxi8g"]
[Info ] [HsBindgen] [builtin-include-dir] BINDGEN_BUILTIN_INCLUDE_DIR set: BuiltinIncDirDisable
In particular (see the Clang command line argument reference),
-B/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0/lib/gcc/x86_64-unknown-linux-gnu/14.3.0
, and--gcc-toolchain=/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0
: Use and search GCC toolchain for executables, libraries, and data files.-B/nix/store/10mkp77lmqz8x2awd8hzv6pf7f7rkf6d-clang-19.1.7-lib/lib
, and-resource-dir=/nix/store/fbfcll570w9vimfbh41f9b4rrwnp33f3-clang-wrapper-19.1.7/resource-root
: Use and search the Clang toolchain for executables, libraries, and data files. Theresource-dir
is particularly important, because it contains the headers of the C standard library. We leths-bindgen
know that we specified theresource-dir
directly, so that it does not have to perform heuristic search (BINDGEN_BUILTIN_INCLUDE_DIR=disable
environment variable).-nostdlibinc
: Disable standard system#include
directories only.-idirafter /nix/store/gf3wh0x0rzb1dkx0wx1jvmipydwfzzd5-glibc-2.40-66-dev/include
: Fall back to theglibc
standard library headers.
Other options not discussed here:
-fmacro-prefix-map=/nix/store/gf3wh0x0rzb1dkx0wx1jvmipydwfzzd5-glibc-2.40-66-dev/include=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-2.40-66-dev/include
,
and -frandom-seed=76bkkqxi8g
.
We also provide a setup hook that can be used by projects depending on
hs-bindgen
during their build process. The hs-bindgen
setup hook
performs the same setup as the wrapper discussed in the section Client
wrapper above. The hs-bindgen
setup hook can be used like other setup
hooks by adding it to buildInputs
or propagatedBuildInputs
.
To inspect the hs-bindgen
setup hook, run
nix build -o hs-bindgen-hook .#hsBindgenHook
cat hs-bindgen-hook/nix-support/setup-hook
For example,
# Populate additional environment variables required by `hs-bindgen`.
# NOTE: Use this setup hook when building packages with `hs-bindgen`. The client
# requires a separate wrapper (doh !) which is defined in `hs-bindgen-cli.nix`.
# Please keep this setup hook and the wrapper synchronized!
populateHsBindgenEnv() {
# Inform `hs-bindgen` about Nix-specific `CFLAGS` and `CCFLAGS`. In contrast
# to `rust-bindgen-hook.sh` (see Nixpkgs), we do not set `CXXFLAGS`.
BINDGEN_EXTRA_CLANG_ARGS="$(</nix/store/fbfcll570w9vimfbh41f9b4rrwnp33f3-clang-wrapper-19.1.7/nix-support/cc-cflags) $(</nix/store/fbfcll570w9vimfbh41f9b4rrwnp33f3-clang-wrapper-19.1.7/nix-support/libc-cflags) $NIX_CFLAGS_COMPILE"
export BINDGEN_EXTRA_CLANG_ARGS
# Inform `hs-bindgen` that it does not have to perform heuristic search for
# the builtin include directory. (We set the builtin include directory using
# `BINDGEN_EXTRA_CLANG_ARGS`).
BINDGEN_BUILTIN_INCLUDE_DIR=disable
export BINDGEN_BUILTIN_INCLUDE_DIR
# ...
}
postHook="${postHook:-}"$'\n'"populateHsBindgenEnv"$'\n'
One possibility to specify the GHC toolchain is to simply use a different
Haskell package set. For example, building the pcap-client
project with GHC
9.12 only requires a small change in the Nix Flake:
...
hpkgs = pkgs.haskell.packages.ghc912;
...
Changing the version of the Clang toolchain requires an overlay. For example,
using libclang
version 20 with the pcap-client
project:
useLlvm20 = final: prev: {
llvmPackages = final.llvmPackages_20;
};
pkgs = import nixpkgs {
inherit system;
overlays = [
hs-bindgen.overlays.default
useLlvm20
];
};
Note that even when you have clang
version 19 in your path, hs-bindgen
uses
clang
version 20 when the above overlay is activated. You can see this by
inspecting BINDGEN_EXTRA_CLANG_ARGS
when the development shell is active:
$ echo $BINDGEN_EXTRA_CLANG_ARGS
...
-resource-dir=/nix/store/8s647qbgn3yy2l52ykznsh0xkvgcrqhx-clang-wrapper-20.1.8/resource-root
...
Important
Last update: October 3, 2025. The upstream Nix Flake may have received updates in the meantime.