0%

使用rust写安卓库

使用rust写安卓库

rust写安卓库也是rust的一个重要应用方向,之前用来写安卓的库的语言大多数都是c/c++。本文不讨论两种(或者叫两类)语言的优劣,只说明如何搭建一个rust-android相互交互的环境。

Android使用Java语言,java与c/c++交互使用jni技术。Android与rust交互也使用jni。在这里我们使用jni-rs库。在使用rust写安卓库之前我们先简化一下问题。先用jni-rs编写一个可以被java调用的库。这个过程可以参考jni-rs的代码说明。但我自己使用的过程中稍微对文档进行了说明。

使用jni-rs库

既然rust作为库,那java端就是使用者。所以一切使用以java端出发。先编写一个HelloWorld.java文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HelloWorld {

private static native String hello(String input);

static {
System.loadLibrary("mylib");
}

// The rest is just regular ol' Java!
public static void main(String[] args) {
String output = HelloWorld.hello("TigerInYourDream");
System.out.println(output);
}
}

代码的第一行是将来rust库需要提供的方法,这里相当于提供一个接口,定义了函数名,参数和返回值。静态代码块是加载库用的,需要在实际调用之前加载。动态库的名称为 ”mylib“。现在是没有的。之后就是常规的调用。这里的调用是静态调用。

现在这段java代码无法运行,因为我们并没有”mylib“这个库。hello methon也需要在rust中实现。我们暂时不着急创建rust lib。我们在目录下使用javac -h命令

1
javac -h . HelloWorld.java

注意要使用 .java 后缀,以免无法识别。

接下来会在目录下生成一个 .h 文件。这个文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: hello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_HelloWorld_hello
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

不要修改这个文件。我们看其中最重要的部分,我们定义rust方法就要按照 .h 文件中的要求来。方法名必须为 JNICALL Java_HelloWorld_hello。知道这一点后开始写rust lib。

直接创建cargo项目 mylib。

1
cargo new mylib --lib

在项目的cargo.toml添加如下

1
2
3
4
5
[dependencies]
jni = "0.16.0"

[lib]
crate_type = ["cdylib"]

这样配置之后如果你运行cargo build。在linux下会得到libmylib.so。在mac下会得到libmylib.dylib。

编写lib.rs

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
use jni::JNIEnv;

use jni::objects::{JClass, JString};

use jni::sys::jstring;

// This keeps Rust from "mangling" the name and making it unique for this
// crate.
#[no_mangle]
pub extern "system" fn Java_HelloWorld_hello(env: JNIEnv,
// This is the class that owns our static method. It's not going to be used,
// but still must be present to match the expected signature of a static
// native method.
class: JClass,
input: JString)
-> jstring {
// First, we have to get the string out of Java. Check out the `strings`
// module for more info on how this works.
let input: String =
env.get_string(input).expect("Couldn't get java string!").into();

// Then we have to create a new Java string to return. Again, more info
// in the `strings` module.
let output = env.new_string(format!("Hello, {}!", input))
.expect("Couldn't create java string!");

// Finally, extract the raw pointer to return.
output.into_inner()
}

Jni-rs库是一个bundle。函数的实现基本上是ffi的标准写法。类型要和jni中的对应。jstring就相当于Java中的String。这里函数一定要和 .h 文件中的一样。代码很简单,获取参数input.然后和Hello 拼接。最后输出。

接下来进行编译即可。拿到库文件,放倒java项目下即可运行。更详细的东西可以自己去jni-rs下面看example。

重点关注的是,java -h命令和rust中的函数如何编写。

再更新

很多人说这样做会找不到库的位置,会出现如下错误

Exception in thread “main” java.lang.UnsatisfiedLinkError: no mylib in java.library.path

和之前没有这个库的报告的错误是一样的。这是因为定位不到库,代码不知道如何去定位”mylib.so”(linux)或者”mylib.dylib”(mac)。假如使用IDEA,请Edit Configurations(就是运行按钮)

在VM Options栏目添加上如下

-Djava.library.path=/path/to/your/lib/target/debug

即指定你lib库的地址即可。注意你实际的so文件叫做libxxxxx.so,但是你在java中loadLibrary时候依然加载的是 xxxxx(你项目的名字)。

