Haskell World logo Haskell WorldWrite code, build worlds
Systems

Haskell FFI for Calling C Libraries Without Crashes

The first time I wrapped a C library with the Haskell FFI, the service ran for eleven days before it segfaulted at three in the morning.

Haskell source with a foreign import declaration beside the C header it binds

The first time I wrapped a C library with the Haskell FFI, the service ran for eleven days before it segfaulted at three in the morning. The bug was a single word in a foreign import: I had marked a call as unsafe because a blog post said it was faster, and the C function occasionally blocked on a socket for two seconds. Every Haskell thread on that capability froze with it, a health check timed out, and the orchestrator killed the process mid write.

The Haskell FFI is small. You can learn the syntax in an afternoon. What takes longer is learning which decisions the type checker cannot see: who owns a pointer, whether the garbage collector may move a buffer, and what happens to the runtime while C code is running. This guide covers those decisions in the order they tend to bite.

What the Haskell FFI Actually Promises

A foreign import gives a C function a Haskell type, and GHC generates the glue to marshal arguments across. That is the whole contract. GHC trusts the type you wrote. If the C header says the function takes a size_t and you declare CInt, nothing complains at compile time, and on a 64 bit machine you now pass garbage in the upper bits of a register.

So the first rule is boring and saves the most time: copy types from the header, not from memory. Use CSize for size_t, CLong for long, Ptr CChar for char pointers, and never plain Int for a C int.

Safe Versus Unsafe Calls and the Two Microsecond Rule

Every foreign import is either safe or unsafe. Neither word is about memory safety; they describe what the runtime does while the call runs.

  • Unsafe calls jump straight into C. Overhead is a few nanoseconds, but the calling capability cannot run other Haskell threads or take part in garbage collection until the call returns.
  • Safe calls release the capability first, so other Haskell threads keep running and the GC can proceed. Overhead is roughly 100 to 200 nanoseconds on current hardware with the threaded runtime.

My working rule: if a call can ever take longer than about two microseconds, or can ever block, it is safe. That includes anything touching files, sockets, locks, or a callback into Haskell. Unsafe is for pure, bounded arithmetic like a hash function on a short buffer or a getter on a struct field. If you link without the threaded runtime, a safe call still blocks every Haskell thread, so pass the threaded flag for any service that calls into C.

Memory Ownership Is the Real Interface

C libraries document ownership in prose, if at all. Before writing a single binding, answer three questions for every pointer that crosses the boundary: who allocates it, who frees it, and how long it must stay valid.

SituationHaskell toolWhy
Buffer needed only during one callallocaBytes or withCStringFreed automatically when the scope ends
C returns memory you must freeForeignPtr with a finalizerTies the free to the Haskell value lifetime
C keeps the pointer after returningmallocBytes plus explicit freeGHC must not reclaim memory C still reads
Opaque handle such as a context structnewtype around ForeignPtrStops callers from mixing handles

The third row causes most of the crashes I have debugged in other people's code. A function like a logging library's set buffer call stores your pointer and writes to it later. If you passed memory from allocaBytes, that stack memory is gone the moment your Haskell scope exits, and the library writes into whatever lives there next.

Why Finalizers Are Not a Cleanup Strategy

The common advice is to wrap every C resource in a ForeignPtr and let the finalizer free it. For memory that is fine. For anything scarce, I disagree.

Finalizers run when the garbage collector notices the value is unreachable. In a service with a large heap and a quiet allocation pattern, that can be minutes. I once watched a process exhaust its 1024 file descriptor limit because each database handle was closed only by a finalizer, and the major GC ran every four minutes.

For file handles, sockets, locks and database connections, use bracket and close the resource explicitly. Keep the ForeignPtr finalizer as a backstop, not the plan. If you have not read your GC statistics before, reading the GHC RTS report will show you how rarely major collections can run in practice.

Pinned Memory and Passing ByteStrings to C

GHC's copying collector moves ordinary heap objects. C does not know this. If you hand C the address of an unpinned byte array and a GC happens during a safe call, C is now reading memory that has been relocated.

Strict ByteString is pinned, which is why useAsCStringLen and unsafeUseAsCStringLen work well for passing bytes in. Large byte arrays above roughly 3 kilobytes are also allocated pinned. Text in version 2 and later is not pinned, so convert to a ByteString or copy into a pinned buffer before the call.

Callbacks, Exceptions and Threads

When C calls back into Haskell, three things go wrong regularly.

Exceptions must not escape

A Haskell exception that propagates out of a callback unwinds through C stack frames that know nothing about it. Catch everything inside the callback, store the error in an IORef or MVar, and return an error code to C.

FunPtr values leak until freed

Each wrapper call allocates a small stub. Call freeHaskellFunPtr when C no longer holds the pointer, or a long running process accumulates thousands of them.

Thread local state in C

Some libraries, OpenGL and many GUI toolkits among them, require every call from the same OS thread. Haskell green threads migrate between OS threads freely. Use forkOS or runInBoundThread for that work, and remember that the main thread is already bound.

If the callback updates shared state that other Haskell threads read, STM for shared state is usually cleaner than locking inside the callback.

A Binding Checklist Before Shipping

  1. Every type matches the C header, including signedness and width.
  2. Every call that can block or exceed two microseconds is marked safe.
  3. Every pointer has a written owner and lifetime in a comment beside the import.
  4. Scarce resources close through bracket, not finalizers alone.
  5. No unpinned memory is passed to a safe call.
  6. Callbacks catch all exceptions and free their FunPtr.
  7. A stress test runs the binding from 50 or more concurrent Haskell threads for at least an hour.

The last item finds more bugs than all the others combined, because ownership mistakes only show up when the GC runs at an unlucky moment.

FAQ

Should I use hsc2hs, c2hs or write imports by hand?

For a library with a few functions, write them by hand. Once you need struct offsets or constants from headers, hsc2hs ships with GHC and is enough for most projects. c2hs saves effort on large APIs but adds a build tool to maintain.

Is calling C from Haskell slower than calling it from C?

An unsafe call costs nanoseconds and is close to a native call. A safe call costs a few hundred nanoseconds. That only matters in tight loops, where batching work into one C call is the usual fix.

Can I call C++ libraries directly?

Not directly. Write a thin C wrapper with extern C functions around the C++ API, catch C++ exceptions inside it, and bind that wrapper from Haskell.

The Practical Takeaway

Treat a Haskell FFI binding as a small systems component, not a type declaration. Write down who owns each pointer, default to safe calls, close scarce resources explicitly, and stress test under concurrency before trusting it in production. For more on running Haskell close to the operating system, browse the systems articles on this site.

FB
Finnian Burke

Finnian Burke spent eight years building distributed systems before leaving to write about the programming patterns that actually hold up under pressure. He has worked with Haskell, Rust, and Go in production, and writes mostly about the parts of functional programming that turn out to be useful regardless of the language you use day to day.

More posts by Finnian

More in Systems

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten qvantor rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr webtech1223411 wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto