รูปแบบ Rust ของ Android

หน้านี้มีข้อมูลเกี่ยวกับการบันทึกของ Android, ตัวอย่าง Rust AIDL, วิธีเรียกใช้ Rust จาก C และวิธีการการทำงานร่วมกันของ Rust/C++ โดยใช้ CXX

การบันทึกของ Android

ตัวอย่างต่อไปนี้แสดงวิธีบันทึกข้อความไปยัง logcat (ในอุปกรณ์) หรือ stdout (ในโฮสต์)

ในโมดูล Android.bp ให้เพิ่ม liblogger และ liblog_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!");
}

กล่าวคือ ให้เพิ่ม Dependency 2 รายการที่แสดงด้านบน (liblogger และ liblog_rust) เรียกใช้เมธอด init 1 ครั้ง (เรียกใช้มากกว่า 1 ครั้งได้หากจำเป็น) และบันทึกข้อความโดยใช้มาโครที่ระบุ ดูรายการตัวเลือกการกำหนดค่าที่เป็นไปได้ใน logger crate

แพ็กเกจบันทึกมี API สำหรับกำหนดสิ่งที่คุณต้องการบันทึก ระบบจะบันทึกข้อความโดยใช้ android_logger หรือ env_logger ทั้งนี้ขึ้นอยู่กับว่าโค้ดทํางานในอุปกรณ์หรือในโฮสต์ (เช่น เป็นส่วนหนึ่งของการทดสอบฝั่งโฮสต์)

ตัวอย่าง Rust AIDL

ส่วนนี้จะแสดงตัวอย่างสไตล์ Hello World ของการใช้ AIDL กับ Rust

ใช้ส่วนภาพรวม AIDL ในคู่มือนักพัฒนาแอป Android เป็นจุดเริ่มต้น สร้าง 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);
}

จากนั้นให้กําหนดโมดูล aidl_interface ในไฟล์ external/rust/binder_example/aidl/Android.bp คุณต้องเปิดใช้แบ็กเอนด์ 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 อื่นๆ ได้ ตัวอย่างการใช้ไลบรารีที่สร้างขึ้นเป็นข้อกําหนดคือ คุณสามารถกําหนด rust_library ดังนี้ใน external/rust/binder_example/Android.bp

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

โปรดทราบว่ารูปแบบชื่อโมดูลสำหรับไลบรารีที่ AIDL สร้างขึ้นซึ่งใช้ใน rustlibs คือชื่อโมดูล 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()
}

ตัวอย่าง AIDL ของ Rust แบบอะซิงโครนัส

ส่วนนี้จะแสดงตัวอย่างสไตล์ Hello World ของการใช้ AIDL กับ Rust แบบแอ็กซิงโครน

จากตัวอย่าง 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();
    });
}

โปรดทราบว่าต้องใช้ block_in_place เพื่อออกจากบริบทแบบแอสซิงค์ ซึ่งจะช่วยให้ join_thread_pool ใช้ block_on ภายในได้ เนื่องจาก #[tokio::main] ตัดโค้ดในคําเรียก block_on และ join_thread_pool อาจเรียก block_on เมื่อจัดการธุรกรรมขาเข้า การโทรหา block_on จากภายใน block_on จะทำให้เกิดอาการตื่นตระหนก นอกจากนี้ คุณยังหลีกเลี่ยงปัญหานี้ได้ด้วยการคอมไพล์รันไทม์ tokioด้วยตนเองแทนที่จะใช้ #[tokio::main] แล้วเรียกใช้ join_thread_pool นอกเมธอด block_on

นอกจากนี้ ไลบรารีแบ็กเอนด์ Rust ที่สร้างขึ้นยังมีอินเทอร์เฟซที่อนุญาตให้ใช้ไคลเอ็นต์แบบไม่พร้อมกัน IRemoteServiceAsync สำหรับ RemoteService ซึ่งสามารถใช้งานได้ดังนี้

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),
    }
}

การเรียกใช้ Rust จาก C

ตัวอย่างนี้แสดงวิธีเรียกใช้ Rust จาก C

