Android の Rust パターン

このページでは、Android のロギングに関する情報、Rust AIDL の例C から Rust を呼び出す方法、CXX を使用した Rust/C++ の相互運用の手順について説明します。

Android のロギング

次の例は、メッセージを logcat(デバイス上)または stdout(ホスト上)に記録する方法を示しています。

Android.bp モジュールで、libloggerliblog_rust を依存関係として追加します。

rust_binary {
    name: "logging_test",
    srcs: ["src/main.rs"],
    rustlibs: [
        "liblogger",
        "liblog_rust",
    ],
}

次に、Rust ソースに以下のコードを追加します。

use log::{debug, error, LevelFilter};

fn main() {
    let _init_success = logger::init(
        logger::Config::default()
            .with_tag_on_device("mytag")
            .with_max_level(LevelFilter::Trace),
    );
    debug!("This is a debug message.");
    error!("Something went wrong!");
}

具体的には、上記の 2 つの依存関係(libloggerliblog_rust)を追加し、init メソッドを 1 回呼び出し(必要に応じて複数回呼び出せます)、提供されているマクロを使用してメッセージをログに記録します。使用可能な構成オプションの一覧については、ロガークレートをご覧ください。

ロガークレートには、記録する対象を定義する API が用意されています。コードがデバイス上で実行されているか、ホスト上(ホスト側のテストの一部など)で実行されているかに応じて、android_logger または env_logger のいずれかを使用してログに記録されます。

Rust AIDL の例

このセクションでは、Rust で AIDL を使用する Hello World スタイルの例を紹介します。

Android デベロッパー ガイドの AIDL の概要を参考に、external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl を作成し、IRemoteService.aidl ファイルに以下の内容を記述します。

// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

次に、external/rust/binder_example/aidl/Android.bp ファイル内で、aidl_interface モジュールを定義します。Rust バックエンドはデフォルトでは有効になっていないため、明示的に有効にする必要があります。

aidl_interface {
    name: "com.example.android.remoteservice",
    srcs: [ "aidl/com/example/android/*.aidl", ],
    unstable: true, // Add during development until the interface is stabilized.
    backend: {
        rust: {
            // By default, the Rust backend is not enabled
            enabled: true,
        },
    },
}

AIDL バックエンドは Rust ソース生成ツールであるため、他の Rust ソース生成ツールと同様に動作し、Rust ライブラリを生成します。生成された Rust ライブラリ モジュールは、他の Rust モジュールで依存関係として使用できます。生成されたライブラリを依存関係として使用する例として、external/rust/binder_example/Android.bprust_library を次のように定義できます。

rust_library {
    name: "libmyservice",
    srcs: ["src/lib.rs"],
    crate_name: "myservice",
    rustlibs: [
        "com.example.android.remoteservice-rust",
        "libbinder_rs",
    ],
}

rustlibs 内で使用する AIDL が生成したライブラリのモジュール名の形式は、aidl_interface モジュール名の後に -rust が続く形式です。ここでは com.example.android.remoteservice-rust です。

AIDL インターフェースは src/lib.rs で次のように参照できます。

// Note carefully the AIDL crates structure:
// * the AIDL module name: "com_example_android_remoteservice"
// * next "::aidl"
// * next the AIDL package name "::com::example::android"
// * the interface: "::IRemoteService"
// * finally, the 'BnRemoteService' and 'IRemoteService' submodules

//! This module implements the IRemoteService AIDL interface
use com_example_android_remoteservice::aidl::com::example::android::{
  IRemoteService::{BnRemoteService, IRemoteService}
};
use binder::{
    BinderFeatures, Interface, Result as BinderResult, Strong,
};

/// This struct is defined to implement IRemoteService AIDL interface.
pub struct MyService;

impl Interface for MyService {}

impl IRemoteService for MyService {
    fn getPid(&self) -> BinderResult<i32> {
        Ok(42)
    }

    fn basicTypes(&self, _: i32, _: i64, _: bool, _: f32, _: f64, _: &str) -> BinderResult<()> {
        // Do something interesting...
        Ok(())
    }
}

最後に、次のように Rust バイナリでサービスを開始します。

use myservice::MyService;

fn main() {
    // [...]
    let my_service = MyService;
    let my_service_binder = BnRemoteService::new_binder(
        my_service,
        BinderFeatures::default(),
    );
    binder::add_service("myservice", my_service_binder.as_binder())
        .expect("Failed to register service?");
    // Does not return - spawn or perform any work you mean to do before this call.
    binder::ProcessState::join_thread_pool()
}

