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

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

דוגמה ל-Rust AIDL

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

משתמשים בקטע AIDL Overview במדריך למפתחי 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, ספריית ה-Backend של 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.

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

שימוש

השימוש בקוד Rust ובקוד Java מוסבר במסמכי ה-crate‏ jni. כדאי לעיין בדוגמה שמופיעה במאמר תחילת העבודה עם Service Management API. אחרי שכותבים 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, גם אם היא לא תלות בזמן בנייה:

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

לחלופין, אם אתם חייבים לכלול את ספריית Rust בקובץ AndroidManifest.xml כלשהו, תוכלו להוסיף את הספרייה ל-uses_libs באופן הבא:

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

‫Rust–C++ interop using CXX

‫Crate‏ 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++‎ לפי הצורך, באמצעות סוגי העטיפה של CXX. לדוגמה, הגדרה של cxx_test.hpp מכילה את הפרטים הבאים:

#pragma once

#include "rust/cxx.h"
#include "lib.rs.h"

int greet(rust::Str greetee);

‫While cxx_test.cppcontains

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