ตัวอย่างไลบรารี Rust

กำหนดไฟล์ libsimple_printer ในexternal/rust/simple_printer/libsimple_printer.rs ดังนี้

//! 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!");
}

ไลบรารี Rust ต้องกำหนดส่วนหัวที่โมดูล C ที่ขึ้นต่อกันสามารถดึงข้อมูลได้ ดังนั้นให้กำหนดส่วนหัว 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 ช่วยให้ Rust ทำงานร่วมกับ Java ได้ผ่าน Java Native Interface (JNI) โดยจะกำหนดคำจำกัดความประเภทที่จำเป็นสำหรับ Rust เพื่อสร้างcdylibคลัง Rust ที่เสียบเข้ากับ JNI ของ Java ได้โดยตรง (JNIEnv, JClass, JString และอื่นๆ) การทำงานร่วมกันของ Java ผ่าน JNI ไม่จำเป็นต้องมีขั้นตอนการสร้างโค้ดระหว่างการบิลด์ ซึ่งแตกต่างจากการเชื่อมโยง C++ ที่ใช้ codegen ผ่าน cxx จึงไม่จำเป็นต้องมีการสนับสนุนระบบการสร้างพิเศษ โค้ด Java จะโหลด cdylib ที่ Rust มีให้ เช่นเดียวกับไลบรารีแบบเนทีฟอื่นๆ

การใช้งาน

การใช้งานทั้งในโค้ด Rust และ Java อยู่ในjniเอกสารประกอบของ Crate โปรดทำตามตัวอย่างการเริ่มต้นใช้งานที่ระบุไว้ หลังจากเขียน src/lib.rs แล้ว ให้กลับไปที่หน้านี้เพื่อดูวิธีสร้างไลบรารีด้วยระบบบิลด์ของ Android

คําจํากัดความของบิลด์

Java กำหนดให้ต้องระบุไลบรารี Rust เป็น cdylib เพื่อให้โหลดแบบไดนามิกได้ คําจํากัดความของไลบรารี Rust ใน Soong มีดังนี้

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"]
        [...]
}

การทํางานร่วมกันระหว่าง Rust กับ C++ โดยใช้ CXX

แพ็กเกจ CXX มี FFI ที่ปลอดภัยระหว่าง Rust กับ C++ ชุดย่อย เอกสารประกอบ CXX มีตัวอย่างที่ดีเกี่ยวกับวิธีการทำงานของแพ็กเกจนี้โดยทั่วไป เราขอแนะนำให้อ่านก่อนเพื่อทำความคุ้นเคยกับไลบรารีและวิธีที่เป็นบริดจ์ระหว่าง C++ กับ Rust ตัวอย่างต่อไปนี้แสดงวิธีใช้ใน Android

หากต้องการให้ CXX สร้างโค้ด C++ ที่ Rust เรียกใช้ ให้กำหนด genrule เพื่อเรียกใช้ CXX และ cc_library_static เพื่อรวมโค้ดดังกล่าวไว้ในไลบรารี หากคุณวางแผนที่จะให้ C++ เรียกใช้โค้ด Rust หรือใช้ประเภทที่แชร์ระหว่าง C++ กับ Rust ให้กำหนด genrule รายการที่ 2 (เพื่อสร้างส่วนหัว C++ ที่มีการเชื่อมโยง Rust)

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"],
}

cxxbridge tool ใช้ด้านบนเพื่อสร้างบริดจ์ฝั่ง C++ ระบบจะใช้libcxx_test_cpp ไลบรารีแบบคงที่ถัดไปเป็นข้อกำหนดเบื้องต้นสำหรับไฟล์ปฏิบัติการ Rust ดังนี้

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

ในไฟล์ .cpp และ .hpp ให้กําหนดฟังก์ชัน C++ ตามที่คุณต้องการ ใช้ประเภท CXX wrapper ตามที่คุณต้องการ เช่น คําจํากัดความ 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 ให้กําหนดบริดจ์ CXX ดังด้านล่างใน lib.rs

#[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;
}