402's Blog

Rust on Android

本文为 Medium 文章 Rust on Android 的翻译版本,供喜欢这门技术的开发者阅读使用,请不要用于任何商业用途。原文理解并不难,所以我建议你可以先尝试阅读一下英文原版。

你可能听说过 Rust,它是一门为内存安全和速度而设计的系统级编程语言。由 Mozilla 打造,旨在提供下一代高性能跨平台软件的能力。如果你还没有听过这门语言,我建议先看一下(入门教程)great learning meterial,但请记住,你可能要花一点时间才会对这门语言感兴趣和欣赏它,因此我建议可以多尝试一些而不只是写一个「Hello World」。 如果你是一名 Android 开发者你可能会问_怎样做_和_为什么_在 Android 开发中使用 Rust。这篇文章将尽可能解释_怎样做_。至于_为什么_,对于我们 Visly 来说最重要的原因是它能够在 Android 和 iOS 平台间以一种高性能和安全的方式共享代码,又比C++容易使用得多。

我们也为 iOS 写了类似的文章

准备开始

在我们开始之前我们需要确保我们已经安装了 Rust 工具链。我们将假定你已经安装了 Android 工具链,如果没有你需要下载 Android Studio 并且根据其他的 Android 入门教程配置好 Android 环境。可以通过检查$ANDDROID_HOME变量来确保环境已经安装。如果你使用的是 macOS 的话,这个变量应该被设置为 ~/Library/Android/sdk

下一步将在我们的操作系统上安装 Rust。Rustup让这一些只需要一行代码这么简单。

curl https://sh.rustup.rs -sSf | sh

你可以通过 rustc --version 命令验证 Rust 是否正确安装和位于你的 PATH 路径下。一旦 Rust 在你的操作系统下安装成功我们需要确保 Rust 知道如何构建才能支持安卓架构。Rust 可以编译支持所有架构,但这不是默认行为。运行如下命令添加合适的架构。

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

接下来我们需要配置一些独立的工具链让 Rust 支持 Android 支持的多种架构。这些只需要被安装一次并且每个项目不会配置多次,所以我们将把它们安装到 home 目录下而不是项目目录下。

mkdir ~/.NDK

$(ANDROID_HOME)/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir ~/.NDK/arm64;
$(ANDROID_HOME)/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch arm --install-dir ~/.NDK/arm;
$(ANDROID_HOME)/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch x86 --install-dir ~/.NDK/x86;

最后我们将告诉 Rust 这些工具链的存在。在~/.cargo/config中添加如下内容,如果这个文件不存在就创建一个。

[target.aarch64-linux-android]
ar = ".NDK/arm64/bin/aarch64-linux-android-ar"
linker = ".NDK/arm64/bin/aarch64-linux-android-clang"

[target.armv7-linux-androideabi]
ar = ".NDK/arm/bin/arm-linux-androideabi-ar"
linker = ".NDK/arm/bin/arm-linux-androideabi-clang"

[target.i686-linux-android]
ar = ".NDK/x86/bin/i686-linux-android-ar"
linker = ".NDK/x86/bin/i686-linux-android-clang"

Hello World

我们使用 Rust 编译一个小的 hello world 应用!我们先来创建一个 rust 库,然后继续创建我们的 Android Studio 工程。

mkdir rust-android-example     
cd rust-android-example

cargo new rust --lib
cd rust

如上代码将创建一个基本的由 cargo 管理的 rust 库,cargo 在 rust 的作用类似于 gradle,我们后续将在 Android Studio 工程中使用 gradle。--lib标记告诉 cargo 我们想要创建一个库(Library),而不是一个可执行二进制文件,在我们新创建的工程文件夹中我们可以找到Cargo.toml这个文件,它的作用类似build.gradle文件定义了你的库以及依赖的 metadata 信息。你还会找到src文件夹包含了 rust 源码。这个目录只包含lib.rs文件,文件中只包含一个简单的测试方法。删掉文件中的内容,替换成如下代码:

#![cfg(target_os="android")]     
#![allow(non_snake_case)]

use std::ffi::{CString, CStr};
use jni::JNIEnv;
use jni::objects::{JObject, JString};
use jni::sys::{jstring};

#[no_mangle]
pub unsafe extern fn Java_com_example_android_MainActivity_hello(env: JNIEnv, _: JObject, j_recipient: JString) -> jstring {
    let recipient = CString::from(
        CStr::from_ptr(
            env.get_string(j_recipient).unwrap().as_ptr()
        )
    );

    let output = env.new_string("Hello ".to_owned() + recipient.to_str().unwrap()).unwrap();
    output.into_inner()
}

