Android项目接入Rust

背景

本次讨论在已有的安卓项目工程中配置rust的方法,同时记录一下踩坑过程,首先本地pc端配置好rust的开发环境,配置rust的过程就默认你会了。

项目配置

项目结构

在已有的项目,我们推荐新建一个module,这里不需要native module,就是普通的java module。

image.png

我们把rust工程可以放到module平级目录,或module下面都是可以的。

image.png

我们把所有rust相关的代码,全部放到rustsdk工程目录,可以单独打开,单独成仓库,单独维护。和这个rust相关的桥接层,我们全部放在本地的java module里面。

项目根grade配置

主要的思路是参考https://github.com/mozilla/rust-android-gradle
现在项目的跟目录下配置rust-android-gradle,具体的版本,可以参考github上最新的

1
2
3
4
5
6
7
8
9
10
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "org.mozilla.rust-android-gradle:plugin:0.9.3"
}
}

image.png

module gradle配置

先给模块添加插件,

1
apply plugin: "org.mozilla.rust-android-gradle.rust-android"

然后主要的就是配置cargo这个dsl,具体的参数表意可以在github上面看
image.png
需要主要的是,modlue 直到 sdk的目录,包含Cargo.toml的位置,libname需要和Cargo.toml文件里面指定的一致,其它按需配置即可。
按需要添加交叉编译器

1
2
rustup target add armv7-linux-androideabi   # for arm
rustup target add aarch64-linux-android # for arm64

可选,每次编译的时候,触发一次rust的编译

1
2
3
4
5
tasks.configureEach { task ->
if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) {
task.dependsOn 'cargoBuild'
}
}

或者项目目录下,手动

1
./gradlew cargoBuild  --stacktrace --info

上面基本安卓项目里面就配置完了。下面展示一个调用示例, java调一下rust。

1
2
3
4
5
6
7
package com.example.mylibrary;
public class Tools {
static {
System.loadLibrary("rust_sdk");
}
public native int addNum(int num1,int num2,);
}

Rust SDK配置

Cargo.toml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[package]
name = "rust_sdk"
version = "0.1.0"

[dependencies]
log = "0.4.14"
jni-sys = "0.3.0"
jni = "0.21.1"

#可选,指定android下才添加依赖
[target.'cfg(target_os = "android")'.dependencies]
#ndk = { version = "0.8.0" }

[lib]
crate_type = ["dylib"]
#指定源码文件
path = "src/lib.rs"

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
extern crate jni;
extern crate jni_sys;

use jni::JNIEnv;
use jni::objects::JClass;
use jni_sys::jint;

#[allow(non_snake_case)]
#[no_mangle]
pub unsafe extern fn Java_com_example_mylibrary_Tools_addNum(_env: JNIEnv, _: JClass, numa: jint, numb: jint) -> jint {
numb + numa
}

然后,在Android Studio里面编译app调用这个方法就ok了。这里会有目标so,可以用检查一下导出函数。
image.png

我没有直接把所有依赖放到 [target.’cfg(target_os = “android”)’.dependencies]下,是因为这样在pc端可以直接编译,检查错误,利于开发。

比起👇🏻新建mod的方法,目前ide,还支持的不是很友好,也不好编程,错误也不好检查。只有去编译目标产物的时候才会发现。
image.png
而脱离目标平台的方法,一些逻辑我是可以提前测试验证的。

注意事项:

java.lang.UnsatisfiedLinkError: dlopen failed: library “librustsdk.so” not found

根据代码是用的什么loadLibrary方法,如果是

1
System.loadLibrary("rust_sdk");

是不需要添加lib前缀的,尽管生成的lib文件带有前缀,例如 librust_sdk.so,还有需要留意你在grade里面指定的文件

1
2
3
4
5
6
7
cargo {
module = "./../rust_sdk" // Or whatever directory contains your Cargo.toml
libname = "rust_sdk" // Or whatever matches Cargo.toml's [package] name.
targets = ["arm64"] // See bellow for a longer list of options
verbose = true
profile = "debug"
}

java.lang.UnsatisfiedLinkError: No implementation found

这个是方法找不到,需要查一下,方法名称和参数是不是对应得上,一个排查方法,就是去看so的导出函数,有没有。

1
2
3
 ./ objdump -T librust_sdk.so | grep addNum

000000000012e2ec g DF .text 0000000000000094 Java_com_example_mylibrary_Tools_addNum

还有一个很容易忽略的点,大坑 apk打包的流程如果是有缓存的话,并不会去检查so是否有更新,写完rust,直接就去编译apk,所以apk里面的so很有可能是旧的,如果是旧的,就需要重新rebuild一次,可以去检查一下。JNI的方法调用,推荐用方法名去做区分,不要用参数重载之类的方法,去做区分,如果你写了一个add函数,发现你多传一个参数,也是可以调的。