非同期 Rust AIDL の例

このセクションでは、非同期 Rust で AIDL を使用する Hello World スタイルの例を紹介します。

引き続き RemoteService の例では、生成された AIDL バックエンド ライブラリに非同期インターフェースが含まれ、これを AIDL インターフェース RemoteService の非同期サーバー実装に使用できます。

生成された非同期サーバー インターフェースの IRemoteServiceAsyncServer は次のように実装できます。

use com_example_android_remoteservice::aidl::com::example::android::IRemoteService::{
    BnRemoteService, IRemoteServiceAsyncServer,
};
use binder::{BinderFeatures, Interface, Result as BinderResult};

/// This struct is defined to implement IRemoteServiceAsyncServer AIDL interface.
pub struct MyAsyncService;

impl Interface for MyAsyncService {}

#[async_trait]
impl IRemoteServiceAsyncServer for MyAsyncService {
    async fn getPid(&self) -> BinderResult<i32> {
        //Do something interesting...
        Ok(42)
    }

    async fn basicTypes(&self, _: i32, _: i64, _: bool, _: f32, _: f64,_: &str,) -> BinderResult<()> {
        //Do something interesting...
        Ok(())
    }
}

非同期サーバーは次のように実装できます。

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
    binder::ProcessState::start_thread_pool();

    let my_service = MyAsyncService;
    let my_service_binder = BnRemoteService::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder.as_binder())
        .expect("Failed to register service?");

    task::block_in_place(move || {
        binder::ProcessState::join_thread_pool();
    });
}

なお、join_thread_poolblock_on を内部的に使用できるようにする非同期コンテキストから離れるには、block_in_place が必要になります。これは、block_on に対する呼び出し内のコードを #[tokio::main] でラップすることになり、受信したトランザクションの処理時に join_thread_poolblock_on を呼び出す可能性があるためです。block_on 内から block_on を呼び出すと予期せぬエラーが発生します。このような事態は、#[tokio::main] を使用して block_on メソッドの外で join_thread_pool を呼び出す代わりに、tokio ランタイムを手動でビルドすることで防ぐこともできます。

さらに Rust バックエンド生成ライブラリには、RemoteService の非同期クライアント IRemoteServiceAsync を実装できるインターフェースが含まれます。次のように実装します。

use com_example_android_remoteservice::aidl::com::example::android::IRemoteService::IRemoteServiceAsync;
use binder_tokio::Tokio;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let binder_service = binder_tokio::wait_for_interface::<dyn IRemoteServiceAsync<Tokio>>("myservice");

    let my_client = binder_service.await.expect("Cannot find Remote Service");

    let result = my_client.getPid().await;

    match result {
        Err(err) => panic!("Cannot get the process id from Remote Service {:?}", err),
        Ok(p_id) => println!("PID = {}", p_id),
    }
}

C から Rust を呼び出す

次の例は、C から Rust を呼び出す方法を示しています。

Rust ライブラリの例

external/rust/simple_printer/libsimple_printer.rslibsimple_printer ファイルを次のように定義します。

//! A simple hello world example that can be called from C

#[no_mangle]
/// Print "Hello Rust!"
pub extern fn print_c_hello_rust() {
    println!("Hello Rust!");
}

依存する C モジュールが取得できるヘッダーを Rust ライブラリで定義する必要があるため、次のように external/rust/simple_printer/simple_printer.h ヘッダーを定義します。

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

次のように external/rust/simple_printer/Android.bp を定義します。

rust_ffi {
    name: "libsimple_c_printer",
    crate_name: "simple_c_printer",
    srcs: ["libsimple_c_printer.rs"],

    // Define export_include_dirs so cc_binary knows where the headers are.
    export_include_dirs: ["."],
}

C バイナリの例

external/rust/c_hello_rust/main.c を次のように定義します。

#include "simple_printer.h"

int main() {
  print_c_hello_rust();
  return 0;
}

external/rust/c_hello_rust/Android.bp を次のように定義します。

cc_binary {
    name: "c_hello_rust",
    srcs: ["main.c"],
    shared_libs: ["libsimple_c_printer"],
}

最後に、m c_hello_rust を呼び出してビルドします。

Rust と Java の相互運用

