דפוסי חלודה ב-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",
    ],
}

בשלב הבא, במקור חלודה, מוסיפים את הקוד הבא:

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), קוראים ל-method init פעם אחת (אפשר לקרוא לה יותר מפעם אחת במקרה הצורך), וגם לשלוח הודעות ביומן באמצעות הוראות המאקרו שסופקו. לצפייה כלב יומן כדי לראות רשימה של אפשרויות הגדרה אפשריות.

כרטיס היומן מספק ממשק API להגדרת מה שרוצים לרשום. בהתאם ל: האם הקוד פועל במכשיר או במארח (למשל, בדיקה בצד המארח), ההודעות נרשמות ביומן באמצעות android_logger או env_logger.

דוגמה ל-Rust AIDL

בקטע הזה מוצגת דוגמה בסגנון Hello World לשימוש ב-AIDL עם Rust.

שימוש במדריך למפתחים של 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_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::main] ואז להתקשר אל join_thread_pool מחוץ ל-method block_on.

בנוסף, הספרייה שנוצרה לקצה העורפי של חלודה כוללת ממשק שמאפשר הטמעה של לקוח אסינכרוני 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.

ספריית חלודה לדוגמה

הגדרת הקובץ 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!");
}

ספריית חלודה חייבת להגדיר כותרות שמודולים של 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 ממשק (JNI). הוא מגדיר את הגדרות הסוגים הנחוצות כדי שחלודה תייצר ספריית cdylib Rust שמתחברת ישירות ל-JNI של Java (JNIEnv, JClass, JString וכן הלאה). בניגוד לקישורי C++ שמבצעים יצירת קוד דרך cxx, יכולת הפעולה ההדדית של Java דרך ה-JNI לא מחייבת שלב של יצירת קוד במהלך ה-build. לכן אין צורך בתמיכה מיוחדת במערכת build. ה-Java טוען את cdylib שסופקה על ידי חלודה כמו כל ספריית נייטיב אחרת.

שימוש

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

הגדרת ה-build

ב-Java נדרש שספריית Rust תהיה זמינה כ-cdylib, נטענות באופן דינמי. ההגדרה של ספריית חלודה בסונג היא:

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, הגדירו סוג שני (כדי ליצור כותרת C++ שמכילה את קישורי חלודה).

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