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 the hs-bindgen 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] COMMANDThe 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 13, 2025),
- the default version of GHC is 9.10.3;
- the Clang toolchain includes version 21.1.1 of packages
llvmPackages.clang,llvmPackages.libclang, andllvmPackages.llvm.
Tip
- If you are interested in how
hs-bindgenfinds included headers, see thehs-bindgenmanual section on includes. - If you want to analyze how
hs-bindgenfinds 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-clientRun the the application
$ nix run .#pcap-clientThis 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:
# Apply the overlay provided by the upstream Nix Flake.
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ hs-bindgen.overlays.default ];
};
# Collect the dependencies for the `pcap` client project.
pcap-client = haskell.lib.compose.generateBindings ./generate-bindings
(haskellPackages.callCabal2nix "pcap-client" ./. { });
...
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
];
};The overlay provided by the hs-bindgen Nix Flake:
- Adds
hs-bindgenrelevant packages to the Haskell package sets (i.e.,haskell.packages.ghc*). In particular, it also addslibclang-bindings, which is not yet available on Hackage nor in Nixpkgs. - Provides the function
generateBindingsin thehaskell.lib.composeattribute set. The functiongenerateBindingsexecutes the provided binding generation script during build. - Provides the
hs-bindgen-clias well ashsBindgenHookpackages.
Interestingly, hsBindgenHook picks up libpcap, which is defined as a
dependency in the Cabal file. Enter the development shell
$ nix developLet'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-bindingsThe 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 foovsfoo). 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-bindgentries 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-bindgenselects all declarations in the provided main header file. However, the main headerpcap.hdoes 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 choose one of them, or
import them qualified.
Tip
There is an excellent Haskell Unfoldr episode about safe and unsafe foreign function imports.
After generating the bindings, compile and run the minimal application using standard commands. We have prepared a Cabal package:
$ cabal buildHave 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
...NIX_CFLAGS_COMPILE is a Nix-specific environment variable. The wrapper for the
C compiler provided by Nix uses NIX_CFLAGS_COMPILE to inject extra C compiler
flags.
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
- nfqueueWe 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.docOn my machine, the path to the documentation is
result-doc/share/doc/pcap-client-0.1.0.0/html/index.htmllibpcap 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-graphInclude 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-thBuild and run the application,
$ nix runThe 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 developand 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-bindgenautomatically 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 CIntDefined at /path/to/Pcap.hs:24:1
__C declaration__: pcap_findalldevs
__defined at__: pcap/pcap.h:795:14
__exported by__: pcap.hFurther, 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.CInTip
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 -v4For 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-diris particularly important, because it contains the headers of the C standard library. We leths-bindgenknow that we specified theresource-dirdirectly, so that it does not have to perform heuristic search (BINDGEN_BUILTIN_INCLUDE_DIR=disableenvironment variable).-nostdlibinc: Disable standard system#includedirectories only.-idirafter /nix/store/gf3wh0x0rzb1dkx0wx1jvmipydwfzzd5-glibc-2.40-66-dev/include: Fall back to theglibcstandard 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-hookFor 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 13, 2025. The upstream Nix Flake may have received updates in the meantime.