Aurora OS 的 Rust 开发指南

大家好!我是 Открытая мобильная платформа 公司的首席工程师,该公司致力于开发"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_PATH。PKG_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 项目中自行研究。
更多推荐


所有评论(0)