以上即可正常运行。但是如果要考虑自动化,请使用别的成套构建工具。但是从写库的角度,这样直接写起来测试即可。毕竟交付的是so文件。

进一步使用交叉编译

上一节的内容并不涉及交叉编译,只是简单讲解了如何使用jni-rs库,以及我们写jni时的主要思路:从使用端去定义接口,然后在rust端实现代码。这一部分开始进入正题,如何使用rust写安卓的库。

在使用之前需要先安装rust环境,默认读者都装了。

然后安装Android环境。我们直接使用AndroidStudio即可。安装好之后,直接在上面的菜单栏找到Tools–>SDK manager–>SDK Tools。

在下面安装如下四个

1
2
3
4
* Android SDK Tools
* NDK
* CMake
* LLDB

这四个一个都不能少。我的环境是MAC。CMake和LLDB之前也装的有,但是为了和NDK配套在这里又装一次。之前没装的状况下,最终交叉编译失败。NDK也可以自行下载安装,但是我建议用AndroidStudio安装,比较方便也很配套,会省去很多麻烦。

还需要注意的事情看清楚上面的Android SDK Location,下好之后需要配置这个路径。

下载好之后先export NDK路径。我使用zsh,修改.zshrc文件,加入如下代码

1
2
export ANDROID_HOME=/Users/$USER/Library/Android/sdk
export NDK_HOME=$ANDROID_HOME/ndk/21.0.6113669

zsh语法不再说明,其实就是加载ndk环境变量。21.0.61xxxx是我目前下载的ndk的版本文件名,读者如果是别的路径或者叫别的文件名 比如:ndk-r21之类的自行修改即可。fish bash 用户自己酌情修改。

接下来创建一个目录 greeting

1
mkdir greeting

这个文件夹是放后面的NDK文件的,我建议把NDK文件,rust project 甚至将来的Android Project放在一起,方案管理。

之后

1
2
cd greeting
mkdir NDK

然后执行

1
2
3
${NDK_HOME}/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir NDK/arm64
${NDK_HOME}/build/tools/make_standalone_toolchain.py --api 26 --arch arm --install-dir NDK/arm
${NDK_HOME}/build/tools/make_standalone_toolchain.py --api 26 --arch x86 --install-dir NDK/x86

执行上面三个命令行,大家应该看出来了,就是为了执行ndk tools中的make_standalone_toolchain.py脚本,然后东西放到NDK文件夹下。执行命令会出现warn! 既然不报错,假装没看到。如果不喜欢我这种文件组织形式,按照这个思路自己换路径即可。

打开NDK文件夹我们应该可以看到arm64 arm x86这三个文件夹了。

接下来我们在创建一个cargo-config.toml。touch应该不用我说。在这个文件中写入

1
2
3
4
5
6
7
8
9
10
11
[target.aarch64-linux-android]
ar = "<project path>/greetings/NDK/arm64/bin/aarch64-linux-android-ar"
linker = "<project path>/greetings/NDK/arm64/bin/aarch64-linux-android-clang"

[target.armv7-linux-androideabi]
ar = "<project path>/greetings/NDK/arm/bin/arm-linux-androideabi-ar"
linker = "<project path>/greetings/NDK/arm/bin/arm-linux-androideabi-clang"

[target.i686-linux-android]
ar = "<project path>/greetings/NDK/x86/bin/i686-linux-android-ar"
linker = "<project path>/greetings/NDK/x86/bin/i686-linux-android-clang"

这里的就是项目有路径,具体点就是/Users/xxx 这种。读者自己写入自己的具体路径就可以。

然后把这个文件拷入.cargo 文件夹下的config文件中

1
cp cargo-config.toml ~/.cargo/config

可以自己去 .cargo文件夹下看这个文件。需要保证ar 和 linker路径正确。接下来运行

1
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android

加入这三个target。

我们还是在greeting文件夹下创建自己的rust lib

1
2
cargo new cargo --lib
mkdir android

rust项目叫cargo似乎不太好,但暂时这样。后面的android文件夹是放后面的anroid项目的。这里再次建议:NDK,rust,Android这三个项目放在一个文件夹下方便管理。本例中就是放在greeting文件夹下。

然后在cargo/src/lib.rs中随便写点代码

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::os::raw::{c_char};
use std::ffi::{CString, CStr};

