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?
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.