Android Rust के पैटर्न

इस पेज पर, Android Logging के बारे में जानकारी दी गई है. साथ ही, Rust AIDL का उदाहरण दिया गया है. इसमें C से Rust को कॉल करने का तरीका बताया गया है. साथ ही, 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 तरीके को एक बार कॉल करें (ज़रूरी होने पर, इसे एक से ज़्यादा बार कॉल किया जा सकता है) और दिए गए मैक्रो का इस्तेमाल करके मैसेज लॉग करें. कॉन्फ़िगरेशन के संभावित विकल्पों की सूची के लिए, लॉगर क्रेट देखें.

लॉगर क्रेट, आपको यह तय करने के लिए एपीआई उपलब्ध कराता है कि आपको क्या लॉग करना है. कोड, डिवाइस पर चल रहा है या होस्ट पर (जैसे, होस्ट-साइड टेस्ट का हिस्सा), इस आधार पर मैसेज को android_logger या env_logger का इस्तेमाल करके लॉग किया जाता है.

Rust AIDL का उदाहरण

इस सेक्शन में, हैलो वर्ल्ड स्टाइल में 'रस्ट' के साथ एआईडीएल का इस्तेमाल करने का उदाहरण दिया गया है.

Android डेवलपर गाइड के AIDL की खास जानकारी सेक्शन को शुरुआती बिंदु के तौर पर इस्तेमाल करके, IRemoteService.aidl फ़ाइल में यहां दिए गए कॉन्टेंट के साथ external/rust/binder_example/aidl/com/example/android/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",
    ],
}

ध्यान दें कि rustlibs में इस्तेमाल की गई, एआईडीएल से जनरेट की गई लाइब्रेरी के लिए मॉड्यूल के नाम का फ़ॉर्मैट, aidl_interface मॉड्यूल का नाम है. इसके बाद, -rust आता है. इस मामले में, com.example.android.remoteservice-rust आता है.

इसके बाद, एआईडीएल इंटरफ़ेस का रेफ़रंस 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 का उदाहरण

इस सेक्शन में, हैलो वर्ल्ड स्टाइल में एक उदाहरण दिया गया है. इसमें एक साथ कई रस्ट के साथ एआईडीएल का इस्तेमाल करने का उदाहरण दिया गया है.

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

ध्यान दें कि join_thread_pool को अंदरूनी तौर पर block_on का इस्तेमाल करने की अनुमति देने के लिए, एक साथ काम न करने वाले कॉन्टेक्स्ट से बाहर निकलने के लिए, block_in_place की ज़रूरत होती है. ऐसा इसलिए होता है, क्योंकि #[tokio::main], block_on को किए जाने वाले कॉल में कोड को रैप करता है. वहीं, join_thread_pool कोई लेन-देन करते समय block_on को कॉल कर सकता है. block_on के भीतर से block_on को कॉल करने से दहशत फैल जाती है. इस समस्या से बचने के लिए, #[tokio::main] का इस्तेमाल करने के बजाय, टोक्यो रनटाइम को मैन्युअल तौर पर बनाएं. इसके बाद, block_on तरीके के बजाय join_thread_pool को कॉल करें.

इसके अलावा, रस्ट बैकएंड जनरेट की गई लाइब्रेरी में एक इंटरफ़ेस शामिल है, जो RemoteService के लिए एसिंक क्लाइंट IRemoteServiceAsync को लागू करने की अनुमति देता है. इसे इस तरह लागू किया जा सकता है:

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),
    }
}

C से Rust को कॉल करना

इस उदाहरण में C से Rust को कॉल करने का तरीका बताया गया है.

Rust लाइब्रेरी का उदाहरण

external/rust/simple_printer/libsimple_printer.rs में libsimple_printer फ़ाइल को इस तरह से तय करें:

//! 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 को कॉल करके बिल्ड करें.

रस्ट-जावा इंटरऑप

jni क्रेट, Java नेटिव इंटरफ़ेस (JNI) के ज़रिए Rust को Java के साथ इंटरऑपरेबल बनाता है. यह Rust के लिए ज़रूरी टाइप की परिभाषाएं तय करता है, ताकि Rust cdylib लाइब्रेरी बना सकें, जो सीधे Java के JNI (JNIEnv, JClass, JString वगैरह) में प्लग इन हो जाए. cxx के ज़रिए कोडजन करने वाली C++ बाइंडिंग के उलट, JNI के ज़रिए Java इंटरऑपरेबिलिटी के लिए बिल्ड के दौरान कोड जनरेशन चरण की ज़रूरत नहीं होती. इसलिए इसके लिए खास बिल्ड-सिस्टम सपोर्ट की ज़रूरत नहीं है. Java कोड, किसी दूसरी नेटिव लाइब्रेरी की तरह ही, Rust से मिले cdylib को लोड करता है.

इस्तेमाल

Rust और Java कोड, दोनों के इस्तेमाल के बारे में jni क्रेट दस्तावेज़ में बताया गया है. कृपया वहां दिए गए शुरू करने के उदाहरण का पालन करें. src/lib.rs लिखने के बाद, इस पेज पर वापस आकर Android के बिल्ड सिस्टम से लाइब्रेरी बनाने का तरीका जानें.

बिल्ड की परिभाषा

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 लाइब्रेरी के साथ इंस्टॉल किया गया है, भले ही यह बिल्ड-टाइम डिपेंडेंसी न हो:

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

इसके अलावा, अगर आपको AndroidManifest.xml फ़ाइल में Rust लाइब्रेरी शामिल करनी है, तो लाइब्रेरी को uses_libs में इस तरह जोड़ें:

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

CXX का इस्तेमाल करके रस्ट–C++ इंटरऑप

CXX क्रेट, Rust और C++ के सबसेट के बीच सुरक्षित FFI उपलब्ध कराता है. CXX के दस्तावेज़ में इसके अच्छे उदाहरण दिए गए हैं कि यह सामान्य तौर पर कैसे काम करता है. हमारा सुझाव है कि लाइब्रेरी के बारे में जानने और C++ और Rust को जोड़ने के लिए, इसे पहले पढ़ें. नीचे दिए गए उदाहरण में, Android में इसका इस्तेमाल करने का तरीका बताया गया है.

CXX जिस C++ कोड में रस्ट कॉल करता है उसे जनरेट करने के लिए, CXX को शुरू करने के लिए genrule और उसे लाइब्रेरी में शामिल करने के लिए cc_library_static तय करें. अगर C++ से Rust कोड का इस्तेमाल किया जाना है या C++ और Rust के बीच शेयर किए गए टाइप का इस्तेमाल करना है, तो दूसरा जनरेशन नियम तय करें. इससे 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++ साइड को जनरेट करने के लिए किया गया है. हमारे Rust एक्ज़ीक्यूटेबल के लिए, इसके बाद libcxx_test_cpp स्टैटिक लाइब्रेरी का इस्तेमाल डिपेंडेंसी के तौर पर किया जाता है:

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 से इसका इस्तेमाल करने के लिए, lib.rs में नीचे बताए गए तरीके से CXX ब्रिज तय करें:

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