
Calling C++ and Go from Dart using FFI
Calling native libraries from Dart with dart:ffi, and which languages work.
Phanith LIM
dart:ffi lets Dart call into a shared library (.so or .dylib) directly. No platform channels, no plugin, no Flutter needed.
That is worth having when the code you want already exists in C or C++ and you would rather bind to it than port it, or when a hot loop is faster in a compiled language than in Dart. The cost is that you now have a build step per platform and a pointer or two to look after.
I wired up C++ and Go this way and worked out which other languages can join in. Both are below.
The one rule
Dart can only call functions that expose a C ABI: a plain C-compatible symbol with no name mangling. Every language reaches that differently, but the Dart side never changes: open the library, declare the types, look up the function.
C++
C++ mangles function names, so add becomes something unrecognisable. extern "C" turns that off.
// native/cpp/mylib.cpp
#include <cstdint>
extern "C" {
int32_t add(int32_t a, int32_t b) {
return a + b;
}
}
Build it with CMake:
cmake -S native/cpp -B native/cpp/build -DCMAKE_BUILD_TYPE=Release
cmake --build native/cpp/build
Go
Go exports through CGo. Two details will bite you if you miss them: the file needs package main with an empty func main(), and //export must sit on the line directly above the function, with no blank line between.
// native/go/mylib.go
package main
import "C"
//export multiply
func multiply(a, b int32) int32 { return a * b }
//export fibonacci
func fibonacci(n int32) int64 {
if n <= 1 { return int64(n) }
a, b := int64(0), int64(1)
for i := int32(2); i <= n; i++ { a, b = b, a+b }
return b
}
func main() {}
Build:
cd native/go && CGO_ENABLED=1 go build -o ../libs/libgolib.so -buildmode=c-shared .
The Dart side
This part is the same no matter which language produced the library. Declare the native signature, declare the Dart signature, then lookupFunction.
// lib/src/go_bindings.dart
import 'dart:ffi';
import 'package:path/path.dart' as p;
typedef _FibonacciNative = Int64 Function(Int32 n);
typedef FibonacciDart = int Function(int n);
class GoLib {
final DynamicLibrary _lib;
late final FibonacciDart fibonacci;
GoLib(String libsDir)
: _lib = DynamicLibrary.open(p.join(libsDir, 'libgolib.so')) {
fibonacci = _lib.lookupFunction<_FibonacciNative, FibonacciDart>('fibonacci');
}
}
Then call it like ordinary Dart:
final go = GoLib(libsDir);
print(go.fibonacci(10)); // 55
Numbers are the easy case. They copy across on their own, and nothing needs cleaning up afterwards.
Strings do not. A Pointer<Utf8> is memory you asked for, so you convert with toNativeUtf8(), pass the pointer, read the reply back with toDartString(), and free what you allocated. Forget the free and you have a leak that no garbage collector will find for you.
Which languages work
Anything that compiles to a shared library with C-compatible symbols.
Works
| Language | Export mechanism |
|---|---|
| C | Native, no wrapper needed |
| C++ | extern "C" { ... } block |
| Go | CGo //export + -buildmode=c-shared |
| Rust | #[no_mangle] extern "C" fn + crate-type = ["cdylib"] |
| Zig | export fn + compile as shared library |
| Nim | {.exportc, dynlib.} pragma |
| D | extern(C) linkage |
| Kotlin/Native | @CName annotation (not Kotlin/JVM) |
| Swift | @_cdecl("name") on a free function |
| Fortran | ISO_C_BINDING + bind(C) attribute |
Does not work
| Language | Reason |
|---|---|
| Python | No C ABI export, would need embedding CPython |
| Java | JVM bytecode only; JNI goes the other direction |
| Kotlin (JVM) | Runs on the JVM, no C ABI |
| Scala | JVM, no C ABI |
| C# / .NET | CLR managed runtime, no native C ABI export |
| JavaScript / TypeScript | Runs in a JS engine, no C ABI |
| Ruby | Interpreter-based, no C ABI export |
| PHP | Interpreter-based, no C ABI export |
The pattern behind both tables: if the language can produce a .so with plain C symbol names, Dart FFI can call it. If it needs its own runtime to be alive first, it cannot.
Type mappings
C and C++
| Dart FFI type | C / C++ type |
|---|---|
Int8 | int8_t |
Int16 | int16_t |
Int32 | int32_t |
Int64 | int64_t |
Float | float |
Double | double |
Bool | bool |
Void | void |
Pointer<Utf8> | char* |
Pointer<T> | T* |
Go (via CGo)
| Dart FFI type | Go / CGo type |
|---|---|
Int32 | int32 / C.int |
Int64 | int64 / C.longlong |
Float | float32 / C.float |
Double | float64 / C.double |
Bool | bool / C.int (0/1) |
Pointer<Utf8> | *C.char |
Pointer<Int32> | *C.int |
Rust
| Dart FFI type | Rust type |
|---|---|
Int32 | i32 |
Int64 | i64 |
Float | f32 |
Double | f64 |
Bool | bool |
Pointer<Utf8> | *const c_char |
Pointer<T> | *mut T |
Kotlin/Native
| Dart FFI type | Kotlin type |
|---|---|
Int32 | Int |
Int64 | Long |
Float | Float |
Double | Double |
Bool | Boolean |
Pointer<Utf8> | CPointer<ByteVar>? |
Pointer<Int32> | CPointer<IntVar>? |
Swift
| Dart FFI type | Swift type |
|---|---|
Int32 | Int32 |
Int64 | Int64 |
Float | Float |
Double | Double |
Bool | Bool |
Pointer<Utf8> | UnsafePointer<CChar>? |
Pointer<Utf8> | UnsafeMutablePointer<CChar>? (out) |
Source
Source code: phanithlim/dart-native
Everything lives under native/, one directory per language, each with its own README and build steps. Clone it, build one of the libraries, and the Dart side will call straight into it.