דפוסי חלודה ב-Android

בדף הזה תמצאו מידע על רישום ביומן Android, מספק דוגמה ל-Rust AIDL, שמסביר איך לקרוא ל-Rust מ-C , ומספק הוראות לRust/C++ Interop באמצעות 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!");
}

כלומר, מוסיפים את שתי יחסי התלות שמוצגים למעלה (liblogger ו-liblog_rust), קוראים לשיטה init פעם אחת (אפשר לקרוא לה יותר מפעם אחת במקרה הצורך) ומתעדים הודעות ביומן באמצעות המאקרוסים שסופקו. במסמך הזה מפורטת רשימה של אפשרויות ההגדרה האפשריות.

ה-crate של ה-logger מספק ממשק 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()
}

דוגמה ל-Async Rust AIDL

בקטע הזה מוצגת דוגמה בסגנון 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 מחוץ ל-method 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),
    }
}

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

לבסוף, מריצים את ה-build באמצעות m c_hello_rust.

יכולת פעולה הדדית ב-Rust-Java

הארגז jni מאפשר יכולת פעולה הדדית של Rust עם Java באמצעות ממשק Java Native (JNI). הוא מגדיר את הגדרות הסוגים הנדרשות ל-Rust כדי ליצור ספריית cdylib ב-Rust שמתחבר ישירות ל-JNI של Java‏ (JNIEnv,‏ JClass,‏ JString וכו'). בניגוד לקישורי C++ שמבצעים קודגן באמצעות cxx, יכולת הפעולה ההדדית של Java דרך ה-JNI לא מחייבת שלב של יצירת קוד במהלך ה-build. לכן אין צורך בתמיכה מיוחדת במערכת ה-build. קוד Java טוען את cdylib שסופק על ידי Rust כמו כל ספרייה מקומית אחרת.

שימוש

השימוש בקוד Rust ובקוד Java מוסבר במסמכי התיעוד של jni. יש לפעול לפי הדוגמה שמופיעה בקטע תחילת השימוש. אחרי שכותבים את src/lib.rs, חוזרים לדף הזה כדי ללמוד איך יוצרים את הספרייה באמצעות מערכת ה-build של Android.

הגדרת build

ב-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, למרות שלא מדובר בתלות בזמן build:

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 כדי לארוז את הקוד בספרייה. אם אתם מתכננים לקרוא לקוד Rust מ-C++ או להשתמש בסוגי נתונים שקיימים גם ב-C++ וגם ב-Rust, צריך להגדיר genrule שני (כדי ליצור כותרת 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 משמש למעלה כדי ליצור את הצד C++ של הגשר. הספרייה הסטטית libcxx_test_cpp משמשת בתור תלות בקובץ ההפעלה 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();
}

כדי להשתמש באפשרות הזו מ'חלודה', צריך להגדיר גשר 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;
}