בדף הזה יש מידע על רישום ביומן ב-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 (סקירה כללית של 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()
}
דוגמה ל-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 כמו כל ספריית Native אחרת.
שימוש
השימוש בקוד 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 כדי לאגד את הקוד הזה לספרייה. אם אתם מתכננים להשתמש ב-C++ כדי לקרוא לקוד Rust, או להשתמש בסוגים שמשותפים ל-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;
}