在嵌入式系统中集成Rust和Qt的实践

嵌入式技术

1372人已加入

描述

  Rust 为嵌入式软件提供高性能、高可靠性和强大的安全性,并帮助开发人员检查并减少复杂、低级应用程序常见的内存和线程错误。凭借这些明显的优势,Rust 在嵌入式软件开发人员中越来越受欢迎。同样,作为跨平台嵌入式应用程序的框架之一,Qt 为嵌入式领域带来了强大的业务逻辑功能。由于 Rust 还没有的图形用户界面 (GUI),这意味着 Rust-Qt 集成对嵌入式开发人员越来越感兴趣,并已成为向嵌入式 Rust 应用程序添加 GUI 的关键方法之一。

  将两者整合在一起时,挑战就来了。如果没有仔细的计划和努力,组合的 Rust-Qt 应用程序就会成为这两种语言弱属性的牺牲品。Qt/C++对Rust的调用可能不安全,域间可能会出现并发问题,Rust的高性能和可移植性可能会打折扣。

  本文介绍了为嵌入式应用程序实现 Rust 和 Qt 世界的推荐原则。

  Rust 和 Qt 的背景

  Rust 拥有丰富的库生态系统,用于序列化和反序列化、异步操作、解析不安全输入、线程、静态分析等,而 Qt 是一个 C++ 工具包,支持跨各种平台的丰富的、基于 GUI 的应用程序,从 iOS 到嵌入式Linux。Qt 应用程序包括表示业务逻辑的 C++ 插件、定义 GUI 组件和布局的 QML 以及用于 GUI 脚本的 JavaScript。

  将 Rust 库集成到 Qt 框架中,为开发人员提供了一种强大的机制来提供高性能和安全的基础,从而推动复杂的用户体验。考虑一个农业仪表板应用程序,该应用程序需要安全访问后端 I/O 微控制器或从基于 RTOS 的单板计算机提取数据的飞机驾驶舱显示器。

  一种推荐的应用程序架构是将业务逻辑保留在 Rust 后端,并为用户界面使用 Qt/C++ 插件。这解耦了两个域,将 Qt/QML 用于 GUI 开发的快速迭代速度和灵活性从业务级代码中分离出来。

  如何为嵌入式软件集成 Rust 和 Qt

  将 Qt 与 Rust 集成的常见方法是让 Rust 调用 Qt 的 C++ 库。虽然有几种绑定方法,但这些方法通常不是 Rust 惯用的,并且往往会失去安全优势,如图 1 所示。此外,Qt 的大多数 Rust 绑定不会将 Rust 代码暴露给 C++,这使得它们难以集成到现有的 C++ 代码库中。

  

