รูปแบบ 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 ของโปรแกรมบันทึกมี 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;
}