#[no_mangle]
pub extern fn rust_greeting(to: *const c_char) -> *mut c_char {
let c_str = unsafe { CStr::from_ptr(to) };
let recipient = match c_str.to_str() {
Err(_) => "there",
Ok(string) => string,
};

CString::new("Hello ".to_owned() + recipient).unwrap().into_raw()
}

无伤大雅,随便写点。这是一段可以和c交互的代码。

然后开始在android目录下创建一个安卓项目,具体过程非本文重点,不展示。

我们在GreetingsActivity同级的文件中建立一个叫做RustGreetings.java的类。写上如下代码

1
2
3
4
5
6
7
8
public class RustGreetings {

private static native String greeting(final String pattern);

public String sayHello(String to) {
return greeting(to);
}
}

这个例子和第一节中的java代码作用是类似的。定义需要的native方法 greeting。下面一段是需要用的greeting methon的sayHello method。

接下来我们开始回去写rust代码 cargo/src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// Expose the JNI interface for android below
#[cfg(target_os="android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;

use super::*;
use self::jni::JNIEnv;
use self::jni::objects::{JClass, JString};
use self::jni::sys::{jstring};

#[no_mangle]
pub unsafe extern fn Java_com_mozilla_greetings_RustGreetings_greeting(env: JNIEnv, _: JClass, java_pattern: JString) -> jstring {
// Our Java companion code might pass-in "world" as a string, hence the name.
let world = rust_greeting(env.get_string(java_pattern).expect("invalid pattern string").as_ptr());
// Retake pointer so that we can use it below and allow memory to be freed when it goes out of scope.
let world_ptr = CString::from_raw(world);
let output = env.new_string(world_ptr.to_str().unwrap()).expect("Couldn't create java string!");

output.into_inner()
}
}

这才是需要的rust代码。至于为什么的的方法是com mozilla。因为我在创建Android项目时候用domain用了mozilla.com。总体来说这个方法名体现的就是路径+接口方法名,和第一节体现出的命名规则是一致的。如果不熟悉 建议用java -h 。从头文件中把文件名copy过来。

#[cfg(target_os=”android”)]代表的是条件编译。细心的人已经看到我们用extern crate jni。所以我们去cargo.toml加上。

Cargo.tomal需要如下

1
2
3
4
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.16", default-features = false }
[lib]
crate-type = ["dylib"]

这和前一节的示例是一样的。cfg选项依然对应条件编译。配置好之后我们可以开始编译了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

cd ../android/Greetings/app/src/main
mkdir jniLibs
mkdir jniLibs/arm64
mkdir jniLibs/armeabi
mkdir jniLibs/x86

ln -s <project_path>/greetings/cargo/target/aarch64-linux-android/release/libgreetings.so jniLibs/arm64/libgreetings.so
ln -s <project_path>/greetings/cargo/target/armv7-linux-androideabi/release/libgreetings.so jniLibs/armeabi/libgreetings.so
ln -s <project_path>/greetings/cargo/target/i686-linux-android/release/libgreetings.so jniLibs/x86/libgreetings.so

这一段干了三件事情

  1. 编译到三种不同so文件。现在工具不够好,得一个一个编译.

  2. 在我们的android目录下建立jniLibs文件,熟悉安卓的话知道,这是为了放编译好的so文件的

  3. 软连接,把lib下面的so文件软连接到第二步建立好的jniLibs文件目录下。当然你直接拷贝过去用也是可以的。看我这个实例你应该明白,你要用绝对路径做symlinks。不可以用相对路径,不然找不到。

    然后我们在GreetingsActivity (主Activity)里面加上如下代码

    1
    2
    3
    static {
    System.loadLibrary("greetings");
    }

    强调下必须在 onCreate method 之前就加

    接下来去对应的activity-greetings.xml中随便写点界面,反正能用到刚才定义的方法即可。

    然后我们再打开GreetingsActivity写上这个

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_greetings);

    RustGreetings g = new RustGreetings();
    String r = g.sayHello("world");
    ((TextView)findViewById(R.id.greetingField)).setText(r);
    }

    就假定刚才的用的控件id为greetingField。然后运行App即可。注意他是安卓App,用模拟器或者真机。

    好了,你已经开始Hello World了!

    文章可以结束了