jni クレートにより、Java Native Interface(JNI)を介した Java と Rust の相互運用が可能となります。これで、Java の JNI(JNIEnvJClassJString など)に直接接続する Rust cdylib ライブラリを生成するために必要な Rust の型定義が定義されます。cxx を介して codegen を実行する C++ バインディングとは異なり、JNI を介した Java の相互運用の場合、ビルド中のコード生成の手順は必要ありません。そのため、特別なビルドシステムのサポートは不要です。Java コードは、他のネイティブ ライブラリと同様に、Rust が提供する cdylib を読み込みます。

使用方法

Rust と Java の両方のコードの使用方法は、jni クレートのドキュメントに記載されています。提供されている開始時の例に従ってください。src/lib.rs を作成したら、このページに戻って Android のビルドシステムでライブラリをビルドする方法を確認してください。

ビルド定義

Java では、動的に読み込めるように、Rust ライブラリを cdylib として指定する必要があります。Soong の Rust ライブラリの定義は次のとおりです。

rust_ffi_shared {
    name: "libhello_jni",
    crate_name: "hello_jni",
    srcs: ["src/lib.rs"],

    // The jni crate is required
    rustlibs: ["libjni"],
}

Java ライブラリは、Rust ライブラリを required 依存関係としてリストします。これにより、ビルド時の依存関係でなくとも、Java ライブラリとともにデバイスにインストールされます。

java_library {
        name: "libhelloworld",
        [...]
        required: ["libhellorust"]
        [...]
}

または、Rust ライブラリを AndroidManifest.xml ファイルに含める必要がある場合は、次のようにライブラリを uses_libs に追加します。

java_library {
        name: "libhelloworld",
        [...]
        uses_libs: ["libhellorust"]
        [...]
}

CXX を使用した Rust と C++ の相互運用

CXX クレートは、Rust と C++ サブセット間の安全な FFI を提供します。一般的な仕組みの例については、CXX のドキュメントをご覧ください。このドキュメントを最初にご覧いただければ、このライブラリ自体と、ライブラリを利用して C++ と Rust を連携する方法について理解しやすくなります。次の例は、Android での使用方法を示しています。

Rust が呼び出す C++ コードを CXX に生成させるには、CXX を呼び出す genrule と、それをライブラリにバンドルする cc_library_static を定義します。C++ で Rust コードを呼び出す場合、または C++ と Rust で共有している型を使用する場合は、(Rust バインディングを含む C++ ヘッダーを生成するために)2 つ目の genrule を定義します。

cc_library_static {
    name: "libcxx_test_cpp",
    srcs: ["cxx_test.cpp"],
    generated_headers: [
        "cxx-bridge-header",
        "libcxx_test_bridge_header"
    ],
    generated_sources: ["libcxx_test_bridge_code"],
}

// Generate the C++ code that Rust calls into.
genrule {
    name: "libcxx_test_bridge_code",
    tools: ["cxxbridge"],
    cmd: "$(location cxxbridge) $(in) > $(out)",
    srcs: ["lib.rs"],
    out: ["libcxx_test_cxx_generated.cc"],
}

// Generate a C++ header containing the C++ bindings
// to the Rust exported functions in lib.rs.
genrule {
    name: "libcxx_test_bridge_header",
    tools: ["cxxbridge"],
    cmd: "$(location cxxbridge) $(in) --header > $(out)",
    srcs: ["lib.rs"],
    out: ["lib.rs.h"],
}

上記では、ブリッジの C++ 側を生成するために cxxbridge ツールが使用されています。次に Rust の実行可能ファイルの依存関係として libcxx_test_cpp 静的ライブラリが使用されています。

rust_binary {
    name: "cxx_test",
    srcs: ["lib.rs"],
    rustlibs: ["libcxx"],
    static_libs: ["libcxx_test_cpp"],
}

.cpp ファイルと .hpp ファイルで、必要に応じて CXX ラッパータイプを使用して、C++ 関数を定義します。 たとえば、cxx_test.hpp 定義には次のものが含まれます。

#pragma once

#include "rust/cxx.h"
#include "lib.rs.h"

int greet(rust::Str greetee);

cxx_test.cpp には以下が含まれます。

#include "cxx_test.hpp"
#include "lib.rs.h"

#include <iostream>

int greet(rust::Str greetee) {
  std::cout << "Hello, " << greetee << std::endl;
  return get_num();
}

Rust からこれを使用するには、lib.rs で次のように CXX ブリッジを定義します。

#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("cxx_test.hpp");
        fn greet(greetee: &str) -> i32;
    }
    extern "Rust" {
        fn get_num() -> i32;
    }
}

fn main() {
    let result = ffi::greet("world");
    println!("C++ returned {}", result);
}

fn get_num() -> i32 {
    return 42;
}