在这里插入图片描述

大家好!我是 Открытая мобильная платформа 公司的首席工程师,该公司致力于开发"Aurora"操作系统以及其他多项业务。最近,我创建了一个项目,演示了如何在 Windows/Linux/macOS 上为 Aurora OS 编译和运行 Rust 应用程序。

目前,Aurora OS 是一个移动操作系统。然而,已有原型系统允许将 Aurora OS 用作桌面或车载操作系统。Aurora OS 基于 Linux 内核。其前身是 Sailfish OS,但这两个系统早已独立发展。这是大体情况,关于该操作系统的更多信息可以从 https://auroraos.ru 开始了解。

Aurora OS 没有官方支持的 Rust 版本,其 PSDK(一套用于开发、调试和测试 Aurora OS 组件的工具集,仅可在 Linux 上安装)中包含的是一个过时的版本(1.6),不适合用于编写应用程序。我实现了演示项目 Rust for Aurora OS,展示了如何获取编写应用程序所需的一切:

  • 最新版本的 Rust
  • 对系统主要接口的访问
  • 应用程序和 RPM 软件包构建的细节
  • 在 Windows/Linux/macOS 上进行交叉编译

创建项目

可以在 Windows/Linux/macOS 上安装 Rust 语言,安装包中将包含 Cargo 构建系统,用于创建项目:

cargo new aurora-rs --bin

项目立即可用于构建,进入创建的 aurora-rs 目录并执行:

cargo run --

您应该能看到 Hello, world! 文本。我们可以从这里开始。

构建应用程序

Aurora OS 支持三种架构:armv7hl, aarch64, x86_64。Rust 具有便捷的交叉编译功能,但在 Windows 和 macOS 上配置针对 Linux(Aurora OS 的基础)的应用程序构建环境并非易事。我们希望能在所有主流操作系统(Windows/Linux/macOS)上自由工作。项目 Cross 可以帮助解决这个问题——您无需配置系统进行构建,只需在系统中安装 Docker 即可。

但 Docker 不仅用于构建——我们还需要它来安装 Aurora SDK。目前,对带有 Apple silicon (aarch64) 处理器的 macOS 的支持仅在 Aurora SDK 的测试版中可用,最新的测试版本是 5.2.75,所以我们将使用测试版。可能在您阅读本文时,5.2 版本已经稳定,对您来说会更简单。目前该版本仍在完善中,可能存在一些小错误。但如果您是经验丰富的开发者,应该不会遇到问题。

需要安装的正是 Build Tools 版本(BT)——这是带有 Docker 容器内构建的版本。但正如我所说,那里的 Rust 版本过时,不适合我们。我们将使用上游版本,我当前的版本是 1.9。我们在 Docker 中需要的是用于访问系统接口的库。那里我们有各种 C/C++/Qt/D-Bus 接口的集合。稍后我们会详细讨论,现在先进行构建。

可以使用以下命令安装 Cross:

cargo install cross --git https://github.com/cross-rs/cross

安装后,我们可以将构建命令 cargo 替换为 cross——系统将自动启动容器并在其中进行构建。例如,我们想为 x86_64 架构构建项目,执行:

cross build --target="x86_64-unknown-linux-gnu"

成功构建后,您将在 target/x86_64-unknown-linux-gnu/debug 目录中找到构建好的应用程序。但我们有三种架构(armv7hl, aarch64, x86_64)。对于 armv7hl, aarch64 架构,需要指定 linker。这可以在 .cargo 目录下的 config.toml 文件中完成(如果文件不存在,则需要创建)。

<project>/.cargo/config.toml

# armv7hl
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
rustflags = [
    "-C", "link-arg=-Wl,--as-needed",
]

# aarch64
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = [
    "-C", "link-arg=-Wl,--as-needed",
]

# x86_64
[target.x86_64-unknown-linux-gnu]
rustflags = [
    "-C", "link-arg=-Wl,--as-needed",
]

在这里,我们还可以通过 rustflags 指定通用的编译标志。--as-needed 标志指示链接器只包含指定库中程序实际使用的符号(函数、变量等)——我们不需要多余的东西,Aurora OS 中有一个严格的验证器,如果存在未处理的依赖项,它将不允许安装应用程序。

另一个重要的特性是 glibc 的版本。在 Aurora OS 5.2 中,glibc 版本是 2.38,最好完全匹配,但我做了折衷:简单性/兼容性。Ubuntu 24.04 使用 glibc 2.39 版本。它最接近 Aurora OS,并且构建的应用程序如果不使用 2.39 中的功能,将在 2.38 上运行。为了配置 Cross 使用 Ubuntu,我们可以在 Cargo.toml 中指定构建目标的设置:

# armv7hl
[workspace.metadata.cross.target.armv7-unknown-linux-gnueabihf]
image = "ubuntu:24.04"
pre-build = ["apt-get update && apt-get -y install curl build-essential clang pkg-config g++-arm-linux-gnueabihf"]

# aarch64
[workspace.metadata.cross.target.aarch64-unknown-linux-gnu]
image = "ubuntu:24.04"
pre-build = ["apt-get update && apt-get -y install curl build-essential clang pkg-config g++-aarch64-linux-gnu"]

# x86_64
[workspace.metadata.cross.target.x86_64-unknown-linux-gnu]
image = "ubuntu:24.04"
pre-build = ["apt-get update && apt-get -y install curl build-essential clang pkg-config"]

在这里,我们为每个构建目标指定了 image——Docker 镜像,并安装了交叉编译所需的一切。现在,执行构建命令后,Cross 将拉取我们需要的镜像并为目标架构构建应用程序:

# armv7hl
cross build --target="armv7-unknown-linux-gnueabihf"
# aarch64
cross build --target="aarch64-unknown-linux-gnu"
# x86_64
cross build --target="x86_64-unknown-linux-gnu"

Aurora CLI

Aurora CLI 应用程序是我的开发成果,是一个简化 Aurora OS 生态系统工作的工具。我们需要它来将我们的 Rust 应用程序提升到新的水平。这是 Aurora CLI 的第 4 个版本——跨平台,使用 Aurora SDK (BT) 来简化流程。该应用程序正在开发中,但已经可以帮助我们:

  • 在主机上创建包含库的目录,这些库是扩展应用程序功能和访问系统功能所必需的。
  • 为 Qt 类创建 MOC 文件,这些文件对于从 QObject 继承是必需的,从而允许在应用程序中使用 signal/slot
  • 构建用于安装到 Aurora OS 的 RPM 软件包,对其进行验证并使用公钥签名。

该应用程序具有命令行界面(CLI),并使用与 Aurora SDK (BT) 一起提供的 Docker 镜像进行工作。该应用程序可在 crates.io/aurora-cli 获取,并可通过以下命令在 macOS/Linux/Windows 上安装:

cargo install aurora-cli

构建软件包

为了将构建的应用程序安装到 Aurora OS,需要构建 RPM 软件包,对其进行签名,上传到模拟器/设备,执行安装命令,并通过负责安全性的验证器。

为此,让我们在项目中创建一个 rpm 目录,包含以下文件集:

.
├── applications
│   └── com.keygenqt.aurora_rs.desktop
├── bin
│   └── aurora-rs
├── com.keygenqt.aurora_rs.spec
└── icons
    └── hicolor
        ├── 108x108
        │   └── apps
        │       └── com.keygenqt.aurora_rs.png
        ├── 128x128
        │   └── apps
        │       └── com.keygenqt.aurora_rs.png
        ├── 172x172
        │   └── apps
        │       └── com.keygenqt.aurora_rs.png
        └── 86x86
            └── apps
                └── com.keygenqt.aurora_rs.png

文件的位置结构很重要,它在构建软件包时使用。软件包构建的配置在 spec 文件中进行,有关其配置的详细信息可以在 RPM 文档 中找到。我们的配置如下所示:

Name:       com.keygenqt.aurora_rs
Summary:    Rust and interacts with the main interfaces of Aurora OS.
Version:    0.0.1
Release:    1
License:    BSD 3-Clause

%description
%{summary}

%prep
%autosetup

%build

%install
mkdir -p %{buildroot}/%{_bindir}
mkdir -p %{buildroot}/%{_datadir}/icons
mkdir -p %{buildroot}/%{_datadir}/applications
mkdir -p %{buildroot}/%{_datadir}/%{name}/lib
install -m 0755 bin/aurora-rs %{buildroot}/%{_bindir}/%{name}
cp -a icons %{buildroot}/%{_datadir}
cp -a applications %{buildroot}/%{_datadir}

