Android Rust 接入

背景

google官方宣布在android中推动rust应用。希望了解当前android中rust接入的状态。

hello world 构建

参考 https://security.googleblog.com/2021/05/integrating-rust-into-android-open.html
AOSP已经在其 Soong 构建系统中支持了rust语言的构建,可以合理推断rust的工具链支撑也基本到位了(否则构建系统的支持无意义)。考虑直接尝试构建hello world程序。
https://android.googlesource.com/platform/build/soong/ 的文档入手,可以在 https://ci.android.com/builds/latest/branches/aosp-build-tools/targets/linux/view/soong_build.html 中找到当前已经支持的rust target类型。
我们需要的应该hello world程序,应该属于rust_binary类型。

下载最新的AOSP代码,在external/rust/下(注意这个目录不能随便选,aosp 限制了rust能使用的目录范围,放错位置会出现violates neverallow -dir:device/google/cuttlefish/* 这样的错误)新建一个目录hello_rust,然后在其中创建hello world 源代码文件hello.rs 如下所示。

1
2
3
4
5
6
//! hello android
fn main() {
// Statements here are executed when the compiled binary is called
// Print text to the console
println!("Hello Android!");
}

注意一定要保留头部的//! 注释,其是用于生成文档的,如果缺失构建的时候会报错(error: missing documentation for the crate)。
随后在该目录中建立Android.bp,如下所示。

1
2
3
4
5
6
rust_binary {
name: "hello_rs",
srcs: ["./hello.rs"],
ld_flags: ["-lunwind"],
static_executable: true
}

static_executable 配置是直接生成静态链接文件(默认是动态链接,在本机启动稍微麻烦一些)
其中ld_flags添加的原因是默认情况下build/soong/cc/config/global.go中添加了-Wl,–exclude-libs,libunwind.a。
静态链接时会报如下错误。这里应该是代码还没调整好。

1
2
3
4
5
6
7
8
ld.lld: error: undefined symbol: _Unwind_Backtrace
>>> referenced by libunwind.rs:90 (prebuilts/rust/linux-x86/1.51.0/src/stdlibs/library/std/src/../../backtrace/src/backtrace/libunwind.rs:90)
>>> out/soong/.intermediates/system/bt/mj_rust_test/hello_rs/android_x86_64_silvermont/hello_rs.hello.7rcbfp3g-cgu.0.rcgu.o:(_RNvXNvNtNtCsglGYCpMRyF7_3std10sys_common9backtrace6__printNtB2_16DisplayBacktraceNtNtCsfOHkQPwunBC_4core3fmt7Display3fmt)

ld.lld: error: undefined symbol: _Unwind_GetIP
>>> referenced by libunwind.rs:43 (prebuilts/rust/linux-x86/1.51.0/src/stdlibs/library/std/src/../../backtrace/src/backtrace/libunwind.rs:43)
>>> out/soong/.intermediates/system/bt/mj_rust_test/hello_rs/android_x86_64_silvermont/hello_rs.hello.7rcbfp3g-cgu.0.rcgu.o:(_RNCNvNtNtCsglGYCpMRyF7_3std10sys_common9backtrace10__print_fmts_0B7_)
clang-12: error: linker command failed with exit code 1 (use -v to see invocation)

建立完成后,回到android 根目录下,使用标准的方式进行构建即可。

1
2
3
4
source  build/envsetup.sh
lunch aosp_cf_x86_64_pc-userdebug
cd external/rust/hello_rust
mma

由于是静态链接文件,可以在x86服务器上直接运行构建出的程序。

1
2
3
mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source$ out/target/product/vsoc_x86_64/system/bin/hello_rs
Hello Android!
mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source$

与C交互

通过核对代码(external/rust/crates/bindgen),可以确认 android 使用了 https://github.com/rust-lang/rust-bindgen 来完成rust与C的交互。参考https://ci.android.com/builds/submitted/7279978/linux/latest/raw/rust.html#rust_bindgen 可以看到字段描述。
不借助这个bindgen工具,我们也可以进行rust与c的交互,只是用于描述C接口和数据结构的rust代码我们得自己手写。使用bindgen工具可以自动根据头文件生成出这些描述代码,减小我们的维护工作量。

rust使用c功能

在external/rust/ 建立目录,存放待封装的c库。

1
mkdir rust_c_bind; cd rust_c_bind; mkdir include

构建出待rust调用的c库

使用如下所示的示例代码,模拟一个待rust调用的c库,test.h是其对外头文件,addon.h是库的内部头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source/external/rust/rust_c_bind$ cat mytestc.c
#include "test.h"
testc_result_t* get_result()
{
static testc_result_t internal_data;

internal_data.t1 = 1;
internal_data.t2 = 123;
internal_data.f = 3.14;
return &internal_data;
}
mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source/external/rust/rust_c_bind$ cat test.h
#include "addon.h"
struct testc_result
{
int t1;
int t2;
double f;
};
typedef struct testc_result testc_result_t;
extern testc_result_t re;
extern testc_result_t* get_result();
mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source/external/rust/rust_c_bind$ cat include/addon.h
extern char * buf;

使用如下命令编译出动态库。

1
/work/mj/android-ndk-r21b/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android29-clang mytestc.c -o libmytestc.so -shared -fPIC -g3 -I ./ -I ./include/

编写rust端对c库的封装和依赖

使用如下Android.bp生成rust封装接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
cc_prebuilt_library_shared {
name: "libmytestc",
srcs: ["libmytestc.so"],
}

rust_bindgen {
name: "libtestc_bind",
wrapper_src: "./test.h",
crate_name: "testc_bind",
source_stem: "testc_bind_rs",
local_include_dirs: ["include"],
shared_libs : ["libmytestc"]
}

wrapper_src 是接口封装头文件(一般就是c库对外的公共头文件)
local_include_dirs 指定编译接口头文件时搜索的本地路径(#include “xxx”的路径)
shared_libs 指定被封装动态库的名称
cc_prebuilt_library_shared 提供了被封装动态库的target(libmytestc.so需要我们自行编译出来,这个模拟的是真实部署时我们依赖外部库的情况。需要用android ndk工具链编译,用aosp中的host编译也可以,但是很比较麻烦,需要自己配一堆参数)。

遗留问题:封装动态库依赖的libmytestc.so(readelf -d可以看到) 其搜索路径中包含了out/soong/.intermediates/external/rust/rust_c_bind/libmytestc/android_x86_64_silvermont_shared/libmytestc.so 这样的路径。感觉似乎有些不太正常,因为这个是构建路径,部署后可能找不到。但是可以通过patchelf等工具处理,暂时不进一步研究。

在rust端使用c库功能

我们在前面的hello world基础上进行扩展,修改其Android.bp。
添加rlibs一行引入封装库,去掉了static_executable 以便使用动态库。

1
2
3
4
5
6
7
rust_binary {
name: "hello_rs",
srcs: ["./hello.rs"],
ld_flags: ["-lunwind"],
edition: "2018",
rlibs: ["libtestc_bind"]
}

在代码中直接使用crate名称即可调用封装的函数,如下所示。

1
2
3
4
5
6
7
8
//! hello android
fn main() {
// Statements here are executed when the compiled binary is called
let re = unsafe { testc_bind::get_result()};
// Print text to the console
println!("Hello Android!");
unsafe {println!("result {}:{}:{}", (*re).t1, (*re).t2, (*re).f)};
}

然后使用如下命令即可启动程序(如果没有构建过bionic,需要进入bionic目录mma一次,构建出linker)。

1
2
3
4
export MY_AOSP_ROOT=/work/mj/aosp_source/
cd ${MY_AOSP_ROOT}
export LD_LIBRARY_PATH=${MY_AOSP_ROOT}/out/target/product/vsoc_x86_64/apex/com.android.runtime/lib64/bionic:${MY_AOSP_ROOT}/out/target/product/vsoc_x86_64/system/lib64
${MY_AOSP_ROOT}/out/target/product/vsoc_x86_64/symbols/apex/com.android.runtime/bin/linker64 ${MY_AOSP_ROOT}/out/target/product/vsoc_x86_64/system/bin/hello_rs

程序打印如下:

1
2
Hello Android!
result 1:123:3.14

c使用rust功能

c使用rust相对更为直接,主要的工作是在rust代码中添加特殊标注,确保生成的rust二进制代码能被c理解,包含如下三个部分。

  • 使用extern “C”修饰 ,确保rust使用与C兼容的ABI(例如传参规则)
  • 使用#[no_mangle]修饰,使得函数/数据名称保留原始名称,便于C端调用(这步可选,也可以用#[export_name = “xx”]指定导出名称)
  • 使用#[repr(C)] 修饰结构体,确保rust生成的内存布局与C一致
    完成上面的工作后,rust生成的二进制文件其实与c生成的没有区别了。因此在c端进行实际调用时,与调用其他c函数也没有区别。
    我们继续在上面hello world的基础上扩展,在rust端添加一个函数修改result的结果,在c端调用该函数,然后在rust中再打印result。

    构建待c端调用的rust函数

    在rust_c_bind目录下,新增c_call_rust.rs文件如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    extern crate libc;
    use libc::{c_int, c_double};

    #[repr(C)]
    #[no_mangle]
    pub struct rust_result {
    r_t1 : c_int,
    r_t2 : c_int,
    r_f : c_double
    }

    #[no_mangle]
    pub extern "C" fn rust_mody(input : *mut rust_result) {
    unsafe {
    (*input).r_t1 += 1;
    (*input).r_t2 += 2;
    (*input).r_f += 3.14;
    }
    }
    然后修改该目录下Android.bp如下,我们添加了rust_library_dylib目标使得刚刚新增的rust功能可以作为动态库被加载,这样使用比较灵活。如果没有这个需求,可以将其作为静态库,这样c端可以直接调用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    cc_prebuilt_library_shared {
    name: "libmytestc",
    srcs: ["libmytestc.so"],
    }

    rust_library_dylib {
    name: "libtestc_bind_rust",
    crate_name: "testc_bind_rust",
    srcs: ["c_call_rust.rs"]
    }

    rust_bindgen {
    name: "libtestc_bind",
    wrapper_src: "test.h",
    crate_name: "testc_bind",
    source_stem: "testc_bind_rs",
    local_include_dirs: ["include"],
    shared_libs : ["libmytestc"]
    }
    修改后,可以构建出libtestc_bind_rust.dylib.so这个动态库,这个动态库与普通c构建出的动态库没有区别(引入头文件,直接调用对应符号即可)。

    在c库端调用rust功能动态库

    在external/rust/rust_c_bind目录,修改c库端的代码如下。新增call_rust_mody 用于调用rust生成的功能代码,同时将其开放到头文件中。
    这里使用了松耦合的dlopen+dlsym方式来执行rust功能,因此无需在链接时指定rust功能代码所在的动态库。如果使用普通的直接调用方式。则需要在构建系统中添加依赖。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source/external/rust/rust_c_bind$ cat mytestc.c
    #include <stdio.h>
    #include <dlfcn.h>
    #include "test.h"
    testc_result_t* get_result()
    {
    static testc_result_t internal_data;

    internal_data.t1 = 1;
    internal_data.t2 = 123;
    internal_data.f = 3.14;
    return &internal_data;
    }

    void call_rust_mody(testc_result_t *in)
    {
    void *handle;
    void (*rust_func)(testc_result_t *);
    char *error;

    handle = dlopen("libtestc_bind_rust.dylib.so", RTLD_LAZY);
    if (!handle) {
    fprintf(stderr, "%s\n", dlerror());
    }

    dlerror(); /* Clear any existing error */

    rust_func = (void (*)(testc_result_t *)) dlsym(handle, "rust_mody");


    error = dlerror();
    if (error != NULL) {
    fprintf(stderr, "%s\n", error);
    }

    rust_func(in);
    dlclose(handle);
    }

    mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source/external/rust/rust_c_bind$ cat test.h
    #include "addon.h"
    struct testc_result
    {
    int t1;
    int t2;
    double f;
    };
    typedef struct testc_result testc_result_t;
    extern testc_result_t re;
    extern testc_result_t* get_result();
    extern void call_rust_mody(testc_result_t *in);
    然后继续使用同样的命令构建出c端动态库。
    1
    /work/mj/android-ndk-r21b/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android29-clang mytestc.c -o libmytestc.so -shared -fPIC -g3 -I ./ -I ./include/

在rust端查看调用后结果

修改rust端主程序,查看调用后的结果。
回到external/rust/hello_rust目录下,修改hello.rsr如下。添加c端的新接口call_rust_mody,其会再调用rust构建的动态库。

1
2
3
4
5
6
7
8
9
10
//! hello android
fn main() {
// Statements here are executed when the compiled binary is called
let re = unsafe { testc_bind::get_result()};
// Print text to the console
println!("Hello Android!");
unsafe {println!("result {}:{}:{}", (*re).t1, (*re).t2, (*re).f)};
unsafe {testc_bind::call_rust_mody(re)};
unsafe {println!("result {}:{}:{}", (*re).t1, (*re).t2, (*re).f)};
}

可以看到执行call_rust_mody后,result结果按预期产生了变化。

1
2
3
Hello Android!
result 1:123:3.14
result 2:125:6.28