首页 > 编程语言 >Rust 在 Android 的编程实践——技术驱动的车云一体化解决方案探索

Rust 在 Android 的编程实践——技术驱动的车云一体化解决方案探索

时间:2024-11-10 20:56:58浏览次数:1  
标签:车云 backtrace 编译 Android panic com Rust

Rust 在 Android 的编程实践——技术驱动的车云一体化解决方案探索
Greptime 车云一体化解决方案颠覆了从前传统的车云协同模式,采用更加低成本、高效率的方案来满足当前的市场需求。其中 GreptimeDB Edge 作为核心组件,专为车机环境量身打造。本文旨在详尽探讨在 Android 平台利用 Rust 语言进行开发过程中所积累的经验和教训。

交叉编译
在车机场景下,GreptimeDB Edge 通常以服务的形式部署在 Android 环境上,这要求我们将其编译成适用于 Android 平台的可执行文件。一个初步的方案可能是购置一款 Android 开发板,安装 Rust 工具链以进行编译工作。然而,这种做法可能会面临以下挑战:

在 Android 开发板上配置 Rust 编译环境可能比较复杂(作者实际也没配置过);

多数 Android 开发板的 CPU 性能较弱,编译大型项目时速度缓慢,效率低下;

本地 Android API 版本可能与目标设备上的 API 版本存在差异,甚至 CPU 架构也可能不同,从而导致兼容性问题。

相对而言,交叉编译提供了一个更为高效的替代方案。它允许开发者在一个系统平台上(例如 x86 的 PC)编译出能在另一种系统平台上(例如 ARM 移动设备)运行的程序,这在目标系统上直接编译困难时尤其有用。

Rust 对交叉编译的支持非常出色,加之 Android NDK 提供了必要的工具链和库,这进一步简化了交叉编译的过程。由于我们的开发或编译环境通常是 macOS 或 Linux,所以选择通过交叉编译的方式来生成 Android 可执行文件是一个理想的解决方案。

Rust 编译
首先,我们需要大致了解一下 Rust 编译过程。Rustc 先把 Rust 代码编译为 LLVM-IR,然后再由 LLVM 将 LLVM-IR 编译为各个平台的二进制,最终由 linker 链接在一起,生成最终的二进制文件。

Rustc 是 Rust 的编译器,以 LLVM 作为后端(也可以说 Rustc 是 LLVM 的前端)。

下面是一个简化版本的 Rust 编译架构图:

(图 1 :简化版本的 Rust 编译架构图)

GreptimeDB 交叉编译实战
GreptimeDB Edge 是以开源版本的 GreptimeDB 为内核进行构建。所以,我们接下来,以开源版本的 GreptimeDB 为例, 一步一步向大家展示如何在 x86 Linux 上进行交叉编译,生成 aarch64-linux-android 架构的可执行文件。

首先安装 Android NDK,下载地址为: https://developer.android.com/ndk/downloads?hl=zh-cn。此外设置一个环境变量,方便后续操作,如下所示:

export ANDROID_NDK_HOME=<YOUR_NDK_ROOT>

示例

export ANDROID_NDK_HOME=/home/fys/soft/ndk/android-ndk-r25c

接下来,从 GitHub 上拉取 GreptimeDB 的源码:

git clone https://github.com/GreptimeTeam/greptimedb.git --depth 1

然后,添加 Target 到 Rust 工具链是实现跨平台编译的关键步骤。这允许 Rustc 将中间表示层 LLVM-IR 代码编译成目标平台的机器语言。在这个例子中,目标平台架构是 aarch64-linux-android,在 GreptimeDB 项目根目录下执行以下命令:

rustup target add aarch64-linux-android

Rust 平台支持详见这里。

这时候,尝试编译可能会报错: “-lgcc” 找不到。原因是:Android NDK 的 libgcc.a 已经被 libunwind.a 替代,解决方案是复制一份 libunwind.a 并重命名为 libgcc.a,详见 Rust blog。

具体路径可能随着ndk版本的不同,需要改动。

cd $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64/
cp libunwind.a libgcc.a

