الگوهای Android Rust

این صفحه حاوی اطلاعاتی در مورد ثبت وقایع اندروید (Android Logging) است، یک مثال از Rust AIDL ارائه می‌دهد، نحوه فراخوانی Rust از زبان C را به شما می‌گوید و دستورالعمل‌هایی برای تعامل Rust/C++ با استفاده از CXX ارائه می‌دهد.

ثبت وقایع اندروید

مثال زیر نشان می‌دهد که چگونه می‌توانید پیام‌ها را در 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 crate) یک API برای تعریف آنچه می‌خواهید ثبت کنید، ارائه می‌دهد. بسته به اینکه کد روی دستگاه یا روی میزبان اجرا می‌شود (مانند بخشی از یک تست سمت میزبان)، پیام‌ها با استفاده از android_logger یا env_logger ثبت می‌شوند.

مثال Rust AIDL

این بخش یک مثال به سبک Hello World از استفاده از AIDL با Rust ارائه می‌دهد.

با استفاده از بخش مرور کلی 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 تولید می‌کند. ماژول کتابخانه 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 ، کتابخانه‌ی بک‌اند 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(())
    }
}

پیاده‌سازی سرور async می‌تواند به صورت زیر آغاز شود:

#[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 منجر به ایجاد یک خطای panic می‌شود. همچنین می‌توان با ساخت دستی زمان اجرای 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

جعبه jni قابلیت همکاری Rust با جاوا را از طریق رابط بومی جاوا (JNI) فراهم می‌کند. این جعبه تعاریف نوع لازم برای Rust را برای تولید یک کتابخانه Rust cdylib که مستقیماً به JNI جاوا ( JNIEnv ، JClass ، JString و غیره) متصل می‌شود، تعریف می‌کند. برخلاف پیوندهای C++ که از طریق cxx کدنویسی را انجام می‌دهند، قابلیت همکاری جاوا از طریق JNI نیازی به مرحله تولید کد در طول ساخت ندارد. بنابراین به پشتیبانی ویژه سیستم ساخت نیاز ندارد. کد جاوا cdylib ارائه شده توسط Rust را مانند هر کتابخانه بومی دیگر بارگذاری می‌کند.

کاربرد

نحوه‌ی استفاده از آن در کدهای Rust و Java در مستندات jni crate توضیح داده شده است. لطفاً مثال شروع به کار ارائه شده در آنجا را دنبال کنید. پس از نوشتن src/lib.rs ، برای یادگیری نحوه‌ی ساخت کتابخانه با سیستم ساخت اندروید، به این صفحه بازگردید.

تعریف ساخت

جاوا نیاز دارد که کتابخانه 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"],
}

کتابخانه جاوا، کتابخانه Rust را به عنوان یک وابستگی required فهرست می‌کند؛ این امر تضمین می‌کند که این کتابخانه در کنار کتابخانه جاوا، حتی اگر یک وابستگی زمان ساخت نباشد، روی دستگاه نصب می‌شود:

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 آشنا شوید. مثال زیر نحوه استفاده از آن را در اندروید نشان می‌دهد.

برای اینکه 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);

در حالی که 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 به شکل زیر در 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;
}