使用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 | class HelloWorld { |
代码的第一行是将来rust库需要提供的方法,这里相当于提供一个接口,定义了函数名,参数和返回值。静态代码块是加载库用的,需要在实际调用之前加载。动态库的名称为 ”mylib“。现在是没有的。之后就是常规的调用。这里的调用是静态调用。
现在这段java代码无法运行,因为我们并没有”mylib“这个库。hello methon也需要在rust中实现。我们暂时不着急创建rust lib。我们在目录下使用javac -h命令
1 | javac -h . HelloWorld.java |
注意要使用 .java 后缀,以免无法识别。
接下来会在目录下生成一个 .h 文件。这个文件如下
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
不要修改这个文件。我们看其中最重要的部分,我们定义rust方法就要按照 .h 文件中的要求来。方法名必须为 JNICALL Java_HelloWorld_hello。知道这一点后开始写rust lib。
直接创建cargo项目 mylib。
1 | cargo new mylib --lib |
在项目的cargo.toml添加如下
1 | [dependencies] |
这样配置之后如果你运行cargo build。在linux下会得到libmylib.so。在mac下会得到libmylib.dylib。
编写lib.rs
1 | use jni::JNIEnv; |
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 | * Android SDK Tools |
这四个一个都不能少。我的环境是MAC。CMake和LLDB之前也装的有,但是为了和NDK配套在这里又装一次。之前没装的状况下,最终交叉编译失败。NDK也可以自行下载安装,但是我建议用AndroidStudio安装,比较方便也很配套,会省去很多麻烦。
还需要注意的事情看清楚上面的Android SDK Location,下好之后需要配置这个路径。
下载好之后先export NDK路径。我使用zsh,修改.zshrc文件,加入如下代码
1 | export ANDROID_HOME=/Users/$USER/Library/Android/sdk |
zsh语法不再说明,其实就是加载ndk环境变量。21.0.61xxxx是我目前下载的ndk的版本文件名,读者如果是别的路径或者叫别的文件名 比如:ndk-r21之类的自行修改即可。fish bash 用户自己酌情修改。
接下来创建一个目录 greeting
1 | mkdir greeting |
这个文件夹是放后面的NDK文件的,我建议把NDK文件,rust project 甚至将来的Android Project放在一起,方案管理。
之后
1 | cd greeting |
然后执行
1 | ${NDK_HOME}/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir NDK/arm64 |
执行上面三个命令行,大家应该看出来了,就是为了执行ndk tools中的make_standalone_toolchain.py脚本,然后东西放到NDK文件夹下。执行命令会出现warn! 既然不报错,假装没看到。如果不喜欢我这种文件组织形式,按照这个思路自己换路径即可。
打开NDK文件夹我们应该可以看到arm64 arm x86这三个文件夹了。
接下来我们在创建一个cargo-config.toml。touch应该不用我说。在这个文件中写入
1 | [target.aarch64-linux-android] |
这里的
然后把这个文件拷入.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 | cargo new cargo --lib |
rust项目叫cargo似乎不太好,但暂时这样。后面的android文件夹是放后面的anroid项目的。这里再次建议:NDK,rust,Android这三个项目放在一个文件夹下方便管理。本例中就是放在greeting文件夹下。
然后在cargo/src/lib.rs中随便写点代码
1 | use std::os::raw::{c_char}; |
无伤大雅,随便写点。这是一段可以和c交互的代码。
然后开始在android目录下创建一个安卓项目,具体过程非本文重点,不展示。
我们在GreetingsActivity同级的文件中建立一个叫做RustGreetings.java的类。写上如下代码
1 | public class RustGreetings { |
这个例子和第一节中的java代码作用是类似的。定义需要的native方法 greeting。下面一段是需要用的greeting methon的sayHello method。
接下来我们开始回去写rust代码 cargo/src/lib.rs
1 | /// Expose the JNI interface for android below |
这才是需要的rust代码。至于为什么的的方法是com mozilla。因为我在创建Android项目时候用domain用了mozilla.com。总体来说这个方法名体现的就是路径+接口方法名,和第一节体现出的命名规则是一致的。如果不熟悉 建议用java -h 。从头文件中把文件名copy过来。
#[cfg(target_os=”android”)]代表的是条件编译。细心的人已经看到我们用extern crate jni。所以我们去cargo.toml加上。
Cargo.tomal需要如下
1 | [target.'cfg(target_os="android")'.dependencies] |
这和前一节的示例是一样的。cfg选项依然对应条件编译。配置好之后我们可以开始编译了
1 | cargo build --target aarch64-linux-android --release |
这一段干了三件事情
编译到三种不同so文件。现在工具不够好,得一个一个编译.
在我们的android目录下建立jniLibs文件,熟悉安卓的话知道,这是为了放编译好的so文件的
软连接,把lib下面的so文件软连接到第二步建立好的jniLibs文件目录下。当然你直接拷贝过去用也是可以的。看我这个实例你应该明白,你要用绝对路径做symlinks。不可以用相对路径,不然找不到。
然后我们在GreetingsActivity (主Activity)里面加上如下代码
1
2
3static {
System.loadLibrary("greetings");
}强调下必须在
onCreate
method 之前就加。接下来去对应的activity-greetings.xml中随便写点界面,反正能用到刚才定义的方法即可。
然后我们再打开GreetingsActivity写上这个
1
2
3
4
5
6
7
8
9
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了!
文章可以结束了