Build and deploy native C libraries with Flutter

J-P Nurmi
Flutter Community
Published in
6 min readOct 12, 2020

Foreign Function Interface aka FFI is a neat mechanism for supercharging your Dart and Flutter apps by harnessing the power of native C libraries.

Thanks to dart:ffi, one does not need to reinvent the wheel in Dart, but one can grab a ready-made C library out there, and call the C APIs directly from Dart.

Even though dart:ffi is still in beta, it is already becoming popular. There are already dozens of FFI-based packages on pub.dev. FFI is arguably playing an important role in the Dart and Flutter ecosystems, as it makes it possible to utilize existing native libraries interfacing hardware, networking, databases, and an endless amount of other things, without spending time and effort porting them to Dart.

When calling C from Dart, some plumbing is necessary to connect the two worlds. In essence, two things are needed; 1) the C API must be bound to Dart, and 2) the C library and its symbols must be made available in one way or another. There is a nice ffigen utility that can help with the former by generating Dart bindings from C headers. As for the latter, the process depends on the library and the chosen deployment method.

Consider the following categories of libraries:

  • Static libraries
  • Shared system libraries
  • Shared non-system libraries

Static libraries and shared system libraries are easiest to handle. Static libraries must be linked to at build time, which makes it possible to access them at runtime via DynamicLibrary.executable(). Shared system libraries do not require linking — they can be accessed at runtime via DynamicLibrary.open(String name) by simply passing the name of the library. Given that system libraries are available in the search path of the dynamic loader, it is not necessary to specify a path. Things get a bit more complicated with the last category, shared non-system libraries. These are libraries that are not necessarily available in the search path, so DynamicLibrary may need help with locating the library on some platforms.

Even though things tend to get a bit complicated with shared non-system libraries, there is an important use case for them. A mandatory disclaimer here that I am not a lawyer specialized to open source licenses, but the most prominent case is to make use of libraries licensed under weak copyleft licenses, such as LGPL, in closed source proprietary applications. Since the other two categories are fairly straight-forward, the rest of the article focuses on building and deploying specifically shared non-system libraries.

Flutter is a cross-platform framework and in general, makes it really easy to target multiple platforms. However, when building and deploying additional C libraries for each target platform, Flutter cannot abstract it away — you need to play by the rules of each target platform. Even though it is possible to distribute prebuilt libraries using such services as Maven repositories, it is often less hassle to build a native library on the fly as part of a Flutter app or plugin. Notice that this approach works particularly well if the size of the C library is reasonable, and the build does not take awfully long.

What it comes to building C libraries, the most common build systems are Autotools and CMake. Autotools is more common among older C libraries, whereas CMake has become a popular choice during the past half a decade or so. Autotools tends to be a bit more tedious to work with and may involve e.g. manually pre-generating a config.h file for some target platforms, but CMake-based libraries are straightforward to integrate with Flutter.

The following sections focus on the CMake side of things and walk through the steps of integrating C libraries on each platform. These are also partly covered by the Binding to native code using dart:ffi page in the Flutter docs, even though the docs do not yet cover Windows and Linux desktop platforms that are still in alpha.

Android

The first and foremost prerequisite for building native C libraries on Android is to have the Android NDK installed. The NDK provides everything that is needed for building native libraries for Android. The rest is handled by Gradle, which has built-in support for external native builds. Building a native library with Gradle and CMake is a matter of specifying a path to the library’s CMakeLists.txt in build.gradle:

Integrating an external library on Android

Depending on the library, it may be necessary to specify compiler options and such. Basically, the externalNativeBuild block alone is enough to build and deploy a native library on Android. The library is automatically built for each target architecture, and the lib-directory in the resulting APK looks something like:

lib/
├── arm64-v8a
│ └── libfoo.so
├── armeabi-v7a
│ └── libfoo.so
├── x86
│ └── libfoo.so
└── x86_64
└── libfoo.so

As such, this is enough for DynamicLibrary.open('libfoo.so') to work without having to specify a path. That’s all for Android.

Windows

On Windows, Flutter uses CMake for building the native bits of app runners and plugins. To integrate an extra native library to the build system, it is enough to specify it in windows/CMakeLists.txt:

Integrating an external library on Windows

Notice that the name in $<TARGET_FILE:foo> must match the external library target name in path/to/foo/CMakeLists.txt. In any case, specifying the library in flutter_xxx_bundled_libraries makes Flutter deploy the resulting DLL next to the application executable, which in turn allows opening the library with DynamicLibrary.open('libfoo.dll') without having to specify a path.

Linux

The steps for integrating an external library on Linux are otherwise the same as on Windows, except that locating the library at runtime requires some additional steps. Here’s an example that utilizes the Flutter plugin code to resolve the correct path, and uses an environment variable to pass the information to Dart:

Flutter: integrating an external CMake-based library on Linux

Flutter’s build system bundles the external library into a lib subdirectory next to the application executable:

lib/
├── libflutter_linux_gtk.so
├── libflutter_foo_plugin.so
└── libfoo.so

Notice that to locate the library at runtime, one cannot rely on the current working directory, which could be anything depending on how the application is launched. It must be resolved relative to the application executable. Above, we’re using a helper function borrowed from the Flutter engine’s Linux shell to do that. Once we have the correct location resolved, we can use the environment variable to lookup the library in Dart: DynamicLibrary.open(Platform.environment['LIBFOO_PATH']).

iOS & macOS

Last but not least, the fruity platforms. According to the Flutter docs, it should be possible to specify external native libraries in Xcode. I am not exactly sure how that translates to the build system that Flutter has set up for us, though, and there is also an annoying limitation that CocoaPods does not allow including sources above the podspec file.

Instead of letting this limitation determine how to structure your repository and where to place third party code, an alternative option is to make a separate podspec for the external library and include it as a dependency for the Flutter app or plugin. There are plenty of pure C-libraries on CocoaPods, that can be used as a starting point. The rest of creating a podspec is left as an exercise to the reader.

Once you have a Pod for the external library, it can be included as a dependency in the podspec of the Flutter app or plugin. The build system takes care of bundling it as appropriate. To help to locate the library in Dart, we can resolve the path in Swift where we have access to the bundle, and pass the information as an environment variable similarly to how we previously did on Linux.

Integrating external libraries on macOS

That’s it! Hopefully, this article helps you creating FFI packages that work out of the box in the sense that users don’t need to figure out how to build and deploy native dependencies, which was the case with my first FFI package.

--

--