รูปแบบ Rust ของ Android

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

การบันทึก Android

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

ในโมดูล Android.bp ให้เพิ่ม liblogger และ liblog_rust เป็นทรัพยากร Dependency ดังนี้

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 รายการที่แสดงด้านบน (liblogger และ liblog_rust) เรียกใช้เมธอด init หนึ่งครั้ง (คุณเรียกใช้ได้มากกว่า 1 ครั้งหากจำเป็น) และ บันทึกข้อความโดยใช้มาโครที่ระบุ ดูรายการตัวเลือกการกำหนดค่าที่เป็นไปได้ในlogger crate

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

ตัวอย่าง AIDL ของ Rust

ส่วนนี้จะแสดงตัวอย่างการใช้ AIDL กับ Rust ในรูปแบบ "Hello World"

ใช้ส่วนภาพรวม 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);
}

จากนั้นในไฟล์ 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 ที่สร้างขึ้นสามารถใช้เป็นทรัพยากร Dependency โดยโมดูล Rust อื่นๆ ได้ ตัวอย่างการใช้ไลบรารีที่สร้างขึ้นเป็นทรัพยากร Dependency สามารถกำหนด 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 แบบอะซิงโครนัส

ส่วนนี้จะแสดงตัวอย่างการใช้ AIDL กับ Rust แบบไม่พร้อมกันในสไตล์ "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();
    });
}

โปรดทราบว่าต้องใช้ 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

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

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

หรือหากต้องรวมไลบรารี Rust ไว้ในไฟล์ AndroidManifest.xml ให้เพิ่มไลบรารีลงใน uses_libs ดังนี้

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

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

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

หากต้องการให้ CXX สร้างโค้ด C++ ที่ Rust เรียกใช้ ให้กำหนด genrule เพื่อเรียกใช้ CXX และ cc_library_static เพื่อรวมโค้ดนั้นไว้ในไลบรารี หากคุณวางแผน ที่จะให้ C++ เรียกใช้โค้ด Rust หรือใช้ประเภทที่แชร์ระหว่าง C++ กับ Rust ให้กำหนด genrule ที่ 2 (เพื่อสร้างส่วนหัว C++ ที่มี Binding ของ 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เครื่องมือ ใช้ด้านบนเพื่อสร้างฝั่ง C++ ของบริดจ์ จากนั้นจะใช้libcxx_test_cpp ไลบรารีแบบคงที่เป็นทรัพยากร Dependency สำหรับไฟล์ปฏิบัติการ Rust ของเรา

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

ในไฟล์ .cpp และ .hpp ให้กำหนดฟังก์ชัน C++ ตามที่ต้องการ โดยใช้ประเภท Wrapper CXX ตามที่ต้องการ เช่น คำจำกัดความของ 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 Bridge ดังที่แสดงด้านล่างใน 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;
}