代码最开始我们通过#[cfg(target_os="android"来告诉 rust 这个文件只在面向 Android 时使用,同时由于 JNI 需要驼峰法命名方法,这在 rust 中不是标准用法,因此需要通过#[allow(non_snake_case)]来允许。还有一些需要注意的是,由于需要和 Kotlin 交流,我们要使用 C 调用约定和 JNI,这意味着我们需要告诉 rust 不要 mangle 任何名字(#[no_mangle)。

下面我们定义了一个基本方法,通过给定字符串构造一个新的字符串。我们需要变换一个 jni 字符串为 C 字符串,在变换为一个 rust 字符串然后返回。Rust 的 jniffi 库让这个过程相当安全,后续我们将链接一些我们在 Visly 中使用的开发范式让这个操作更简单。由于 Kotlin 和 Rust 间的胶水代码可以非常的小,所以在一个更大的应用中,这不是一个问题。

我们同样需要更新Cargo.toml添加jni库的依赖,定义最终二进制的名称和如何编译它。

[dependencies]     
jni = { version = "0.10.2", default-features = false }

[profile.release]
lto = true

[lib]
name = "rust"
crate-type = ["cdylib"]

在我们开始 Android Studio 工程前的最后一件事情是编译二进制包。

cargo build --target aarch64-linux-android --release     
cargo build --target armv7-linux-androideabi --release     
cargo build --target i686-linux-android --release

Android Studio

到了开始一个新的 Android Studio 工程,在模拟器中测试它的时候了。先去标准工程配置中,我们将使用 Kotlin ,当然你也可以使用 Java。我们将工程命名为 android 保存项目到 rust-android-example根目录中。

打开MainActivity.kt,粘贴如下代码。我们声明一个外部方法 hello 告诉 Android 寻找一个原生库方法叫做 Java_example_com_android_MainActivity_hello。在我们调用这个方法之前我们需要使用System.loadLibrary加载我们的库。

package com.example.android

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        System.loadLibrary("rust")
        Log.d("rust", hello("World"))
    }

    external fun hello(to: String): String
}

如果这个时候尝试编译应用,会在启动时发生崩溃。因为我们没有包含原生库到工程中。使用如下代码拷贝进来。

cd rust-android-example

mkdir android/app/src/main/jniLibs
mkdir android/app/src/main/jniLibs/arm64-v8a
mkdir android/app/src/main/jniLibs/armeabi-v7a
mkdir android/app/src/main/jniLibs/x86

cp rust/target/aarch64-linux-android/release/librust.so android/app/src/main/jniLibs/arm64-v8a/librust.so
cp rust/target/armv7-linux-androideabi/release/librust.so android/app/src/main/jniLibs/armeabi-v7a/librust.so
cp rust/target/i686-linux-android/release/librust.so android/app/src/main/jniLibs/x86/librust.so

现在我们可以再次编译和运行我们的应用,我们将看到「Hello World」在 Logcat 中输出。恭喜你!你已经成功的配置和运行 Rust 到 Android。

Automating the process

自动化拷贝二进制包这个过程可以由下面这个 bash 脚本轻松的办到。

 #!/bin/sh     
JNI_LIBS=../android/app/src/main/jniLibs

cd rust
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

rm -rf $JNI_LIBS
mkdir $JNI_LIBS
mkdir $JNI_LIBS/arm64-v8a
mkdir $JNI_LIBS/armeabi-v7a
mkdir $JNI_LIBS/x86

cp target/aarch64-linux-android/release/librust.so $JNI_LIBS/arm64-v8a/librust.so
cp target/armv7-linux-androideabi/release/librust.so $JNI_LIBS/armeabi-v7a/librust.so
cp target/i686-linux-android/release/librust.so $JNI_LIBS/x86/librust.so\

保存上述代码到 rust-android-example/install.sh 并在任何一次更新了 rust 代码后运行它,编译和安装它到你的 Android Studio 工程中。如果你想 get fancy 你可以在 gradle 文件中将其添加为编译步骤,每次编译 Android Studio 工程前它都会被执行。

下一步

当上述代码工作时,并不是很容易掌握。在更大的工程中我们希望压缩那些用于 Kotlin 和 Rust 之间通信的丑陋二进制数字。在接下来的 post 中我将讲一下我们在构建 Visly 时用到的开发范式来简化这个工作。 在 Github 上可以找到本教程的实力代码,如果你计划在 Android 开发中使用 Rust,这将是一个很好的起点。如果你有任何问题可以给我发推

评论