%files
%defattr(-,root,root,-)
%{_bindir}/%{name}
%defattr(644,root,root,-)
%{_datadir}/applications/%{name}.desktop
%{_datadir}/icons/hicolor/*/apps/%{name}.png

desktop 文件用于在系统中配置软件包:应用程序名称、描述、权限设置和其他应用程序设置。可以在此处了解其配置:.desktop 文件

[Desktop Entry]
Type=Application
Name=Aurora Rust
Comment=Rust and interacts with the main interfaces of Aurora OS.
Icon=com.keygenqt.aurora_rs
Exec=/usr/bin/com.keygenqt.aurora_rs
X-Nemo-Application-Type=silica-qt5

[X-Application]
Permissions=
OrganizationName=com.keygenqt
ApplicationName=aurora_rs

bin 目录中有 1 个文件——我们的 Rust 应用程序。Aurora CLI 将在此处查找它以确定所需的架构。

icons 目录中是一组图标,它们将被打包并随应用程序一起安装到系统中。请遵守结构和格式,或者根据您的需要更改 spec 文件的设置。

获得所有必需文件并根据需要配置好后,我们调用 Aurora CLI 的构建命令,指定包含 RPM 软件包文件的目录:

aurora-cli services --rpmbuild "/path/to/my/rpm"

之后,将启动来自 Build Tools 镜像的容器,并进行带有详细输出的构建。

软件包签名和验证

软件包已成功构建。但并不意味着它可以安装到设备/模拟器上。它必须通过验证并被签名。Aurora CLI 将通过以下命令帮助执行验证:

aurora-cli services --validate "/path/to/my/com.keygenqt.aurora_rs.rpm"

该命令将给出详细的报告和结果。关于验证需要知道的重要一点是:并非我们指定的所有依赖项在系统中都可用。也就是说,如果我们需要,例如,libfreetype.so 库,并且它甚至存在于系统库中,这并不意味着应用程序可以访问它,我们需要将它一起带上——我们接下来会这样做。

软件包签名在文档中有描述,为了测试应用程序的工作情况,我们不需要费心——为此有用于测试应用程序的公钥。Aurora CLI 将使用以下命令用公钥为您的 RPM 软件包签名:

aurora-cli services --keysign "/path/to/my/com.keygenqt.aurora_rs.rpm"

如果我们通过了验证,构建并签署了软件包——它就准备好安装到设备/模拟器上了。

安装软件包

我们将把软件包安装到模拟器上——安装 Aurora SDK 后,它对所有人可用。在 Windows/Linux 上,其架构是 x86_64 macOS 上是 aarch64 CLI 中与模拟器的交互尚未实现,但这没关系,我们可以通过终端自行完成所有操作。

首先,我们需要将软件包上传到模拟器。可以通过位于 Aurora SDK 路径 <sdk>/vmshare/ssh/private_keys/sdk 的密钥访问它,模拟器端口是 2223,用户是 defaultuser。我们可以使用以下命令上传软件包:

scp -P 2223 -i ~/AuroraOS/vmshare/ssh/private_keys/sdk \
  /path/to/my/com.keygenqt.aurora_rs.rpm \
  defaultuser@localhost:/home/defaultuser/Downloads

可以通过以下命令通过 ssh 连接到模拟器的终端:

ssh -p 2223 -i ~/AuroraOS/vmshare/ssh/private_keys/sdk defaultuser@localhost

/home/defaultuser/Downloads 目录中,您应该找到已上传并准备安装的软件包。可以使用以下命令执行安装:

gdbus call --system --dest ru.omp.APM --object-path /ru/omp/APM --method ru.omp.APM.Install ~/Downloads/com.keygenqt.aurora_rs.rpm '{}'

如果您已经安装了这个软件包,需要先将其删除:

gdbus call --system --dest ru.omp.APM --object-path /ru/omp/APM --method ru.omp.APM.Remove com.keygenqt.aurora_rs '{}'

应用程序已安装并准备启动。可以通过系统界面或通过终端使用以下命令启动应用程序:

runtime-manager-tool Control startDebug com.keygenqt.aurora_rs --output-to-console

启动应用程序后,我们应该在控制台中看到来自 Rust 应用程序的 Hello, world!。让我们增加难度,添加与系统的交互。

接口

我们如何从应用程序访问 Aurora OS 的功能?例如,我们想保存数据——如何获取保存缓存所需的路径?或者我们想获取系统提供的设备名称。也许我们需要获取网络连接状态。对于每一项任务,都有其对应的接口。C 语言库 appdir 可以帮助获取路径,获取设备名称需要使用 D-Bus。而网络状态可以通过 C++/Qt 获取。幸运的是,Rust 能够与所有这些接口交互。

D-Bus

D-Bus 是类 Unix 操作系统的系统级 IPC(进程间通信)和高级 API。许多任务可以通过这个接口完成。例如,我们将使用 Device Info API 来获取设备型号。

执行请求需要添加访问此信息的权限。在应用程序的 desktop 文件中添加 DeviceInfo

[X-Application]
Permissions=DeviceInfo

连接 dbus crate(就是它——它肯定能工作,例如 zbus 拒绝工作——Aurora OS 上的 D-Bus 有一些特点):

[dependencies]
dbus = "0.9.9"

我们只需要调用 getDeviceModel 方法:

use std::time::Duration;

use dbus::{Error, blocking::Connection};

const DEST: &str = "ru.omp.deviceinfo";
const PATH: &str = "/ru/omp/deviceinfo/Features";
const IFACE: &str = "ru.omp.deviceinfo.Features";

pub fn device_info_get_device_model() -> Option<String> {
    let conn = match Connection::new_system() {
        Ok(conn) => conn,
        Err(_) => return None,
    };
    let proxy = conn.with_proxy(DEST, PATH, Duration::from_millis(5000));
    let result: Result<(String,), Error> = proxy.method_call(IFACE, "getDeviceModel", ());
    if let Ok((result,)) = result {
        Some(result)
    } else {
        None
    }
}

此函数将在成功时返回设备型号。

C 语言库

C 语言库具有稳定的应用程序二进制接口(ABI),并且许多语言通过外部函数接口(FFI)添加了对它的支持——Rust 也不例外。连接必要的 bindgen crate 以生成绑定。

[build-dependencies]
bindgen = "0.72"

我们将使用构建脚本 build.rs,只需将其添加到项目根目录即可。在其中,我们可以配置应用程序构建的附加参数。我们需要生成绑定以从 Rust 访问 C 库。还剩一个小细节——在哪里可以找到这些库?

可以使用 Aurora CLI 获取库。所有库都位于 Build Tools 的 Docker 镜像中,但我们需要它们在主机上。为此,我们可以执行命令来获取它们:

aurora-cli services --sysroot

命令成功完成后,您将在 ~/.aurora-sysroot 目录中找到 Aurora OS 的所有必需库,适用于所有架构:armv7hl, aarch64, x86_64。现在我们可以配置 build.rs

use std::{env, path::PathBuf};

fn main() {
    let sysroot = PathBuf::from(env::var("PKG_CONFIG_SYSROOT_DIR").unwrap());
    let include = &sysroot.join("usr/include").to_string_lossy().to_string();
    let lib = &sysroot.join("usr/lib").to_string_lossy().to_string();

    // Set rustflags sysroot
    println!("cargo:rustc-link-arg=--sysroot={}", sysroot.to_string_lossy());

    // Set search
    println!("cargo:rustc-link-search={lib}");

    // C appdir
    println!("cargo:rustc-link-lib=appdir");
    bindgen::Builder::default()
        .header(format!("{include}/appdir/appdir.h"))
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("Unable to generate appdir")
        .write_to_file(
            PathBuf::from(env::var("OUT_DIR").unwrap()).join("libappdir.rs")
        )
        .expect("Couldn't write appdir!");
}

环境变量 PKG_CONFIG_SYSROOT_DIR 指向包含我们所需架构的 ~/.aurora-sysroot 目录。对于所有操作系统,这将是不同的,而我们的构建是通过 Cross 进行的,它在 Docker 容器中执行构建。让我们添加一个本地用户配置 Cross.local.toml,这将是他的个人配置——并设置环境变量,同时将目录挂载到容器中。对于我的 macOS,配置如下所示:

# armv7hl
[target.armv7-unknown-linux-gnueabihf]
env.volumes = [
    "PKG_CONFIG_SYSROOT_DIR=/Users/keygenqt/.aurora-sysroot/armv7hl",
    "PKG_CONFIG_LIBDIR=/Users/keygenqt/.aurora-sysroot/armv7hl/usr/lib",
    "PKG_CONFIG_PATH=/Users/keygenqt/.aurora-sysroot/armv7hl/usr/lib/pkgconfig",
]

# aarch64
[target.aarch64-unknown-linux-gnu]
env.volumes = [
    "PKG_CONFIG_SYSROOT_DIR=/Users/keygenqt/.aurora-sysroot/aarch64",
    "PKG_CONFIG_LIBDIR=/Users/keygenqt/.aurora-sysroot/aarch64/usr/lib",
    "PKG_CONFIG_PATH=/Users/keygenqt/.aurora-sysroot/aarch64/usr/lib/pkgconfig",
]

# x86_64
[target.x86_64-unknown-linux-gnu]
env.volumes = [
    "PKG_CONFIG_SYSROOT_DIR=/Users/keygenqt/.aurora-sysroot/x86_64",
    "PKG_CONFIG_LIBDIR=/Users/keygenqt/.aurora-sysroot/x86_64/usr/lib",
    "PKG_CONFIG_PATH=/Users/keygenqt/.aurora-sysroot/x86_64/usr/lib/pkgconfig",
]

可以在构建命令前添加 CROSS_CONFIG 来应用配置。从项目根目录,命令如下所示:

CROSS_CONFIG="Cross.local.toml" cross build --target="x86_64-unknown-linux-gnu"

现在必要的目录将被挂载到容器中,并设置环境变量。这里有三个:PKG_CONFIG_SYSROOT_DIR, PKG_CONFIG_LIBDIR, PKG_CONFIG_PATHPKG_CONFIG_SYSROOT_DIR 指向全局 sysroot(编译器应在此处查找库的位置),另外两个用于通过 pkg-config 搜索库——我们在连接 C++/Qt 时使用它。

如果您通过 Visual Studio Code 工作,rust 分析器也想知道环境变量 PKG_CONFIG_SYSROOT_DIR 和其他变量,可以在用户设置中指定:

"rust-analyzer.cargo.extraEnv": {
    "PKG_CONFIG_SYSROOT_DIR": "/Users/keygenqt/.aurora-sysroot/aarch64",
    "PKG_CONFIG_LIBDIR": "/Users/keygenqt/.aurora-sysroot/aarch64/usr/lib",
    "PKG_CONFIG_PATH": "/Users/keygenqt/.aurora-sysroot/aarch64/usr/lib/pkgconfig"
},

请根据您自己的情况调整路径,这是本地设置,虽然不多,但还是有的,没有它们怎么行呢。

为了将 bindgen 生成的绑定添加到项目中,需要创建一个包含以下内容的文件:

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(improper_ctypes)]

include!(concat!(env!("OUT_DIR"), "/libappdir.rs"));

将此文件作为一个模块引入,库中的所有方法都对我们可用。获取应用程序的缓存目录路径:

use std::ffi::CStr;

use crate::clib::libappdir;

pub fn get_app_cache_location() -> Option<String> {
    let result = unsafe {
        let out = libappdir::appdir_get_path(
            libappdir::PathType_AppCacheLocation
        );
        let str = CStr::from_ptr(out);
        let str = str.to_owned();
        String::from_utf8(str.to_bytes().to_vec())
    };
    match result {
        Ok(app_cache) => Some(app_cache),
        Err(_) => None,
    }
}

此函数将返回应用程序目录的路径,可以在其中保存缓存。

С++/Qt

连接 C++ 库就没那么简单了。C++ 具有不稳定的 ABI,并且没有为其制作 FFI。但我们可以使用优秀的 CXX crate,它允许我们从 Rust 与 C++ 交互。我们还将连接 pkg-config crate,它将负责查找所有 C++ 库依赖项。

[build-dependencies]
cxx-build = "1.0"
pkg-config = "0.3"

为了演示工作,我们将连接 Qt5Network 库并从中执行一个方法。我们还将添加一个 C++ 类对 QObject 的继承——这对于使用 Qt 的 signal/slot 系统是必需的。我们现在不会使用它,但如果您需要,您会知道如何操作。

CXX 假设对 C++ 代码进行封装,也就是说,我们需要制作 C++ 代码,在其中我们将与 C++ 库交互,而它又将与 Rust 交互。让我们来实现这一点。

在项目中创建目录:src/cxx/cpp/qnetwork。在其中添加头文件和实现文件:

cxx_qnetwork.h

#pragma once
#include "rust/cxx.h"
#include <memory>

#include <QNetworkConfigurationManager>

namespace com
{
  namespace keygenqt
  {
    namespace aurora_rs
    {
      class CxxQnetwork : public QObject
      {
        Q_OBJECT

      public:
        CxxQnetwork();
        bool is_online() const;

      private:
        QNetworkConfigurationManager m_manager;
      };

      // Create class
      std::unique_ptr<CxxQnetwork> new_cxx_qnetwork();
    } // namespace aurora_rs
  } // namespace keygenqt
} // namespace com

cxx_qnetwork.cpp

#include "aurora-rs/src/cxx/cpp/qnetwork/cxx_qnetwork.h"
#include "aurora-rs/src/cxx/rust/cxx_qnetwork.rs.h"

namespace com
{
  namespace keygenqt
  {
    namespace aurora_rs
    {
        std::unique_ptr<CxxQnetwork> new_cxx_qnetwork()
        {
            return std::make_unique<CxxQnetwork>();
        }

        CxxQnetwork::CxxQnetwork() {}

        bool CxxQnetwork::is_online() const
        {
            return m_manager.isOnline();
        }
    } // namespace aurora_rs
  } // namespace keygenqt
} // namespace com

#include "cxx_qnetwork.moc"

请注意实现文件底部的这一行:

#include "cxx_qnetwork.moc"

我们没有这个文件,但 Aurora CLI 可以为我们创建它。要生成 Qt 的 MOC 文件,请执行以下命令:

aurora-cli services --moc /path/to/project

命令成功执行后,您将在头文件旁边看到一个 cxx_qnetwork.moc 文件,这是 Qt 工作所必需的。

将 CXX 连接到 Rust。为此,在项目中创建目录:src/cxx/rust 并添加文件 cxx_qnetwork.rs,该文件将与我们所创建的 C++ 类交互:

#[cxx::bridge(namespace = "com::keygenqt::aurora_rs")]
mod ffi {
    unsafe extern "C++" {
        include!("aurora-rs/src/cxx/cpp/qnetwork/cxx_qnetwork.h");

        type CxxQnetwork;

        fn new_cxx_qnetwork() -> UniquePtr<CxxQnetwork>;
        fn is_online(&self) -> bool;
    }
}

/// Example execute
pub fn is_online() -> bool {
    ffi::new_cxx_qnetwork().is_online()
}

为了使 CXX 工作,需要在 Cargo.toml 中添加 links,在我的例子中,包名为 aurora-rs

[package]
links = "aurora-rs"

剩下的就是连接 Qt 库,构建后,函数 is_online 应能指示网络连接状态。但为了使其工作,需要在 desktop 文件中添加 Internet 访问权限:

[X-Application]
Permissions=DeviceInfo;Internet

连接 C++ 库和生成 C++ 绑定的过程可以如下所示:

if cfg!(target_os = "linux") {
    let cpp_libs = vec!["Qt5Core", "Qt5Network"];
    let includes: Vec<Vec<PathBuf>> = cpp_libs
        .iter()
        .map(|lib| pkg_config::probe_library(lib).unwrap().include_paths)
        .collect();
    for bridge_name in ["format", "qnetwork"] {
        cxx_build::bridge(format!("src/cxx/rust/cxx_{bridge_name}.rs"))
            .includes(includes.iter().flatten())
            .std("c++17")
            .file(format!("src/cxx/cpp/{bridge_name}/cxx_{bridge_name}.cpp"))
            .compile(bridge_name);
        println!("cargo:rerun-if-changed=src/cxx/cpp/{bridge_name}/cxx_{bridge_name}.h");
        println!("cargo:rerun-if-changed=src/cxx/cpp/{bridge_name}/cxx_{bridge_name}.cpp");
        println!("cargo:rerun-if-changed=src/cxx/rust/cxx_{bridge_name}.rs");
    }
    for lib in cpp_libs {
        println!("cargo:rustc-link-lib={lib}");
    }
}

这里需要注意几点:

  • 除了 Qt5Network 本身,还需要连接 Qt5Core
  • 生成仅适用于 Linux——但这不影响在其他操作系统上工作,对于它们可以简单地禁用。
  • 库需要在生成后绑定到应用程序,否则您可能会遇到 undefined reference 问题。

我们已准备好构建并执行 is_online 方法。

GUI

应用程序已经能够与 D-Bus, C-lib, C++/Qt 协同工作,但我们如何将这些信息显示在屏幕上呢?为此,我们将使用为 Aurora OS 修改过的 winit crate,并在 skia-safe 上绘制第一帧。连接所有必需项可以这样做:

[dependencies]
winit = { git = "https://github.com/lmaxyz/winit", branch = "aurora" }
skia-safe = "0.90.0"
softbuffer = "0.4.6"
image = "0.25.6"

我不会列出所有代码,可以在项目仓库中熟悉它。主窗口在 show 方法中创建,在这里我们对其进行初始化并订阅事件。当收到 WindowEvent::RedrawRequested 事件时,我们获取表面缓冲区,在 skia 上绘制内容,将其转换为图像并绘制:

use image::GenericImageView;
use softbuffer::Context;
use softbuffer::Surface;
use std::num::NonZeroU32;
use winit::event::WindowEvent;
use winit::event::Event;
use winit::event_loop::{ControlFlow, EventLoop};

use crate::AppInfo;
use crate::window::app_skia;

#[path = "application.rs"]
mod application;

pub fn show(app_info: AppInfo, event_loop: EventLoop<()>) {
    let app = application::WinitAppBuilder::with_init(
        move |elwt| {
            let app_info = app_info.clone();
            let window = application::make_window(&app_info, elwt, |w| w);
            let context = Context::new(window.clone()).unwrap();
            (app_info, window, context)
        },
        |_elwt, (_, window, context)| Surface::new(context, window.clone()).unwrap(),
    )
    .with_event_handler(|(app_info, window, _context), surface, event, elwt| {
        elwt.set_control_flow(ControlFlow::Wait);
        match event {
            Event::WindowEvent {
                window_id,
                event: WindowEvent::RedrawRequested,
            } if window_id == window.id() => {
                let Some(surface) = surface else {
                    eprintln!("RedrawRequested fired before Resumed or after Suspended");
                    return;
                };
                let size = window.inner_size();
                if let (Some(width), Some(height)) =
                    (NonZeroU32::new(size.width), NonZeroU32::new(size.height))
                {
                    surface
                        .resize(width, height)
                        .expect("Failed to resize surface");
                    // Get image from skia draw
                    let image = app_skia::main::draw(
                        app_info,
                        u32::from(width) as i32,
                        u32::from(height) as i32
                    ).expect("Failed to draw image");
                    // Update buffer
                    let mut buffer = surface.buffer_mut().unwrap();
                    let width = image.width() as usize;
                    for (x, y, pixel) in image.pixels() {
                        let red = pixel.0[0] as u32;
                        let green = pixel.0[1] as u32;
                        let blue = pixel.0[2] as u32;
                        let color = blue | (green << 8) | (red << 16);
                        buffer[y as usize * width + x as usize] = color;
                    }
                    // Redraw surface
                    buffer.present().unwrap();
                }
            }
            _ => {}
        }
    });
    application::run(event_loop, app);
}

该应用程序是演示性的,为了不使其过载,图像只绘制一次。因此,这种从 skia 获取 image_snapshot 并将其绘制到缓冲区中的简单方法非常适合我们,只需一次。在 skia 上绘制是一项引人入胜的任务,它是一个相当低级的库:

use image::DynamicImage;

use skia_safe::{Color, EncodedImageFormat, Font, FontMgr, FontStyle, Paint, TextBlob};

use crate::AppInfo;

pub fn draw(
    app_info: &AppInfo,
    width: i32,
    height: i32,
) -> Result<DynamicImage, Box<dyn std::error::Error>> {
    // Create surface
    let mut surface = skia_safe::surfaces::raster_n32_premul((width, height)).unwrap();
    let canvas = surface.canvas();

    // Get font
    let typeface = FontMgr::new()
        .legacy_make_typeface(None, FontStyle::default())
        .unwrap();

    // Get paint
    let paint = &mut Paint::default();

    // Set background color
    canvas.clear(Color::from_rgb(50, 50, 50));

    let shift = 90;

    // Title
    paint.set_color(Color::from_rgb(246, 87, 0));
    let font_size = 40.0;
    let text = &app_info.title;
    let x = width / 2 - (text.len() as i32 * font_size as i32 / 4);
    let y = (height / 2) - shift;
    let font = Font::from_typeface(&typeface, font_size);
    let blob = TextBlob::from_str(text, &font).unwrap();
    let _ = canvas.draw_text_blob(&blob, (x - 5, y), paint);

    // D-Bus
    paint.set_color(Color::WHITE);
    let font_size = 24.0;
    let text = &app_info.model;
    let x = width / 2 - (text.len() as i32 * font_size as i32 / 4);
    let y = (height / 2 + 40 + 40) - shift;
    let font = Font::from_typeface(&typeface, font_size);
    let blob = TextBlob::from_str(text, &font).unwrap();
    let _ = canvas.draw_text_blob(&blob, (x - 10, y), paint);

    // C lib
    paint.set_color(Color::WHITE);
    let font_size = 24.0;
    let text = &app_info.location;
    let x = width / 2 - (text.len() as i32 * font_size as i32 / 4);
    let y = (height / 2 + (40 + 40) + 24 + 20) - shift;
    let font = Font::from_typeface(&typeface, font_size);
    let blob = TextBlob::from_str(text, &font).unwrap();
    let _ = canvas.draw_text_blob(&blob, (x - 10, y), paint);

    // C++ clear
    paint.set_color(Color::WHITE);
    let font_size = 24.0;
    let text = &app_info.format;
    let x = width / 2 - (text.len() as i32 * font_size as i32 / 4);
    let y = (height / 2 + (40 + 40) + (24 + 20) + 24 + 20) - shift;
    let font = Font::from_typeface(&typeface, font_size);
    let blob = TextBlob::from_str(text, &font).unwrap();
    let _ = canvas.draw_text_blob(&blob, (x - 15, y), paint);

    // C++ Qt
    paint.set_color(Color::WHITE);
    let font_size = 24.0;
    let text = &format!("Is online: {}", app_info.is_online);
    let x = width / 2 - (text.len() as i32 * font_size as i32 / 4);
    let y = (height / 2 + (40 + 40) + (24 + 20) + (24 + 20) + 24 + 20) - shift;
    let font = Font::from_typeface(&typeface, font_size);
    let blob = TextBlob::from_str(text, &font).unwrap();
    let _ = canvas.draw_text_blob(&blob, (x, y), paint);

    // Get image
    let image = surface.image_snapshot();
    let mut context = surface.direct_context();
    let binding = image
        .encode(context.as_mut(), EncodedImageFormat::PNG, None)
        .expect("Fail get image");
    Ok(image::load_from_memory(binding.as_bytes())?)
}

在文章中,我没有描述创建纯 C++(无依赖)——format,我们直接看了使用 Qt 依赖的复杂例子。

现在我们可以整合所有应用程序来显示从 Aurora OS 获取的数据。在 main.rs 中添加以下代码:

use winit::event_loop::EventLoop;

mod clib;
mod cxx;
mod dbus;
mod window;

#[derive(Clone, Debug)]
pub struct AppInfo {
    title: String,
    model: String,
    location: String,
    format: String,
    is_online: bool,
}

impl AppInfo {
    fn new() -> Self {
        AppInfo {
            title: "Rust for Aurora OS".to_string(),
            model: dbus::dbus_deviceinfo::device_info_get_device_model().unwrap(),
            location: clib::c_appdir::get_app_cache_location().unwrap(),
            format: cxx::rust::cxx_format::time("yyyy-mm-ddThh:mm:ssZ"),
            is_online: cxx::rust::cxx_qnetwork::is_online(),
        }
    }
}

fn main() {
    // Run window
    window::app_winit::main::show(AppInfo::new(), EventLoop::new().unwrap());
}

应用程序已准备就绪,但这还不是终点。还有一个重要的细节——验证器不会放过我们:Skia 使用了 freetype 库,该库是被禁止的,系统中应用程序无法访问它。我们需要把它带上。它在 Aurora CLI 创建的 ~/.aurora-sysroot 中有,不需要自己构建。首先将依赖项放入 rpm 目录:

.
├── applications
│   └── com.keygenqt.aurora_rs.desktop
├── bin
│   └── aurora-rs
├── com.keygenqt.aurora_rs.spec
├── icons
│   └── hicolor
│       ├── 108x108
│       │   └── apps
│       │       └── com.keygenqt.aurora_rs.png
│       ├── 128x128
│       │   └── apps
│       │       └── com.keygenqt.aurora_rs.png
│       ├── 172x172
│       │   └── apps
│       │       └── com.keygenqt.aurora_rs.png
│       └── 86x86
│           └── apps
│               └── com.keygenqt.aurora_rs.png
└── libs
    ├── aarch64
    │   └── libfreetype.so.6
    ├── armv7hl
    │   └── libfreetype.so.6
    └── x86_64
        └── libfreetype.so.6

为了告诉验证器库是我们自带的,在 spec 文件开头添加几行:

%global __provides_exclude_from ^%{_datadir}/%{name}/lib/.*$
%global __requires_exclude ^lib(freetype)\\.so.*$

并将其带入 rpm 包,修改 %install%files

%install
mkdir -p %{buildroot}/%{_bindir}
mkdir -p %{buildroot}/%{_datadir}/icons
mkdir -p %{buildroot}/%{_datadir}/applications
mkdir -p %{buildroot}/%{_datadir}/%{name}/lib
install -m 0755 bin/aurora-rs %{buildroot}/%{_bindir}/%{name}
install -m 644 libs/%{_target_cpu}/*so* %{buildroot}/%{_datadir}/%{name}/lib
cp -a icons %{buildroot}/%{_datadir}
cp -a applications %{buildroot}/%{_datadir}

%files
%defattr(-,root,root,-)
%{_bindir}/%{name}
%defattr(644,root,root,-)
%{_datadir}/%{name}/lib/*so*
%{_datadir}/applications/%{name}.desktop
%{_datadir}/icons/hicolor/*/apps/%{name}.png

构建时,具有所需架构的库将被添加到我们的 rpm 包中。但这还不是终点。当验证器找不到 RPATH 时会感到不安,Rust 构建时添加的是 RUNPATH,我们需要在 build.rs 中添加一行,明确指定 RPATH

println!("cargo:rustc-link-arg=-Wl,--disable-new-dtags,-rpath=/usr/share/com.keygenqt.aurora_rs/lib");

现在我们完成了,可以构建项目并获得以下结果:
在这里插入图片描述

所有描述的内容都可以在 Rust for Aurora OS 项目中自行研究。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