見出し画像

Android NDK で Rust は使えるのか?調査してみた

こんにちは、けんにぃです。

ナビタイムジャパンで時刻表のサービス開発・DevOps・機械学習の研究開発などを担当しています。

今回は C++ の代替として注目を浴びている Rust を使って Android NDK を実装することはできるのか?について調査したことをまとめようと思います。

C++ はつらいよ

最近 Android NDK の開発をしていて後輩から「C++ はつらいです」と言われました。確かに C++ は難易度の高い言語なので、この気持は良く分かります。Android NDK の開発に限らず、サーバサイドでも同じようなことをつぶやく後輩はとても多いです。

しかし最近だと C++ の代わりに Rust を使う事例を少しづつ見るようになってきました。Android NDK でも Rust が使えると便利そうだなぁと思ったので、使えるのかどうか調査してみました。

Android NDK で Rust を使う方法

下記の環境で動作確認を行いました。

・macOS
・Android Studio 4.1.2
・Rust 1.50.0

Android NDK に Rust を取り込むには Mozilla が開発している Rust Android Gradle Plugin を使用します。

Android Studio を起動し、新規プロジェクト作成で Native C++ を選択してプロジェクトを作成します。プロジェクト名は本稿では HelloWorld にします。作成すると下記のような C++ コードが生成されるので、これを Rust に移植してみようと思います。

HelloWorld/app/src/main/cpp/native-lib.cpp

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_helloworld2_MainActivity_stringFromJNI(
       JNIEnv* env,
       jobject /* this */) {
   std::string hello = "Hello from C++";
   return env->NewStringUTF(hello.c_str());
}

Rust コードを保存するディレクトリを作成し、hello というプロジェクトを作成します。

$ cd HelloWorld/app/src/main
$ mkdir rust
$ cd rust
$ cargo new --lib hello

HelloWorld/build.gradle に下記の設定を追加します。

buildscript {
   repositories {
       maven {
           url "https://plugins.gradle.org/m2/"
       }
   }
   dependencies {
       classpath 'gradle.plugin.org.mozilla.rust-android-gradle:plugin:0.8.3'
   }
}

HelloWorld/app/build.gradle に下記の設定を追加します。

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

cargo {
   // Rust モジュールのパス
   module  = "src/main/rust/hello"

   // Rust モジュールの名称
   libname = "hello"

   // ビルド対象のアーキテクチャ
   targets = ["arm", "arm64", "x86", "x86_64"]
}

// ビルド時に Rust コードもビルドする
tasks.whenTaskAdded { task ->
   if (task.name == "javaPreCompileDebug" || task.name == "javaPreCompileRelease") {
       task.dependsOn "cargoBuild"
   }
}

これでビルド時に Rust コードのビルドが同時に走るようになります。

次に Android 向けの Rust ツールチェインをインストールします。

$ rustup target add aarch64-linux-android     # arm64-v8a
$ rustup target add armv7-linux-androideabi   # armeabi-v7a
$ rustup target add i686-linux-android        # x86
$ rustup target add x86_64-linux-android      # x86_64

HelloWorld/app/src/main/rust/hello/Cargo.toml に下記の設定を追加します。

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

[dependencies]
jni = "0.19.0"

Android NDK は JNI を使用するので JNI の Rust ラッパである jni を依存ライブラリとして使用します。

HelloWorld/app/src/main/rust/hello/src/lib.rs に下記のようなコードを書きます。

use jni::objects::JObject;
use jni::sys::jstring;
use jni::JNIEnv;

#[no_mangle]
pub unsafe extern "C" fn Java_com_example_helloworld_MainActivity_stringFromJNI(
   env: JNIEnv,
   _this: JObject,
) -> jstring {
   let hello = "Hello from Rust";

   env.new_string(hello)
       .expect("Couldn't create Java string!")
       .into_inner()
}

JNI 経由で関数を呼ぶためには C 言語形式の関数を作る必要がありますが Rust は下記のキーワードを付けることで関数を C 言語と互換性のある形式でコンパイルすることができます。

#[no_mangle]
extern "C" fn

詳細は下記のページを御覧ください。

ビルドをすると HelloWorld/app/build/rustJniLibs 配下に libhello.so が作成されます。ここまでできたら後は C++ を使う場合と同様にJava もしくは Kotlin のコードから libhello.so を呼び出せば関数が呼び出せます。

HelloWorld/app/src/main/java/com/example/helloworld/MainActivity.kt

package com.example.helloworld

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView

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

       findViewById<TextView>(R.id.sample_text).text = stringFromJNI()
   }

   // Rust で定義した関数を宣言
   external fun stringFromJNI(): String

   companion object {
       init {
           // ここで libhello.so を呼び出す
           System.loadLibrary("hello")
       }
   }
}

これで完成です。ビルドして実行するとメッセージが表示されます。

スクリーンショット 2021-03-05 21.17.32

注意点

以上のように簡単なプログラムは Rust でも書けそうです。しかし Android / iOS のサポート状況は現在のところ Tier 2(ビルドは通る)というステータスになっているため、現時点でプロダクトに Rust を組み込むのはそれなりにチャレンジングな試みとなります。

各 OS のサポート状況

Rust は C++ のつらみを解消するのか?

今後 C++ の代わりに Rust を使うことで開発効率は大幅に改善するだろうと思っています。

しかし、その一方で C++ の開発がつらいと感じているエンジニアたちが Rust を使ったらつらみが解消されるのか?という問いについては考察の余地があると思っています。

なぜかというと「C++ がつらい」と言ってもその理由にはいろんな理由があるからです。C++ 初学者がよくつまづく点にはだいたい下記のようなものがあります。

・ポインタがわからない
・ヒープとスタックの違いがわからない
・ディープコピーとシャローコピーの違いが理解できていない
・クラッシュしたときのデバッグが難しい
・仕様が複雑

ポインタやヒープなどのメモリ管理の概念はそもそも C++ 特有のものではなく、他の言語を扱う場合でも知っておいたほうが良い知識です。実際 Go でもポインタの概念が出てきます。この辺に精通してないと Rust を使うときにもハマります。メモリ管理の仕組みを知らないのは C++ の問題ではなくエンジニアの知識の問題です。

C++ の本当の問題点は不正なポインタが参照できてしまったり、固定長配列のインデックスが不正でもコンパイルできてしまうといったメモリ系のバグに弱いという点だと思います。メモリ系のバグはたとえメモリ管理の仕組みを理解していてもケアレスミスとして発生しうる事象です。この点でいえば Rust は強力な言語になると思います。

さいごに

Rust は学習コストが高い言語だと言われますが C++ を知っている(メモリ管理の仕組みを知っている)とそれなりに低コストで学べる言語ではないかと思います。いつの日か Android NDK でも Rust が安定動作をするようになるといいですね。