Я создал приложение на C++, которое работает на встроенном Linux (OrangePi), и в настоящее время оно использует экран HMI (Nextion). Но это действительно привязывает меня к конкретной марке экрана и функциям, которые они предоставляют.
Недавно я создал другое приложение с полным графическим интерфейсом, используя исключительно Flutter, и оно мне очень нравится. Это также намного приятнее, чем писать графический интерфейс с нуля с использованием любой библиотеки C++, и имеет преимущество кроссплатформенности.
Мне было интересно, возможно ли/разумно отказаться от экрана HMI для приложения C++ и использовать Flutter? Это позволит мне, вероятно, запустить его и на мобильных устройствах в будущем, что будет бонусом.
Идея высокого уровня заключалась бы в том, чтобы запустить Flutter в качестве основного «потока», а затем загрузить мое приложение C++ в качестве общей библиотеки и запустить его во втором «потоке»/изолировать. Затем, когда нажимаются кнопки в графическом интерфейсе, dart будет вызывать C, а когда новые данные будут отображены на экране, C будет вызывать dart (это направление кажется более неудобным).
Судя по тому, что я вижу в Интернете, dart-ffi — это инструмент, который можно использовать для этого. Кажется, что он может «легко» звонить из dart в C, но не наоборот, если только не используются обратные вызовы. Я не думаю, что это действительно сработает для меня, поскольку приложению C++ независимо придется часто вызывать dart для обновления графического интерфейса без взаимодействия с пользователем, что исключает использование обратного вызова в традиционном смысле. У меня очень ограниченный опыт работы с JNI для выполнения подобных задач, и у него нет проблем с вызовом с C на Java/Kotlin, так что, надеюсь, я просто что-то упускаю в dart-ffi.
NativeCallable.listener выглядит интересно, но каждый раз закрывает обратный вызов после его вызова. На самом деле я не хочу этого делать, я бы хотел, чтобы C++ вызывал вызов, когда захочет. Судя по тому, что я там читаю, кажется, что каждый раз, когда вы создаете обратный вызов, создается новый изолят. Если бы мне пришлось создавать множество обратных вызовов при запуске и передавать их приложению C++, чтобы оно могло использовать их как обычные вызовы функций на протяжении всей своей жизни, все эти изоляты были бы проблемой.
Есть ли способ заставить Flutter работать в моем случае? Мне не хватает некоторых примеров или документации о том, как заставить это работать?
@user12002570 user12002570 вы использовали Flutter с серверной частью C++ или просто отдельно?
Я думал об использовании C++ в качестве серверной части и Flutter в качестве внешнего интерфейса, но потом понял, что смешивать C++ с любым другим языком в проекте слишком сложно. C++ только усложнит ситуацию. Затем я использовал Flutter для всего проекта (бэкэнд и интерфейс). Несколько лет назад я также задал очень похожий вопрос о самой SO.
Почему бы не запустить бит C++ в отдельном процессе и не поговорить с ним через сокет?
Я нашел способ объединить серверную часть C++ и интерфейс Flutter. Это означает, что вам нужно передавать любые функции, которые вы хотите, чтобы C++ вызывал асинхронно, в качестве обратных вызовов в начале, что не идеально, но выполнимо в качестве шага инициализации.
Обратите внимание, что это все еще находится в состоянии POC с глобальными переменными и т. д.
С++
#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;
}
Дарт
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();
}
});
}
Реализация dlopenPlatformSpecific
находится здесь.
«Это также намного приятнее, чем писать графический интерфейс с нуля с помощью любой библиотеки C++…» Я полностью согласен. Предоставленный Flutter графический интерфейс намного лучше. Я использовал
wxWidgets
иQt
в C++, а также Flutter для создания графического интерфейса. Флаттер лучше по количеству усилий, а также по внешнему виду.