Discreet Log #9: Flutter with Native Go Libraries
09 Jun 2021
Why
Languages, programming environments, and frameworks are all good for different things. When we started Cwtch we evaluated Go as a good candidate for building a system library. In our ongoing research we’ve also identified Rust as having excelent properties for making very safe critical apps. However neither of these langauges come equiped with frameworks for rapidly, easily and seamlessly building crossplatform UIs. If you find yourself in a similar situation starting out, or in a situation where there is a mature solution to a problem in a language that is also not well suited to UI building than you might find this overview of our work combinging a robust Go networking library, Cwtch, with Flutter to build a single UI and codebase for Windows, Android, and Linux.
As for why Flutter specifically? One of the most important priorities for Cwtch is maintaining privacy and anonymity for the user. For this reason, we started our evaluation of UI frameworks from a place of ruling out any and all HTML frameworks (Electron, React Native, etc) because the risk of one unescaped bit of HTML in a highly user content filled application getting rendered could pierce and destroy a user’s anonymity. This didn’t leave us with many options, but one that was new from the last time we looked, was Flutter, which was now boasting experimental and soon to be stable desktop support and big name support behind it.
Also a note: we’re still actively developing the Cwtch Flutter UI so our codebase will likely change in the future from what it is that we are showing here today, especially as we’re in the middle of adding Android Service support. I’ll make note of what is likely to change further.
Library API Design and Building
Most in language APIs offer a rich interface using the full extent of language features and data types. When making a cross language API, often and specifically in this case, there are a lot of constraints. Between dart:ffi
and gomobile
(which we’ll get into in the following sections) we were left with a greatly reduced set of data types and expressiveness that we could use. Our first step was then to build a new wrapper library, libcwtch-go, to offer a new and simpler and cross language suitable API. Often when we still need more complex data types, we serialized then to a JSON string on the Go side, pass the simple string, and the deserialize on the Dart or Kotlin side. This could be of performance consideration if you plan on moving a large amount of data over such an interface.
Desktop and FFI
“Foreign Function Interface” of ffi
are functionality offered in many languages to call code from dynamic libraries from other languages, usually using C shared object libraries as the standard. Dart comes with built in ffi functionality in the dart:ffi
module so that was clearly the way forward, at least on desktop.
The trick for this and as we’ll see later, the mobile case as well, is that we have to compile our Go code into a C style library (.so and .dll) to be accessible. This had a slight air of novelty to it since an important design principal in Go is no dynamic linking, in Go everything is compiled into one static binary, with no dependencies. So it was a little ironic to be packaging Go code in a dynamic library itself for use in another language. Thankfully while it seems slightly antithetical to Go, it is actually baked into the core go tooling in the form of cgo.
go build -buildmode c-shared -o libCwtch.so
To make DLLs for windows requires mingw
and with that, Go has built in support for Windows DLLs and even cross compiling to them. We use:
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc-win32 go build -buildmode c-shared -o libCwtch.dll
There is one more catch. Dart’s FFI expects a library with C style interface and data types, so we also have to write an API for our functions that meets that. Go supports that with the C
package, supplying a full set of C data type wrappers and converters.
lib.go
import "C"
...
//export c_GetMessages
func c_GetMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, start C.int, end C.int) *C.char {
profile := C.GoStringN(profile_ptr, profile_len)
The //export
comment is required and used by cgo
to name the function in the shared object.
Gomobile
Dynamic library management and use on mobile like Android is already harder than on desktop for reasons of packaging and how apps are deployed. But that isn’t the only challenge, Android apps also need to support both arm 7 32bit and arm64-v8 CPUs so you need to generate both (more cross compiling) and package them appropriately. We probably could have figured this out with time, and if you’re not using Go, then you may have to, but Go provides a tool for mobile development with this already figured out in gomobile. In this case, it cross compiles two libraries for the two architectures, and also generates a Java Runtime Environment (JRE) compatible API for them to be accessed on Android (and a Obj C version for iOs I believe).
gomobile bind -target android
This generates a cwtch.aar
with both arm7 32bit and arm64-v8 libraries inside it, and the JRE interface.
Putting The Library Together
gomobile
and dart:ffi
both require slightly different interfaces. Gomobile handles data type conversion from Go native to something that the JRE can use, on the other hand, Dart’s ffi expects C style functions and data as mentioned above. To manage this all, all our functions have two definitions that take the form of the following example
lib.go
//export c_GetMessages
func c_GetMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, start C.int, end C.int) *C.char {
profile := C.GoStringN(profile_ptr, profile_len)
handle := C.GoStringN(handle_ptr, handle_len)
return C.CString(GetMessages(profile, handle, int(start), int(end)))
}
func GetMessages(profile, handle string, start, end int) string {
messages := application.GetPeer(profile).GetContact(handle).Timeline.Messages[start:end]
bytes, _ := json.Marshal(messages)
return string(bytes)
}
All our functions that will be callable from the FFI interface in Dart start with c_
and have parameters in C data types. We then convert them to Go native data using cgo
and call the go native function, which Gomobile will “directly” call via Kotlin and the generated JRE interface. Finally, when we get a result, the c_
function converts it to C native types and returns that.
However we’re not quite done. Gomobile and go build both expect a slightly different file format, so to easily use one main file for both we have a last little bit of boiler plate to drop in.
Go requires a main
function even when compiling to a library, where as gomobile does not. We put
lib.go
// Leave as is, needed by ffi
func main() {}
At the bottom of the file. Go requires a main
package as well, but Gomobile expects a package name to match the lib name, in this case ‘cwtch’. We place
lib.go
//package cwtch
package main
At the top of the file. To toggle these little details, we have two helper scripts: switch-ffi.sh and switch-gomobile.sh.
These commands comment or uncomment the func main ()
line and comment or uncomment the appropriate package
invocation. We then have each script respectively being invoked in our Makefile before the above mentioned compile commands.
As a final note on library design, for simplicity, we have one top level go files, lib.go, with the cgo and gomobile interface. All the rest of the functionality of the wrapper library is in sub packages so they won’t be converted by gomobile or cgo and can all use native go data types.
Library usage in Flutter
Flutter operates the UI from it’s main thread or “isolate”, which we won’t ever want to block or the UI would become unresonsive. Most of the blocking and network complexity is burried in Cwtch but even calls into the library for data can take a prohibative amount of time so we want all of them to happen off the main isolate. For each platform we’ll take a different approach but they’ll both acomplish this.
To start with, under /lib
we made a /lib/cwtch
module to capture our interface and implementations. We created an interface that could be used as follows
lib/cwtch/cwtch.dart
abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<void> Start();
// ignore: non_constant_identifier_names
void SelectProfile(String onion);
...
For our interface we may have unwittingly followed the Go-ism of Capital named public functions, which the Dart linter does not like, hence the // ignore
comment. This is due to be cleaned up at some point.
FFI Implementation
Our first implementation of the Cwtch interface in Dart is the FFI method of accessing the library which we use on Desktop.
lib/cwtch/ffi.dart
import 'dart:ffi';
...
//func GetMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, start C.int, end C.int) *C.char {
typedef get_json_blob_from_str_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
...
class CwtchFfi implements Cwtch {
late DynamicLibrary library;
...
CwtchFfi() {
if (Platform.isWindows) {
library = DynamicLibrary.open("libCwtch.dll");
} else if (Platform.isLinux) {
library = DynamicLibrary.open("libCwtch.so");
} else {
print("OS ${Platform.operatingSystem} not supported by cwtch/ffi");
// emergency, ideally the app stays on splash and just posts the error till user closes
exit(0);
}
}
...
// ignore: non_constant_identifier_names
Future<String> GetMessages(String profile, String handle, int start, int end) async {
var getMessagesC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_int_function>>("c_GetMessages");
// ignore: non_constant_identifier_names
final GetMessages = getMessagesC.asFunction<GetJsonBlobFromStrStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
Pointer<Utf8> jsonMessagesBytes = GetMessages(utf8profile, utf8profile.length, utf8handle, utf8handle.length, start, end);
String jsonMessages = jsonMessagesBytes.toDartString();
return jsonMessages;
}
The top of the file is filled with a set of function definitions for the calls we want to make, generalized by types such as void_from_string_string_function
etc describing the parameters and return types for re usability.
The constructor attempts to open the appropriate Desktop OS library or exits in error, and our demo function acquires the function from the library (as defined above) and then initializes the C style parameters from Dart values and makes the call. Finally it takes the C style return data and converts it into the expected Dart type and returns it.
A little cumbersome but relatively easy to replicate to fill out the entire interface. One optimization we simple haven’t had the time to get to is to pull all the function lookups into the constructor and store them as class variables. You could start this way and save yourself some refactoring.
Gomobile Implementation
The Gomobile implementation appears a bit more complicated at first, but this later pays off, so stay tuned.
The libcwtch-go interface available is in a JRE compatible format so has to be called from Java or Kotlin. We opted for Kotlin. In order for Dart to make calls into Kotlin, a Dart MethodChannel must be used, so the gomobile implementation in Dart looks like:
lib/cwtch/gomobile.dart
class CwtchGomobile implements Cwtch {
static const cwtchPlatform = const MethodChannel('cwtch');
CwtchGomobile() {
...
}
// ignore: non_constant_identifier_names
Future<dynamic> GetMessages(String profile, String handle, int start, int end) {
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "start": start, "end": end});
}
So far it’s quite straight forward. Since we’re using a Method Channel and calling into another thread, the response type must be a Future so that Dart can pause this line of execution and be free to do other things until the response is ready (cooperative threading). Next we look at the Kotlin side of the implementation.
android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
package im.cwtch.flwtch
...
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import cwtch.Cwtch
...
class MainActivity: FlutterActivity() {
// Channel to get cwtch api calls on
private val CHANNEL_CWTCH = "cwtch"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) }
}
...
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
...
"GetMessages" -> {
val profile = (call.argument("profile") as? String) ?: "";
val handle = (call.argument("contact") as? String) ?: "";
val start = (call.argument("start") as? Long) ?: 0;
val end = (call.argument("end") as? Long) ?: 0;
result.success(Cwtch.getMessages(profile, handle, start, end))
}
...
else -> result.notImplemented()
}
}
Flutter kindly provides the imports we need to Kotlin to make this easy. In the configureFlutterEngine
function which is auto called we set up the Method channel on the Kotlin side to forward to our handler function, and our handler function does some basic argument parsing and verification before calling the libCwtch-go interface. MethodChannel functions in Kotlin are all handled by one handler running a when
statement over the possible sub method calls.
To make the JRE libCwtch-go interface available to Koltlin (or Java), just follow the usual steps to import your .aar file produced by gomobile.
Bidirectional Communication
The cwtch library manages network connections for profiles which are in turn connected to many contacts and groups, and constantly getting messages. As is the broader case with any library providing network connection management, we need more than just a way to push Flutter/Dart UI driven events into the Cwtch library, we need a way for it to respond back when events happen and push those events up to the Dart and Flutter layer of the app.
As is common with Go multi threaded programming, we use a lot of goroutines and use channels to pass information around. We codified this in Cwtch in our eventbus subsystem. With this we already had a system we were previously using to push many events back to a UI (the old QT/go ui), and it was structured to be simple and flexible and handle many events, which was great since it allowed us to write one callback system to handle everything.
For our API, a lot of our calls into the library have defined methods in libcwtch-go, but since we are using the cwtch event system for messages it generates, we are passing them to Dart as JSON to simplify managing the over 20 events with different associated parameters we currently handle.
In libcwtch-go, the hook is as follows
lib.go
func (eh *EventHandler) GetNextEvent() string {
appChan := eh.appBusQueue.OutChan()
select {
case e := <-appChan:
return eh.handleAppBusEvent(&e)
case ev := <-eh.profileEvents:
return eh.handleProfileEvent(&ev)
}
}
...
//export c_GetAppBusEvent
func c_GetAppBusEvent() *C.char {
return C.CString(GetAppBusEvent())
}
// GetAppBusEvent blocks until an event
func GetAppBusEvent() string {
var json = ""
for json == "" {
json = eventHandler.GetNextEvent()
}
return json
}
The handle*Event functions switch over different event types, take corresponding actions or enrich the limited eventbus message with more data and return JSON strings or empty string if the UI needs no notification. The public API of the library is the GetAppBusEvent()
functions which are blocking calls into the library that block on the Go channel waiting for a message. If the message needs to go to the UI then it will pass it up, awaiting a recall by the next layer, but if the event could be handler in the wrapper layer entierly, then it just goes back to blocking, waiting for the next event that the UI needs to be notified of.
The final Flutter side of bubbling event messages up from the library is how to notify the Flutter ui based on an event. For this we use a Notifier, cwtchNotifier
which is a collection of varios UI Notifiers and a message processing function which calls the appropriate sub notifier based on the event it got from libCwtch-go.
FFI
Since GetAppBusEvents is a blocking call, we need to make these calls off the main Dart thread. For FFI, we employ an isolate.
lib/cwtch/ffi.dart
class CwtchFfi implements Cwtch {
...
late CwtchNotifier cwtchNotifier;
late Isolate cwtchIsolate;
CwtchFfi(CwtchNotifier _cwtchNotifier) {
...
cwtchNotifier = _cwtchNotifier;
}
// Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events
@override
void dispose() {
if (cwtchIsolate != null) {
cwtchIsolate.kill(priority: Isolate.immediate);
}
}
// ignore: non_constant_identifier_names
Future<void> Start() async {
...
// Spawn an isolate to listen to events from libcwtch-go and then dispatch them when received on main thread to cwtchNotifier
var _receivePort = ReceivePort();
cwtchIsolate = await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort);
_receivePort.listen((message) {
var env = jsonDecode(message);
cwtchNotifier.handleMessage(env["EventType"], env["Data"]);
});
}
// Entry point for an isolate to listen to a stream of events pulled from libcwtch-go and return them on the sendPort
static void _checkAppbusEvents(SendPort sendPort) async {
var stream = pollAppbusEvents();
await for (var value in stream) {
sendPort.send(value);
}
}
// Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
static Stream<String> pollAppbusEvents() async* {
late DynamicLibrary library;
if (Platform.isWindows) {
library = DynamicLibrary.open("libCwtch.dll");
} else if (Platform.isLinux) {
library = DynamicLibrary.open("libCwtch.so");
}
var getAppbusEventC = library.lookup<NativeFunction<acn_events_function>>("c_GetAppBusEvent");
// ignore: non_constant_identifier_names
final GetAppbusEvent = getAppbusEventC.asFunction<ACNEventsFn>();
while (true) {
Pointer<Utf8> result = GetAppbusEvent();
String event = result.toDartString();
yield event;
}
}
In our constructor we now also take the cwtchNotifier and store it. In our Start() function we create an isolate and tell it to run _checkAppbusEvents
on the isolate coroutine and any data returned from it (JSON strings) we dispatch to the cwtchNotifier on the main Dart thread.
_checkAppbusEvents
simply sets up a Stream<String>
and starts await
-ing values from it to send over it’s sendPort, the isolate IPC mechanism. pollAppbusEvents
needs to get its own handle to the dynamic libraries as it cannot use the class value ones as they are in different coroutines, and Dart blocks that kind of unsafe access. That done, it loops making the blocking getAppbusEvents call, converting it to a Dart string, and yielding the result to _checkAppbusEvents
. We have wired in a dispose
function to cleanup the isolate how ever there are some problems in Flutter lifecycle management currently not working right and preventing this. Check back on the issue and code at a later date to see potential fixes for this as we get to them.
With this all wired up, we now have an isolate polling a blocking call in our libcwtch-go library waiting on a go channel. When go channel events come in, the function returns to the isolate, and the isolate IPCs the message over to the main Dart thread which uses a notifier to dispatch it.
Gomobile
The Gomobile side of this story is a bit simpler because as I mentioned in the previous section, the early work of setting up the MethodChannels to Kotlin will now pay off, as the MainActivity
is already running in a separate thread so there’s no need to setup an isolate on the Flutter/Dart side.
android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
// Channel to send eventbus events on
private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus"
...
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"Start" -> {
...
// seperate coroutine to poll event bus and send to dart
val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...")
GlobalScope.launch(Dispatchers.IO) {
while(true) {
val evt = AppbusEvent(Cwtch.getAppBusEvent())
launch(Dispatchers.Main) {
//todo: this elides evt.EventID which may be needed at some point?
eventbus_chan.invokeMethod(evt.EventType, evt.Data)
}
}
}
}
Kotlin gets the call to “Start” and launches a new coroutine on the Dispatchers.IO scope. This coroutine polls the blocking function of getAppbussEvent
and then puts the result onto the MethodChannel directly. This is a new MethodChannel (that could be named anything, as you can see from our early testing name persisting) that Dart will be listening to.
lib/cwtch/gomobile.dart
class CwtchGomobile implements Cwtch {
...
final appbusEventChannelName = 'test.flutter.dev/eventBus';
CwtchGomobile(CwtchNotifier _cwtchNotifier) {
cwtchNotifier = _cwtchNotifier;
...
// Method channel to receive libcwtch-go events via Kotlin and dispatch them to _handleAppbusEvent (sends to cwtchNotifier)
final appbusEventChannel = MethodChannel(appbusEventChannelName);
appbusEventChannel.setMethodCallHandler(this._handleAppbusEvent);
}
// Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier
Future<void> _handleAppbusEvent(MethodCall call) async {
final String json = call.arguments;
var obj = jsonDecode(json);
cwtchNotifier.handleMessage(call.method, obj);
}
The Dart side was simpler to implement because of the preexisting structure established in the MethodChanel API call work. The constructor makes an EventChannel to listen on and hooks it up to our _handleMessage
which gets the call, and passes it right to the CwtchNotifier
.
This area is subject to change as we finalize our work on making an Android Service to run the libcwtch-go interface to increase stability and correctness. We will be moving the message polling into the Service, in Kotlin, and performing some filtering there too, ie. if the app is not currently focused, the service can create a notification on receipt of message with no need to alert the UI until it is active again. Check the codebase for the latest developments there.
Closing
And that wraps up our tour of cross platform mobile/desktop bidirectional API calling and event handling from Flutter/Dart to a network handling Go library. It is a bit of boiler plate each time you want to add a new API call, but compared to the cost of multiple apps or multiple libraries, it’s nothing. We have an incredibly unified codebase across Android, Windows and Linux. And we’ve been able to leverage our rich and mature 3 year old Cwtch Go library to do all the work, and leave Flutter to rapidly build the UI atop it all. This style seems well suited for any kind of non trivial Flutter app, splitting the work out to a language better suited to it, and leaving Flutter and Dart to manage the UI only, which is what they do best.
As always, this is complex and at times feels like boundery pushing work (like when we run into platform bugs or limits), and the speed with which we’ve been able to build and entire new UI in the last 6 months speaks both well of Flutter, but also the growing maturity of Cwtch as a library supporting apps. Our work is complicated, and pushing many limits, and as always, supported by you, the donors and community, so please consider donating to keep supporting this work if you like any part of it. We are far from done.