~whereswaldon/arbor-dev#101: 
Assess Flutter as a possible platform for Arbor Mobile/Desktop development

~athorp96 suggested flutter as a possible platform for building mobile cross-platform applications for both iOS and Android during our last sync. After some preliminary investigation, it has some promising characteristics. I'm creating this issue to document investigations into the possibilities of flutter as a platform.

My primary areas of concern are:

  • Can we re-use existing implementations of Forest and PGP in Go from Dart/Flutter, or will we need to re-implement them?
  • Flutter's Web interfaces are a Beta feature, and the language (docs) around them gives me little confidence in them right now. Is the ability to build a web interface a make-or-break feature for us?
  • How difficult will it be to build and distribute applications for all of the platforms that we hope to support?
  • What's the Dart learning curve like? Does it seem feasible/wise to have our community invest in learning it?

So far, I have:

  • Some evidence that we could use a combination of Dart's FFI capability and CGO to invoke our Go libraries as statically linked objects. Here's an example of someone doing it from Rust.
  • Concerns due to Flutter's nature as a Google project. All of their SDKs, while open source, contain telemetry that must be opted out of. I really dislike the sheer number of things that you must opt out of in order to prevent sending Google data about your usage of their open source project.
  • Seems like building for {i,mac}OS requires XCode and such, so I'm not going to be able to do it. This is likely true of any tools targeting macOS, but it's especially clear here because the Flutter docs just assume that you already have XCode.

I'm going to keep digging and experimenting though. Please feel free to do your own research or just post your thoughts!

Status
REPORTED
Submitter
~whereswaldon
Assigned to
No-one
Submitted
5 months ago
Updated
5 months ago
Labels
No labels applied.

~whereswaldon 5 months ago

Some findings of my and ~gurnben's exploration of Flutter today:

  • Their desktop application support is actually even more alpha than their web support. Their marketing made me think it was ready to be used, but it literally barely supports windows/linux (macOS, weirdly, is in alpha while the others aren't even ready for that).
  • We ran into two major obstacles with trying to link Go code into a Flutter application:
    1. The flutter build system uses this nightmarish combination of its own build system and Gradle (for android builds). To get the android build system to find the library, we had to try many, many things. Eventually, we discovered that putting it into the android/src/main/jniLibs/armeabi-v7a/go.so subdirectory at least let the build system find it, but it wasn't able to be linked. (I also fear that getting it to work on macOS might be even harder).
    2. Architecture/ABI compiler support. That particular .so was built with env GOARCH=arm64 GOOS=android CC=aarch64-linux-gnu-gcc go build -buildmode=c-shared -o go.so -x ., and I think the problem is that the compiler is targeting linux-gnu instead of linux-android. I found this article about building GCC to target linux-android, but it seems like a huge pain.
  • Rather than go for a custom compiler build, I'm now trying to solve the problem with gomobile bind. It seems like it may have the smarts necessary to build the right thing, and I'm pretty sure that I saw some instructions on how to link an AAR archive into the android build system.

On the whole, getting this to work seems like it's going to be pretty painful. So far, it still seems less painful than re-implementing forest in Dart (doing it in Go took 6 months, remember? :P), but that could change. ~gurnben and I will post here if we make any more progress.

~whereswaldon 5 months ago

I was just (briefly) able to achieve a few measures of success:

  1. I got the DynamicLibrary.open() call in Flutter to not fail outright. I did this by using gomobile bind to create an AAR, then dissecting that AAR to extract the .so files it contained. I manually added them at their architecture directories in android/src/main/jniLibs/* and building that succeeded in opening the library. However, gomobile appears to do some odd things to the symbols exported by your code. I believe the goal of gomobile is to enable Java to invoke your go code, so functions that you export don't actually show up in the symbols. Because of this, trying to resolve my simple test function out of the loaded dynamic library failed.
  2. By running gomobile bind -x, I was able to see the compiler flags that were used to generate the .so file that worked. I was able to manually run a go build using those same flags to generate what appears to be a valid .so (but one that actually contains the symbol table I expect). I'm not currently able to open it from dart, but I think I might just be making a stupid mistake because I'm tired. I'll try again later.

~whereswaldon 5 months ago

I just got it to work. I don't have much time, but I'll document what I can.

Go source code:

package main

import "C"

//export Gimme
func Gimme() int64 {
    return 42
}

func main() {}

Compile that with (for my system, paths will be different for others): shell env PWD=$WORK/src GOOS=android GOARCH=arm64 CC=$ANDROID_HOME/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang CXX=$ANDROID_HOME/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++ CGO_ENABLED=1 GOPATH=$WORK:$GOPATH go build -x -buildmode=c-shared -o=$PWD/thing64.so .

Copy the resulting code into android/src/main/jniLibs/arm64-v8a/thing64.so

Then, in my dart plugin:

import 'dart:async';
import 'dart:ffi';

import 'package:flutter/services.dart';

final DynamicLibrary nativeAddLib = DynamicLibrary.open("thing64.so");

final int Function() runGoCode = nativeAddLib.lookup<NativeFunction<Int64 Function()>>("Gimme").asFunction();

// below here is boilerplate that I didn't touch
class Gonative {
  static const MethodChannel _channel =
      const MethodChannel('gonative');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

My Dart main uses that like so:

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:gonative/gonative.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      platformVersion = await Gonative.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Text('Running on: $_platformVersion
${runGoCode()}'),
        ),
      ),
    );
  }
}

The invocation of the go code is inside of that string formatting, so easy to miss.

I think some of the build system modifications we performed yesterday might be necessary for this to work, but I'll need to experiment to determine which one is allowing success.

In the end though, I have the number 42 on the screen of my demo App, and it's coming from Go! Progress!

~whereswaldon 5 months ago

I've written up what I believe to be the minimal steps required to achieve this here. I got that procedure to work in a brand new project, and it seems to correctly build the go for all of the architectures that android supports.

I'd really appreciate it if anyone has time to run through that and replicate my findings.

Register here or Log in to comment, or comment via email.