Run Go in Dart VM with dart:ffi

Introduction
There is a really cool feature with Dart named dart:ffi. It’s the ability of the dart VM to run code that isn’t written in Dart. Basically, you can execute nearly everything that compiles to a shared library. A shared library is a file that is intended to be used across multiple programs. It takes the form of a dll file on the windows platform or a .so file on the Linux platform.
To glue the shared library with our Dart code, we use the dart:ffi package. It will load the shared library and get the exported functions from it. We can write our shared library code with C, C++, Rust, and theoretically anything that can be built to a shared library. In this demo, we are going to use Go.
We are going to learn how to use low level code in our Dart application.
Demo
To try this out, we are going to write a function in Go, that sums two numbers. Then we will call this function from our Dart code and print the result. It’s pretty easy, but we can learn a lot just with this little demo. Final code can be found here
The Go part
We need a function that makes the sum of two numbers and returns the result. In Go, it looks like this :
func Sum(a int, b int) int {
return a + b
}
Very simple. But there is a problem, our Dart app can’t use this function like this. The reason is that Dart doesn’t know anything about the Golang int type. We must use a type system that is understandable by Dart.
To solve this, we use the C type system. It will be our common type interface. Dart knows about it (thanks to dart:ffi). So let’s rewrite our function like this :
import (
“C”
)func Sum(a C.int, b C.int) C.int {
return a + b
}
We have just replaced types from int to C.int.
The last step is to export the function from the shared library. To do this, we just add a comment on top of our function :
//export Sum
func Sum(a C.int, b C.int) C.int {
return a + b
}
And voilà, our function is ready to be used by Dart. Before that we must actually build our Go code to a shared library. It’s quite easy, just use the following command line
go build -buildmode=c-shared -o lib.a lib.go
It builds a shared library named lib.a with our file lib.go.
You can check that your function is well exported in the shared library by using the command nm -gU lib.a
Ok, perfect! We are all set up on the Go part. Now it’s time to go to the Dart side.
The Dart part
In this part we are going to do three things. First we will load the shared library, then we will pick up our function in it, finally we will call it.
Let’s create a class named FFIBridge that will be responsible for calling methods from the shared library.
class FFIBridge {}
Ok, so remember, the first thing is to load the shared library. We can do it, either statically or dynamically.
Statically means that our shared library will be bundled directly in our Dart program and loaded when it starts. The process is platform specific. You can find more info on this documentation. For the sake of simplicity, we are going to load our library dynamically.
Dynamically means that we will lazily load the shared library, when we want to use it. In order to do it, we must use the `DynamicLibrary` object, and give it the path to our shared library. Let’s add a private method named _getDynamicLibrary :
ffi.DynamicLibrary _getDynamicLibrary() {
final libraryPath = path.join(Directory.current.path, ‘go’, ‘lib.a’);return ffi.DynamicLibrary.open(libraryPath);
}
Perfect, we have loaded our library like professionals. Next thing, pick up functions from it.
There is an attribute named lookup that is used to lookup for a specific function in our shared library. We are going to use it. Let’s add a _lookup attribute to the FFIBridge class, and pass it the value of the `lookup` attribute of our shared library.
class FFIBridge {
/// Holds the symbol lookup function.
late final ffi.Pointer<T> Function<T extends ffi.NativeType>(
String symbolName) _lookup;FFIBridge() {
_lookup = _getDynamicLibrary().lookup;
}
…
}
Now we lookup for functions in the shared library. It’s a good idea to create a method in our bridge to call the shared library one. Let’s add it to the class :
int sum(int a, int b) {
final sumFunctionInLibrary = _lookup<
ffi.NativeFunction<ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b)>>(
‘Sum’);final convertedToDartSumFunction =
sumFunctionInLibrary.asFunction<int Function(int a, int b)>(); return convertedToDartSumFunction(a, b);
}
As you can see, we lookup for a C typed function and return it as a Dart function. We use ffi.Int32 as an equivalent to our Go C.int. Finally, we execute the function we found.
Now that we have everything set up, we can write our program.
import ‘dart/ffi_bridge.dart’;void main() {
final ffibridge = FFIBridge();
final result = ffibridge.sum(2, 2);
print(result);
}
If we execute it, we will see the number 4 printed. We did it!
Conclusion
As we had seen, dart:ffi isn’t that hard to use. Drawbacks are that we rely on the low level C type, and that we need to glue everything up.
There is a great package named ffigen that creates our FFIBridge class automatically. It uses C headers to generate the class.
If you are on a mac M1 desktop, you might be struggling to compile your code. The solution for me was to install the ARM version of Dart from here. More info about that on this stackoverflow question