如何基于C+++Flutter FFI+Arduino架构管理应用状态(含持久化需求)
如何基于C+++Flutter FFI+Arduino架构管理应用状态(含持久化需求)
我完全理解你的纠结——全局状态/单例确实被很多人吐槽,但在跨FFI调用的场景下,它的便利性实在太诱人了。而完全抛弃全局状态又会带来Flutter端需要管理状态句柄的额外成本。结合你的项目场景(单PC端应用,全局唯一的设备/配置状态),我给你几个兼顾便利性和可维护性的方案,你可以根据自己的项目阶段选择:
方案1:受控式单例(推荐入门用)
单例不是洪水猛兽,失控的单例才是。你可以实现一个带明确生命周期的线程安全单例,既保留全局访问的便利性,又避免无限制的全局副作用。
具体实现
1. 定义带线程安全的状态管理器
// app_state.h #pragma once #include <vector> #include <mutex> #include <fstream> #include "nlohmann/json.hpp" // 用这个库做序列化,非常方便 using json = nlohmann::json; // 先定义你的子结构,比如设备、配置 struct AppSettings { bool auto_scan_devices = true; std::string save_path = "app_state.json"; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AppSettings, auto_scan_devices, save_path) struct Device { std::string serial_port; std::string device_id; std::vector<Profile> profiles; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Device, serial_port, device_id, profiles) // 核心应用状态 struct AppState { std::vector<DeviceInfo> compatible_devices; std::vector<Device> connected_devices; AppSettings settings; std::mutex state_mutex; // 线程安全锁 // 持久化方法 void save_to_file() { std::lock_guard<std::mutex> lock(state_mutex); json j = *this; std::ofstream file(settings.save_path); file << j.dump(4); // 格式化输出,方便调试 } static AppState load_from_file(const std::string& path) { AppState default_state; default_state.settings.save_path = path; default_state.compatible_devices = get_compatible_devices(); // 预加载兼容设备列表 std::ifstream file(path); if (!file.is_open()) return default_state; json j; file >> j; return j.get<AppState>(); } }; // 受控单例管理器 class AppStateManager { private: static AppStateManager* instance; static std::mutex init_mutex; AppState state; // 私有构造,禁止外部创建 AppStateManager() = default; ~AppStateManager() = default; public: // 禁止复制/移动 AppStateManager(const AppStateManager&) = delete; AppStateManager& operator=(const AppStateManager&) = delete; AppStateManager(AppStateManager&&) = delete; AppStateManager& operator=(AppStateManager&&) = delete; // 线程安全的实例获取 static AppStateManager& get_instance() { std::lock_guard<std::mutex> lock(init_mutex); if (!instance) { instance = new AppStateManager(); } return *instance; } // 明确的销毁方法,配合stopBackend调用 static void destroy_instance() { std::lock_guard<std::mutex> lock(init_mutex); delete instance; instance = nullptr; } // 获取带锁的状态引用(确保线程安全) AppState& get_state() { return state; } }; // 初始化静态成员 AppStateManager* AppStateManager::instance = nullptr; std::mutex AppStateManager::init_mutex;
2. 后端启动/停止时绑定生命周期
// backend.cpp extern "C" __declspec(dllexport) int startBackend() { auto& manager = AppStateManager::get_instance(); auto& state = manager.get_state(); // 加载本地保存的状态 state = AppState::load_from_file("app_state.json"); // 扫描串口设备 state.connected_devices = scan_serial_ports_for_devices(); // 启动工作线程,直接通过单例访问状态 std::thread console_worker([](){ auto& state = AppStateManager::get_instance().get_state(); while (is_running) { std::lock_guard<std::mutex> lock(state.state_mutex); // 处理串口数据、更新状态 } }); console_worker.detach(); std::thread command_worker([](){ auto& state = AppStateManager::get_instance().get_state(); while (is_running) { std::lock_guard<std::mutex> lock(state.state_mutex); // 执行设备命令 } }); command_worker.detach(); return 0; } extern "C" __declspec(dllexport) void stopBackend() { auto& state = AppStateManager::get_instance().get_state(); // 保存状态到文件 state.save_to_file(); // 停止线程逻辑... // 销毁单例 AppStateManager::destroy_instance(); } // FFI示例:获取已连接设备数量(无需传状态引用) extern "C" __declspec(dllexport) int getConnectedDeviceCount() { auto& state = AppStateManager::get_instance().get_state(); std::lock_guard<std::mutex> lock(state.state_mutex); return state.connected_devices.size(); }
方案1的优缺点
- 优点:FFI调用零额外成本,Flutter端不需要管理任何状态句柄,直接调用函数即可;状态生命周期明确,线程安全有保障。
- 缺点:单例模式对单元测试不友好(但你可以把业务逻辑抽离到
AppState类,测试时直接创建AppState实例而不用单例);如果未来要扩展多实例场景(比如多用户),需要重构。
方案2:状态句柄传递(无全局,最佳实践)
如果你想严格遵循“无全局状态”的原则,可以把AppState的指针作为句柄返回给Flutter,Flutter端保存这个句柄,每次调用FFI函数时传递它。
具体实现
1. 状态定义(去掉单例管理器)
// app_state.h // 这里和方案1的AppState定义完全一样,只是去掉AppStateManager
2. 后端启动返回句柄
// backend.cpp extern "C" __declspec(dllexport) void* startBackend() { AppState* state = new AppState(); *state = AppState::load_from_file("app_state.json"); state->connected_devices = scan_serial_ports_for_devices(); // 工作线程捕获state指针 std::thread console_worker([state](){ while (is_running) { std::lock_guard<std::mutex> lock(state->state_mutex); // 处理串口数据 } }); console_worker.detach(); return static_cast<void*>(state); } extern "C" __declspec(dllexport) void stopBackend(void* stateHandle) { AppState* state = static_cast<AppState*>(stateHandle); state->save_to_file(); // 停止线程... delete state; } // FFI示例:获取设备数量(需要传句柄) extern "C" __declspec(dllexport) int getConnectedDeviceCount(void* stateHandle) { AppState* state = static_cast<AppState*>(stateHandle); std::lock_guard<std::mutex> lock(state->state_mutex); return state->connected_devices.size(); }
3. Flutter端管理句柄
import 'dart:ffi'; import 'dart:io'; final DynamicLibrary backendLib = Platform.isWindows ? DynamicLibrary.open('backend.dll') : DynamicLibrary.open('libbackend.so'); // 定义FFI函数签名 typedef StartBackendFunc = Pointer<Void> Function(); typedef StartBackend = Pointer<Void> Function(); final startBackend = backendLib.lookupFunction<StartBackendFunc, StartBackend>('startBackend'); typedef StopBackendFunc = Void Function(Pointer<Void>); typedef StopBackend = void Function(Pointer<Void>); final stopBackend = backendLib.lookupFunction<StopBackendFunc, StopBackend>('stopBackend'); typedef GetDeviceCountFunc = Int32 Function(Pointer<Void>); typedef GetDeviceCount = int Function(Pointer<Void>); final getDeviceCount = backendLib.lookupFunction<GetDeviceCountFunc, GetDeviceCount>('getConnectedDeviceCount'); // 保存状态句柄(可以用Provider/Riverpod全局管理) Pointer<Void>? _appStateHandle; void main() { _appStateHandle = startBackend(); runApp(const MyApp()); } // 在需要调用的地方使用 int fetchDeviceCount() { if (_appStateHandle == null) throw StateError("Backend not started"); return getDeviceCount(_appStateHandle!); } // 退出时清理 @override void dispose() { _appStateHandle?.let(stopBackend); _appStateHandle = null; super.dispose(); }
方案2的优缺点
- 优点:完全无全局状态,单元测试友好(可以创建多个
AppState实例);符合现代C++的最佳实践。 - 缺点:Flutter端需要额外管理状态句柄,每次FFI调用都要传递;如果业务逻辑复杂,句柄的传递会稍微繁琐,但用Flutter的状态管理库(Provider/Riverpod)可以轻松解决这个问题。
通用注意事项(无论选哪个方案)
- 线程安全是底线:你的工作线程(串口读取、命令执行)都会修改状态,必须用
std::mutex或std::shared_mutex保护所有状态读写操作,避免数据竞争。 - 持久化逻辑抽离:把状态的序列化/反序列化完全封装在
AppState类里,不要和业务逻辑混在一起。推荐用nlohmann/json(轻量、易用)或Protobuf(适合复杂数据结构)做序列化。 - 状态修改通知(可选):如果需要Flutter端实时感知状态变化(比如设备连接/断开),可以在C++端用回调函数通知Flutter,或者Flutter定时轮询状态。
给你的具体建议
如果你的项目还处于早期快速迭代阶段,优先选方案1的受控单例——它能帮你省掉Flutter端管理句柄的麻烦,让你把精力放在核心功能(设备通信、配置管理)上。等项目稳定后,如果觉得单例带来了测试或扩展问题,再逐步重构为方案2的句柄传递模式。
其实在单PC端的桌面应用场景中,受控式单例是完全合理的选择,不用被“不要用单例”的教条束缚——工具是为场景服务的,不是反过来。




