c++cflutterinteropdart-ffi

Is it possible to make a C++ application and use Flutter as the GUI framework?


I've made a C++ application that runs on embedded linux (OrangePi) and currently it uses a HMI screen (Nextion). But this really ties me to a specific brand of screen and the features that they provide.

Recently I made a different full GUI application solely using Flutter, and I really like it. It is also much nicer than writing a GUI from scratch with any C++ library and has the bonus of cross platform.

I was wondering if it is possible/sensible to move away from my HMI screen for the C++ application and use Flutter? This would then enable me to probably run it on mobile in the future as well which would be a bonus.

The high level idea would be to run Flutter as the main "thread" and then load in my C++ application as a shared library and run it on a second "thread"/isolate. Then when buttons on the GUI are pressed dart would call through to C and when new data needs displayed on screen C would call through to dart (this direction seems more awkward).

From what I can see online dart-ffi is the tool to use for this. It seems to be able to call from dart to C "easily" but not the other way, unless using callbacks. I don't think that will really work for me as the C++ application will independently need to frequently call to dart to update the GUI without user interaction which rules out using a callback in the traditional sense. I have very limited experience with JNI for doing similar things and it has no problem calling from C to Java/Kotlin so hopefully I'm just missing something in dart-ffi.

NativeCallable.listener looks interesting but it closes the callback after its called every time. I don't really want to do that, I would like C++ to call through when ever it wants. From what I'm reading there it seems to create a new isolate ever time you create a callback. If I was to create lots of callbacks on startup and pass them to the C++ application so that it could use them as normal function calls throughout its lifetime, all these isolates would be an issue.

Is there a way I could make Flutter work for my use case? Am I missing some examples or documentation on how to make it work?


Solution

  • I have found a way to have a C++ backend and a Flutter frontend. It does mean you have to pass in any functions you want C++ to call asynchronously as callbacks at the start which isn't ideal but do-able as an initialisation step.

    Note this is still in very much a POC state with globals etc.

    C++

    #include <chrono>
    #include <functional>
    #include <thread>
    
    #if defined(_WIN32)
    #define DART_EXPORT extern "C" __declspec(dllexport)
    #else
    #define DART_EXPORT                                                            \
      extern "C" __attribute__((visibility("default"))) __attribute((used))
    #endif
    
    std::function<void(int)> update;
    std::function<void(int)> ping;
    
    DART_EXPORT void start_app()
    {
        for (int i = 0; i < 5; ++i)
        {
            std::this_thread::sleep_for(std::chrono::seconds(2));
            update(1);
            std::this_thread::sleep_for(std::chrono::seconds(1));
            ping(5);
        }
    }
    
    DART_EXPORT void set_update(void (*update_dart)(int))
    {
        update = update_dart;
    }
    
    DART_EXPORT void set_ping(void (*ping_dart)(int))
    {
        ping = ping_dart;
    }
    

    Dart

    import 'dart:async';
    import 'dart:ffi';
    import 'dart:io';
    import 'dart:isolate';
    
    import 'dylib_utils.dart';
    
    typedef Callback = Void Function(Int);
    
    typedef SetFunction = void Function(Pointer<NativeFunction<Callback>>);
    typedef SetNativeFunction = Void Function(Pointer<NativeFunction<Callback>>);
    
    late final DynamicLibrary dylib = dlopenPlatformSpecific(
      "backend",
      paths: [
        Platform.script.resolve('../lib/'),
        Uri.file(Platform.resolvedExecutable),
      ]
    );
    
    final nativeSetUpdate = dylib.lookupFunction<SetNativeFunction, SetFunction>(
      "set_update"
    );
    final nativeSetPing = dylib.lookupFunction<SetNativeFunction, SetFunction>(
      "set_ping"
    );
    
    typedef StartAppFunction = void Function();
    typedef StartAppNativeFunction = Void Function();
    final nativeStartApp = dylib
      .lookupFunction<StartAppNativeFunction, StartAppFunction>("start_app");
    
    void app(final String message) {
      nativeStartApp();
    }
    
    void setUpdate() {
      void onNativeUpdate(final int amount) {
        print("Got an update $amount");
      }
    
      final callback = NativeCallable<Callback>.listener(onNativeUpdate);
      nativeSetUpdate(callback.nativeFunction);
      callback.keepIsolateAlive = false;
    }
    
    void setPing() {
      void onNativePing(final int amount) {
        print("Got a ping $amount");
      }
    
      final callback = NativeCallable<Callback>.listener(onNativePing);
      nativeSetPing(callback.nativeFunction);
      callback.keepIsolateAlive = false;
    }
    
    Future<void> main() async {
      print("Setting update...");
      setUpdate();
    
      print("Setting ping...");
      setPing();
    
      print("Starting app...");
      final ReceivePort exitListener = ReceivePort();
      Isolate.spawn(app, "test", onExit: exitListener.sendPort);
      exitListener.listen((message){
        if (message == null) { // A null message means the isolate exited
          print("App stopped...");
          exitListener.close();
        }
      });
    }
    
    

    Implementation of dlopenPlatformSpecific is here.