背景
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 | //! hello android |
注意一定要保留头部的//! 注释,其是用于生成文档的,如果缺失构建的时候会报错(error: missing documentation for the crate)。
随后在该目录中建立Android.bp,如下所示。
1 | rust_binary { |
static_executable 配置是直接生成静态链接文件(默认是动态链接,在本机启动稍微麻烦一些)
其中ld_flags添加的原因是默认情况下build/soong/cc/config/global.go中添加了-Wl,–exclude-libs,libunwind.a。
静态链接时会报如下错误。这里应该是代码还没调整好。
1 | ld.lld: error: undefined symbol: _Unwind_Backtrace |
建立完成后,回到android 根目录下,使用标准的方式进行构建即可。
1 | source build/envsetup.sh |
由于是静态链接文件,可以在x86服务器上直接运行构建出的程序。
1 | mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source$ out/target/product/vsoc_x86_64/system/bin/hello_rs |
与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 | mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source/external/rust/rust_c_bind$ cat mytestc.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端对c库的封装和依赖
使用如下Android.bp生成rust封装接口。
1 | cc_prebuilt_library_shared { |
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 | rust_binary { |
在代码中直接使用crate名称即可调用封装的函数,如下所示。
1 | //! hello android |
然后使用如下命令即可启动程序(如果没有构建过bionic,需要进入bionic目录mma一次,构建出linker)。
1 | export MY_AOSP_ROOT=/work/mj/aosp_source/ |
程序打印如下:
1 | Hello Android! |
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文件如下。然后修改该目录下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
19extern crate libc;
use libc::{c_int, c_double};
pub struct rust_result {
r_t1 : c_int,
r_t2 : c_int,
r_f : c_double
}
pub extern "C" fn rust_mody(input : *mut rust_result) {
unsafe {
(*input).r_t1 += 1;
(*input).r_t2 += 2;
(*input).r_f += 3.14;
}
}修改后,可以构建出libtestc_bind_rust.dylib.so这个动态库,这个动态库与普通c构建出的动态库没有区别(引入头文件,直接调用对应符号即可)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19cc_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"]
}在c库端调用rust功能动态库
在external/rust/rust_c_bind目录,修改c库端的代码如下。新增call_rust_mody 用于调用rust生成的功能代码,同时将其开放到头文件中。
这里使用了松耦合的dlopen+dlsym方式来执行rust功能,因此无需在链接时指定rust功能代码所在的动态库。如果使用普通的直接调用方式。则需要在构建系统中添加依赖。然后继续使用同样的命令构建出c端动态库。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
51mj@oppo-HP-ProDesk-680-G4-MT:/work/mj/aosp_source/external/rust/rust_c_bind$ cat mytestc.c
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
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);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 | //! hello android |
可以看到执行call_rust_mody后,result结果按预期产生了变化。
1 | Hello Android! |