How to make your own native library bindings with Dart FFI
14 Jun 2021
Personally I find I learn a subject best when I have to make use of that knowledge in the context of a project. So while I was keen to learn about Dart's FFI (Foreign Function Interface) functionality when it was released in beta almost 2 years ago, I didn't really have any project to drive that use until now. The project that prompted me to really dive in and learn Darts FFI was my need to access the ALSA API, to allow me to play back audio on Linux from Dart.
So join me for this first part in a series of articles, where I'll try to cover from start to finish what you need to know to be able to make use of a C library API directly from Dart and Flutter.
While there already exist packages to playback audio for Flutter, for my Firetribe project, what I needed was the ability to playback audio data, generated by my own code, not just from existing audio files and from a command line Dart app as well as from a Flutter app. While I initially considered using a cross-platform wrapper such as Miniaudio and am still intending to make use of that from Dart, I decided to go the route of directly using ALSA after seeing how Morten Boye Mortensen made use of ALSA directly via FFI to add Linux support in his excellent Flutter Midi Command package.
So in this series of articles, I will be using developing my dart_alsa package for code examples.
ffigen is your friend
While its completely possible to use Darts FFI to call a C library by writing all the binding code by hand, this can be a very long and tedious task when it comes to dealing with libraries like ALSA which have a huge API surface area.
So instead of doing it all by-hand, there is the ffigen package, that will generate the boilerplate binding code you and is the approach I took for the dart_alsa package.
The instructions for using the package are very good, though it may be a bit difficult to make your way through all of the available options in your first time using it, so looking at the example my use of several options may be of help, especially in regards to the need I had to include some compiler options for header (include) paths as well as renaming of structs that used underscore prefixes which of course have a significant meaning in Dart.
Using ffi and ffi
One thing that is quite confusing when you first start using Dart FFI is the presence of two FFI imports in many source files. The reason for this is that there is both a core Dart library that is part of the Dart SDK:
dart:ffiand the FFI package
ffi/ffi.dartavailable on pub which contains:
Utilities for working with Foreign Function Interface (FFI) code, incl. converting between Dart strings and C strings encoded with UTF-8 and UTF-16.
Walking down the right path
In order to begin working with a library through FFI, we first need to make a call to "open" it and this should work for system installed libraries by jsut passing in the name, otherwise you need to pass in the pull path to the location of the library file (eg.
.so on Linux).
Pointing out the obvious
C is very much focused on the use of pointers, so having to deal with pointers is almost guaranteed in your Dart FFI code.
For the most part pointers are fairly straight forward to deal with in Dart FFI, though there are a few tips I picked up along the way looking at code examples scattered across the dart SDK and other repos, including:
a handy way of having a null pointer:
The need to allocate memory when needing a pointer to a pointer:
Passing a pointer (to a pointer) into the ALSA API when it wanted to allocate a memory structure and then hand back a pointer to it:
// Allocate parameters object alsa.snd_pcm_hw_params_malloc(paramsPtr);
As well as the simpler case of passing in the value of a pointer (vs the pointer itself) to C calls that want just a memory address value, note the use of the
.value getter on the Dart
All alone with the memory
With C's strong reliance on pointers, comes the need for manual memory management, unlike in Dart, where memory is managed for us by the Dart VM.
For FFI purposes, this mostly just comes down to the need to both manually allocate memory when required, prior to calling C functions via
calloc()and then importantly remembering to free up said memory after returning (and possibly making use of the results from the C function call) using
But wait, there's more!
In this first part, I've covered what you need to know to start calling a C library from Dart. In the next part in this series, I'll be covering how to wrap synchronous access to C calls into a asynchronous Dart API.