أنماط Android Rust

تحتوي هذه الصفحة على معلومات عن تسجيل Android، وتقدّم مثالاً على Rust AIDL، وتوضّح كيفية استدعاء Rust من C ، وتقدّم تعليمات حول التوافق بين Rust وC++ باستخدام 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 crate واجهة برمجة تطبيقات لتحديد ما تريد تسجيله. استنادًا إلى ما إذا كان الرمز البرمجي قيد التشغيل على الجهاز أو على المضيف (مثلاً كجزء من اختبار على جانب المضيف)، يتم تسجيل الرسائل باستخدام 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 خارج طريقة 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),
    }
}

استدعاء 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 مع Java من خلال واجهة Java Native Interface ‏ (JNI). ويحدِّد التعريفات اللازمة للأنواع في Rust لإنشاء مكتبة cdylib في Rust يتم توصيلها مباشرةً بواجهة JNI في Java (JNIEnv وJClass JString وما إلى ذلك). على عكس عمليات ربط C++ التي تُجري عملية إنشاء الرموز البرمجية من خلال cxx، لا تتطلّب إمكانية التشغيل التفاعلي لـ Java من خلال واجهة JNI خطوة إنشاء رموز برمجية أثناء عملية الإنشاء. وبالتالي، لا تحتاج إلى دعم خاص لنظام الإنشاء. تحمِّل رمز Java cdylib المقدَّمة من Rust مثل أي مكتبة أصلية أخرى.

الاستخدام

يمكنك الاطّلاع على كيفية استخدامها في كلّ من رمز Rust وJava في ملف jni مستندات حِزم Rust. يُرجى اتّباع مثال البدء المقدَّم هناك. بعد كتابة src/lib.rs، ارجع إلى هذه الصفحة لاطلاع على كيفية إنشاء المكتبة باستخدام نظام إنشاء تطبيقات Android.

تعريف الإصدار

تتطلّب 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++ باستخدام CXX

توفّر حزمة CXX واجهة برمجة تطبيقات خارجية آمنة بين 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);

بينما يحتوي 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;
}