You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何基于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)可以轻松解决这个问题。

通用注意事项(无论选哪个方案)

  1. 线程安全是底线:你的工作线程(串口读取、命令执行)都会修改状态,必须用std::mutexstd::shared_mutex保护所有状态读写操作,避免数据竞争。
  2. 持久化逻辑抽离:把状态的序列化/反序列化完全封装在AppState类里,不要和业务逻辑混在一起。推荐用nlohmann/json(轻量、易用)或Protobuf(适合复杂数据结构)做序列化。
  3. 状态修改通知(可选):如果需要Flutter端实时感知状态变化(比如设备连接/断开),可以在C++端用回调函数通知Flutter,或者Flutter定时轮询状态。

给你的具体建议

如果你的项目还处于早期快速迭代阶段,优先选方案1的受控单例——它能帮你省掉Flutter端管理句柄的麻烦,让你把精力放在核心功能(设备通信、配置管理)上。等项目稳定后,如果觉得单例带来了测试或扩展问题,再逐步重构为方案2的句柄传递模式。

其实在单PC端的桌面应用场景中,受控式单例是完全合理的选择,不用被“不要用单例”的教条束缚——工具是为场景服务的,不是反过来。

火山引擎 最新活动