Question

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?

 2  63  2
1 Jan 1970

Solution

 0

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.

2024-07-23
av4625