在仅涉及 Rust 语言的项目中,开发者通常需要配置链接器(Linker)和归档器(AR)。然而,Rust 支持在构建脚本(build.rs)中执行构建任务,因此在 Rust 项目的编译过程中,可能需要集成 C 和 C++ 等其他语言的编译工作。这通常需要向编译工具(如 cc 或 cmake 提供一些必要的信息,包括编译器路径(CC 和 CXX)、库文件和头文件的位置等,这一过程往往较为复杂。

cargo-ndk 这个项目帮我们解决了大部分问题。通过执行以下命令,就可以编译出适用于 aarch64-linux-android 平台的 GreptimeDB 二进制程序。

cargo ndk --platform 30 -t aarch64-linux-android build --bin greptime --release

此外,针对那些不兼容特定目标平台的库,处理起来确实较为棘手。一种解决方案是替换为兼容的库;如果涉及到功能并非必需,可以使用 feature guard ,在编译阶段将其去掉。

在编译过程中,如果遇到错误提示缺少 protobuf 库或其他,正确安装即可。

常见问题
之前遇到一个问题,当开启 LTO 优化时,交叉编译 GreptimeDB 就会失败。报错信息如下所示:

= note: ld.lld: error: duplicate symbol: pthread_atfork
>>> defined at crtbegin.c
>>> /home/fys/soft/ndk/android-ndk-r26d/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/lib/aarch64-linux-android/23/crtbegin_dynamic.o:(pthread_atfork)
>>> defined at build_jemalloc.6cd863fbc26b10-cgu.0
>>> /home/fys/source/build_jemalloc/target/aarch64-linux-android/nightly/deps/build_jemalloc-c1434931e7fc5ee2.build_jemalloc.6cd863fbc26b10-cgu.0.rcgu.o:(.text.pthread_atfork+0x0)
clang-17: error: linker command failed with exit code 1 (use -v to see invocation)

当 API level >= 21 时,Android 会提供一个 pthread_atfork 的声明。而 tikv-jemallocator 中也有一个 pthread_atfork 的声明,都是强符号类型,当开启 LTO 优化时,就会导致了符号冲突。解决方案:将 tikv-jemallocator 中 pthread_atfork 设置为弱符号类型。

最新版本的 tikv-jemallocator 已经解决了这个问题,详见这里。

Backtrace on Android
在开发 GreptimeDB Edge 项目的过程中,我们观察到 Rust 语言的标准库的 backtrace 在 Android 环境中无法提供预期的堆栈信息。具体来说,当程序 panic 时,相关的堆栈信息未能正确捕获,而是显示为 unknown,这为问题的诊断带来了极大的困扰。

问题复现
为了复现这一问题,我们编写了一个简化的示例程序。

在 main 方法中触发了一个 panic,模拟程序出现异常:

fn main() {
panic!("Panic here.");
}

指定 rust-toolchain 为 stable 1.81 或者更低的版本:

[toolchain]
channel = "1.81"

交叉编译, 生成在 Android 上运行的二进制文件。在此过程中,我们可以回顾并巩固上一节的内容:

export ANDROID_NDK_HOME=<YOUR_NDK_ROOT>
rustup target add aarch64-linux-android
cargo ndk --platform 28 -t aarch64-linux-android build --release

将二进制 push 到 Android 虚拟机,执行:

RUST_BACKTRACE=full ./<二进制文件路径>

执行结果表明,未能成功获取到预期的 backtrace 信息。问题复现了!

thread 'main' panicked at src/main.rs:2:5:
Panic here
stack backtrace:
0: 0x5d908f7a7535 -
1: 0x5d908f7b336b -
2: 0x5d908f7a617f -
3: 0x5d908f7a8541 -
4: 0x5d908f7a817e -
5: 0x5d908f7a8fe8 -
6: 0x5d908f7a8ed3 -
7: 0x5d908f7a7a09 -
8: 0x5d908f7a8b94 -
9: 0x5d908f7b2753 -
10: 0x5d908f7a1e0c -
11: 0x5d908f7a1db3 -
12: 0x5d908f7a1da9 -
13: 0x5d908f7a4eb9 -
14: 0x5d908f7a1e35 -
15: 0x7d6d9c64478d -
解决方案
我们先介绍一下解决方案,以便对问题原因不感兴趣的的小伙伴可以跳过下一节。

升级 Rust 工具链版本: 建议将 rust-toolchain 版本升级至 1.82 或更高。这个问题已经在 1.82 中被修复了(下一小节会介绍修复方法)。

自定义 Panic Hook: Rust 支持通过注册自定义的 panic hook 函数来替代默认行为。若无法升级 Rust 版本,可利用 backtrace-rs 库设置自定义 panic hook 函数。

Rust 默认的 panic hook 函数可能无法满足特定环境下的需求,例如,在 Android 平台上,可能倾向于将 panic 信息输出到文件或 logcat, 而默认的 panic hook 函数只是把 panic 信息输出到标准错误中。因此,很多场景下都需要我们自定义 panic hook 函数。

下面提供一个实现示例:

pub fn set_panic_hook() {
#[cfg(windows)]
const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &str = "\n";

std::panic::set_hook(Box::new(move |panic| {
    let backtrace = backtrace::Backtrace::new();

    let Some(l) = panic.location() else {
        log::error!(
            "Panic: {:?}, backtrace: {}{:#?}",
            panic, LINE_ENDING, backtrace
        );
        return;
    };

    log::error!(
        "Panic: {:?}, file: {}, line: {}, col: {}, backtrace: {}{:#?}",
        panic,
        l.file(),
        l.line(),
        l.column(),
        LINE_ENDING,
        backtrace,
    );
}));

}

输出的堆栈信息如下所示(在编译选项中去掉了 debug info, 且保留了符号表):

cfrw.scffy.cn
cfrw.cnjiasi.cn
cfrw.xintiao78.com
cfrw.62nsfs.com
cfrw.jinduoceramics.com
cfrw.shuixitech.com
cfrw.huanbao580.com
cfrw.szlcdpq.com
cfrw.sdymsxfh.com
cfrw.tanjiuspace.com
cfrw.yjh9988.com
cfrw.jyh01.com
cfrw.gzysart.com
cfrw.kfamaw.com
cfrw.shplcchina.com
cfrw.51yjjy.com
cfrw.testoppo.com
cfrw.huaxinlighting.com
cfrw.51jyo.com
cfrw.bctiantuo.com

Panic: PanicHookInfo { payload: Any { .. }, location: Location { file: "src/main.rs", line: 3, col: 5 }, can_unwind: true, force_no_backtrace: false }, file: src/main.rs, line: 3, col: 5, backtrace:
0: 0x5a58805bdf63 - cross_compile_on_android::set_panic_hook::{{closure}}::h8ff538cfa624b522
1: 0x5a58805fb0f3 - std::panicking::rust_panic_with_hook::h1f4c9072872fa4b1
2: 0x5a58805fadb3 - std::panicking::begin_panic_handler::{{closure}}::h73465221de2f2f04
3: 0x5a58805f9689 - std::sys::backtrace::__rust_end_short_backtrace::h67f67f7cadadf1c3
4: 0x5a58805faa74 - rust_begin_unwind
5: 0x5a5880605b63 - core::panicking::panic_fmt::h394cd2a8b9d0c24d
6: 0x5a58805bdf11 - cross_compile_on_android::main::h477274cf7246f129
7: 0x5a58805bdb53 - std::sys::backtrace::__rust_begin_short_backtrace::hb593986c2bdf2ffe
8: 0x5a58805bdb49 - std::rt::lang_start::{{closure}}::hdba3573990c4d5eb
9: 0x5a58805f4519 - std::rt::lang_start_internal::h50565391ca281790
10: 0x5a58805be1f5 - main
11: 0x7e5b4020278d - __libc_init

补充说明:输出的堆栈信息与编译选项也有关系。如果把二进制中的符号表和 debug info 都去掉,会生成 unknown 的堆栈。如果保留 debug info,堆栈信息将更详细,但二进制的体积会增加很多。

问题原因
接下来,我们将基于 Rust 1.81,来探究一下之前提出的问题。

前置知识
Rust 标准库的 backtrace 依赖了 backtrace-rs 库,并以 git submodule 的形式集成到了 Rust 标准库中,详见这里。

backtrace-rs 在编译构建时,会判断 Android 的 API 版本。如果大于等于 21,则会启用 dl_iterate_phdr 特性。详见这里(注: backtrace-rs 的版本是 Rust 1.81 依赖的版本,并不是最新版本)。

综合以上两点,Rust 标准库以 git submodule 的形式引入了 backtrace-rs,但是并没有执行 backtrace-rs 中的 build.rs 的构建逻辑,导致 dl_iterate_phdr 特性未能启用。那么标准库的 backtrace 就无法在 Android 上正常工作了。

破案了!

解决方法
实际上,我们只需在标准库中启用 backtrace-rs 的 dl_iterate_phdr 特性即可。但是从 #120593 开始,Rust 对 Android 的最小支持 API 版本从 19 提升至 21,并且 从 21 开始,Android 就支持了 dl_iterate_phdr,具体信息可以查看这里。所以我们可以在 backtrace-rs 库中直接默认开启 dl_iterate_phdr 特性,无需检测 Android 的 API 版本(Rust 1.82 也是这么修复的)。

相关 PR 链接

https://github.com/rust-lang/backtrace-rs/pull/656

https://github.com/rust-lang/rust/pull/129305

总结
交叉编译一直是非常棘手的,可能会碰到各种各样的问题,并没有什么固定的解决方案,我们总是要针对特定的问题进行处理。幸运的是,Cargo NDK 和 Android NDK 提供了一套便捷的解决方案,帮助我们有效地应对了大部分的编译问题。

通过本文的探讨,我们认识到交叉编译在 Android 环境中的重要性,以及 Rust 编译机制的优势。虽然理想的编译过程在实践中会遇到诸多挑战,但希望我们的经验能为后续的开发提供一些实用的参考和启发。

标签:车云,backtrace,编译,Android,panic,com,Rust
From: https://www.cnblogs.com/bkbk123/p/18538480

相关文章

  • 更新教程:如何以 6 种新方式将视频从 Android 传输到 Mac
    概括我们的生活充满了多媒体内容,在设备之间无缝传输视频的需求变得越来越重要。对于寻求将其珍贵视频转移到Mac生态系统的Android用户,本指南提供了多种方法的全面概述,确保该过程既高效又用户友好。无论是传统的USB连接还是无线替代方案,我们都将探索分步说明,使您能够轻松......
  • Rust为什么要搞个match匹配,直接用==判断不行吗(Rust match、Rust ==、Rust模式匹配)
    文章目录1.模式匹配2.更强的类型安全和完整性检查3.解构能力4.清晰和简洁示例Rust中的match关键字和使用==直接进行判断有着不同的用途和优势。match是一种非常强大的控制流结构,用于模式匹配,它不仅可以用来检查等值关系,还能解构、比较和检查类型中的......
  • Rust-AOP编程实战
    文章本天成,妙手偶得之。粹然无疵瑕,岂复须人为?君看古彝器,巧拙两无施。汉最近先秦,固已殊淳漓。胡部何为者,豪竹杂哀丝。后夔不复作,千载谁与期?——《文章》宋·陆游【哲理】文章本是不加人工,天然而成的,是技艺高超的人在偶然间所得到的。其实作者所说的“天成”,并不就是大自然的......
  • 在 termux 中配置 rust 写 wasm 的环境
    最近我开始学用rust写wasm。不用说,我是一个非常好学的人。所以我想随时随地都能学习wasm。刚好我手机上有个termux,我就开始琢磨在termux上配个环境。没想到还不是一件很容易的事。所以写这篇文章记录一下。安装rust很明显需要先安装rust。但是termux上好像没......
  • 【用Rust写CAD】第二章 第四节 变量
    文章目录1、变量定义2、变量命名规则3、不可变与可变4、变量隐藏5、类型推断1、变量定义如果要声明变量,需要使用let关键字。每个变量都有一个唯一的名称。声明变量后,可将其绑定到某个值,也可稍后在程序中绑定该值。以下代码声明名为a的变量。leta;a变量......
  • Android13 修改设备的density(dpi)
    DPIDPI,全称DotsPerInch,是一个衡量屏幕密度的关键指标。其中,Inch(英寸)作为物理单位,在任何设备上的大小都是恒定不变的。因此,DPI具体指的是在一英寸的物理长度内所能容纳的像素点(Dot)数量。例如,160DPI的屏幕意味着在一英寸的长度内包含160个像素点,而320DPI的屏幕则表明一英寸......
  • Android14——Launcher3删除“对话”微件、删除“通讯录微件下面的直接拨打电话,直接发
    删除“对话”微件/packages/apps/Launcher3/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java原始代码:publicList<ComponentWithLabelAndIcon>update(LauncherAppStateapp,@NullablePackageUserKeypackageUser){......
  • 【Android】ANR监控治理技术方案
    一、背景1、bugly针对crash监控做的比较好,但是对anr监控,早期版本监听/data/anr目录的变化可以拿到数据,但是现在高版本已经没有权限监听此目录,当前数据非常少,结合历次数据来看对我们解决ANR问题没有任何帮助。2、当前ANR数据主要来自手机厂商系统监控得到的的AnrTop数......
  • Android Audio中 AudioTrack、 AudioFlinger和 HAL 使用dump的区别
    Audiodump在定位音频的各种问题非常重要,我们主要在AudioTrack、AudioFlinger和HAL层中会用到,这里我们先明确一下在不同层使用dump的区别。以下是关于AudioTrack、AudioFlinger和HAL(HardwareAbstractionLayer,硬件抽象层)中dump的区别和使用场景:一、区别Audi......
  • 【北京迅为】itop-3588开发板摄像头使用手册Android12 双摄方案
     本章节对应资料在网盘资料“iTOP-3588开发板\02_【iTOP-RK3588开发板】开发资料\07_Android系统开发配套资料\08_Android12摄像头使用配套资料”目录下下载。 2.1Android12前摄+后摄网盘中默认的Android12源码支持四个摄像头单独打开,本小节我们来修改源码,实现同......