GUI

  图 1. Rust-Qt 绑定的挑战()

  一种更有效的方法是以安全的方式连接两种语言,尽可能多地保留 Rust 固有的安全优势。为此,Rust 需要能够使用自己的QObject子类和实例以的妥协来扩展 Qt 对象系统。

  后一种方法的一个例子是开源库CXX-Qt,它由 KDAB 发起并管理其正在进行的开发和改进。该库将 Qt 强大的面向对象和元对象系统与 Rust 相结合,并基于和扩展了另一个开源 Rust/C++ 互操作性库CXX 。在 CXX-Qt 中,新的QObject子类由 Rust 模块中的项目组成,以执行桥接功能。这些子类就像 QML 和 C++ 中的任何其他QObject一样被实例化,将 Rust 特性暴露给 Qt。

  CXX-Qt 定义的每个QObject都包含两个组件:

  一个基于 C++ 的对象,它是一个公开属性和可调用方法的包装器

  存储属性、实现可调用对象、管理内部状态并处理来自属性和后台线程的更改请求的Rust结构

  CXX-Qt 自动生成代码以在 Rust 和 Qt/C++ 域之间传输数据,并使用名为CXX的库在两者之间进行通信。

  CXX-Qt 桥接方法的主要原则

  为了解释有效的 Rust-Qt 桥是如何工作的,我们将描述 CXX-Qt 库背后的几个关键原则。

  在 Rust 中声明 QObject

  Qt 的设计本质上是面向对象的,对于 C++ 和 QML 都是如此,而 Rust 不支持继承和多态性等常见的类特性。为了克服这个限制,CXX-Qt 在 Rust 中扩展了 Qt 对象系统,以提供一种自然的方式来集成两种语言,同时保持惯用的 Rust 代码。

  CXX-Qt 桥可以包括以下部分:

  模块周围的宏指示内容与 CXX-Qt 相关

  定义 QObject、Qt 的任何属性和任何私有状态的结构

  Qobject 结构的可选实现,其中函数可以标记为 qinvokable,允许从 QML 和 C++ 调用它们

  定义 QObject 信号的枚举

  普通 CXX 块

  CXX-Qt 在代码生成期间将此 Rust 模块扩展为QObject的 C++ 子类和RustObj结构,如图 2 所示。

  图 2. CXX-Qt 如何扩展 QObjects 以供运行时使用()

  下面是一个 QObject 示例,其中包含三个用于跨域操作的可调用方法和一个 Rust-only 方法:

  #[cxx_qt::bridge]

  mod my_object {

  不安全的外部“C++”{

  包括!(“cxx-qt-lib/qstring.h”);

  输入 QString = cxx_qt_lib::QString;

  包括!(“cxx-qt-lib/qurl.h”);

  输入 QUrl = cxx_qt_lib::QUrl;

  }

  #[cxx_qt::qobject]

  #[推导(默认)]

  酒吧结构我的对象{

  #[q属性]

  is_connected:布尔值,

  #[q属性]

  网址:QUrl,

  }

  #[cxx_qt::qsignals(MyObject)]

  酒吧枚举连接{

  连接的,

  错误{消息:QString},

  }

  实现 qobject::MyObject {

  #[qinvokable]

  pub fn connect(mut self: Pin《&mut Self》, url: QUrl) {

  self.as_mut().set_url(url);

  如果自己

  .as_ref()

  .url()

  .to_string()

  .starts_with(“https://kdab.com”)

  {

  self.as_mut().set_is_connected(true);

  self.emit(Connection::Connected);

  } 别的 {

  self.as_mut().set_is_connected(false);

  self.emit(连接::错误{

  message: QString::from(“URL 不以 https://kdab.com” 开头),

  });

  }

  }

  }

  }

  使用#[qinvokable]属性声明的方法(例如上面的connect)暴露给 QML 和 C++,参数和返回类型在 Qt 端匹配。使用#[qproperty] 属性(例如上面的 is_connected)声明的 QObject 结构中的字段作为 Q_PROPERTY 公开给 QML 和 C++。用#[cxx_qt::qsignals(T)] 属性标记的枚举定义了在 QObject T 上声明的信号。请注意,CXX-Qt 会自动在 snake_case (Rust) 和 CamelCase (Qt) 命名之间进行转换。

  在此示例中,connect 方法用于从 Rust 改变 QObject 的 url 和 connected 属性。然后发出一个信号,指示连接是否成功或是否发生错误。

  没有用属性标记的方法或字段被认为是 Rust 私有的,可用于管理内部状态或用作线程实例数据。

  由于QObject是桥的 C++ 端拥有的构造 Qt 对象,因此它会在运行时销毁 C++ 对象时被销毁。

  跨桥声明通用数据类型

  原始数据类型和 CXX 类型可以跨桥使用,不需要在 Rust 和 Qt 之间进行转换。库 cxx_qt_lib 提供代表常见 Qt 类型(例如 QColor、QString、QVariant 等)的 Rust 类型,以供跨桥使用。

  随着项目的发展,我们计划在cxx_qt_lib中加入更多常用的Qt类型,比如Qt容器类型(如QHash和 QVector) 和其他对 Qt C++ 或 QML 有用的类型。然后,我们希望通过向 Rust 生态系统中已建立的板条箱添加转换来增强这些类型。例如,我们计划支持从 QColor 到颜色包类型的转换或从 QDateTime 到日期时间包类型的转换,或者使用 Rust 包对 Qt 类型进行(反)序列化等。

  保持线程安全

  Rust-Qt 应用程序中安全线程背后的一般概念是每当 Rust 代码执行时在 C++ 端获取一个锁,以防止该代码被多个线程执行。这意味着所有直接从 C++ 调用的 Rust 代码,例如可调用对象,仅在 Qt 线程上执行。

  为了允许开发人员将状态从后台 Rust 线??程同步到 Qt,CXX-Qt 提供了一个帮助程序,允许您从后台线程对 Rust 闭包进行排队。然后从 Qt 事件循环中执行闭包。

  // 在可调用对象中,请求 qt 线程的句柄

  让 qt_thread = self.qt_thread();

  // 产生一个 Rust 线??程

  std::thread::spawn(移动 || {

  让 value = compute_value_on_rust_thread();

  // 使用闭包移动值并在 Qt 事件循环中运行任务

  线程

  .queue(移动|mut qobject| {

  // 发生在 Qt 事件循环中

  qobject.set_value(值);

  })

  .unwrap();

  });

  将 Rust QObject 暴露给 QML

  要在 Qt 应用程序中使用 Rust 代码,必须将 Rust QObject 导出到 QML。CXX-Qt 通过为Rust 中定义的每个QObject子类生成一个 C++ 类来简化这个过程——开发人员只需要在 main.cpp 中包含两个语句:

  包括 QObject 类,例如,

  #include “cxx-qt-gen/my_object.h”

  将 QObject 类导出到 QML,例如,

  qml注册类型( “com.kdab.cxx_qt.demo” , 1 , 0 , “MyObject” );

  构建系统

  CXX-Qt 支持使用 CMake 或 Cargo 构建。

  使用 CMake,Corrosion(以前称为 cmake-cargo)用于将 Rust crate 作为静态库导入,然后链接到您的 Qt 应用程序可执行文件或库。这允许将 CXX-Qt 轻松集成到现有的 CMake Qt 应用程序中。构建 Rust crate 时,build.rs 文件会编译 CXX-Qt 生成的任何 C++ 代码。

  对于使用 Rust 的 Cargo 构建系统的项目,CXX-Qt 也可以在没有 CMake 的情况下从 Cargo 使用。使用此方法,Rust 项目的 build.rs 文件还会触发 Qt 资源的构建、任何其他 C++ 文件的编译以及链接到任何其他 Qt 模块。

  结论

  随着集成 Rust 和 Qt 的用例的增长,开发团队应该知道他们除了一对一直接绑定之外的选择。CXX-Qt 库提供了一种可行的桥接机制,以惯用的方式保留了 Rust 的线程安全和性能优势,同时还允许开发人员使用普通的 Qt 和 Rust 代码。

  通过理解这里的概念,包括 Qt 对象和 Rust QObject之间的映射、Rust 的常见 Qt 类型以及定义运行时互操作性的宏和代码生成,开发人员可以更好地为他们的应用程序开发选择正确的 Rust-Qt 集成机制。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分