Pattern Rust per Android

Questa pagina contiene informazioni su Android Logging, fornisce un esempio di AIDL Rust, spiega come chiamare Rust da C e fornisce istruzioni per l'interoperabilità Rust/C++ utilizzando CXX.

Log di Android

L'esempio seguente mostra come puoi registrare i messaggi in logcat (sul dispositivo) o stdout (sull'host).

Nel modulo Android.bp, aggiungi liblogger e liblog_rust come dipendenze:

rust_binary {
    name: "logging_test",
    srcs: ["src/main.rs"],
    rustlibs: [
        "liblogger",
        "liblog_rust",
    ],
}

Aggiungi questo codice al codice sorgente 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!");
}

In altre parole, aggiungi le due dipendenze mostrate sopra (liblogger e liblog_rust), chiama il metodo init una volta (se necessario puoi chiamarlo più volte) e registra i messaggi utilizzando le macro fornite. Consulta la cassa del logger per un elenco delle possibili opzioni di configurazione.

Il pacchetto logger fornisce un'API per definire ciò che vuoi registrare. A seconda che il codice venga eseguito sul dispositivo o sull'host (ad esempio nell'ambito di un test lato host), i messaggi vengono registrati utilizzando android_logger o env_logger.

Esempio di AIDL in Rust

Questa sezione fornisce un esempio di stile Hello World sull'utilizzo di AIDL con Rust.

Utilizzando la sezione Panoramica di AIDL della Guida per gli sviluppatori Android come punto di partenza, crea external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl con i seguenti contenuti nel file 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);
}

Poi, all'interno del file external/rust/binder_example/aidl/Android.bp, definisci il modulo aidl_interface. Devi abilitare esplicitamente il backend Rust perché non è attivo per impostazione predefinita.

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

Il backend AIDL è un generatore di codice sorgente Rust, quindi funziona come gli altri generatori di codice sorgente Rust e produce una libreria Rust. Il modulo della libreria Rust prodotto può essere utilizzato da altri moduli Rust come dipendenza. Come esempio di utilizzo della biblioteca prodotta come dipendenza, un rust_library può essere definito come segue in 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",
    ],
}

Tieni presente che il formato del nome del modulo per la libreria generata da AIDL utilizzata in rustlibs è il nome del modulo aidl_interface seguito da -rust; in questo caso, com.example.android.remoteservice-rust.

L'interfaccia AIDL può quindi essere richiamata in src/lib.rs come segue:

// 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 {
        Ok(42)
    }

    fn basicTypes(&self, _: i32, _: i64, _: bool, _: f32, _: f64, _: &str) -> BinderResult<()> {
        // Do something interesting...
        Ok(())
    }
}

Infine, avvia il servizio in un file binario Rust come mostrato di seguito:

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

Esempio di AIDL Rust asincrono

Questa sezione fornisce un esempio di stile Hello World sull'utilizzo di AIDL con Rust asincrono.

Continuando con l'esempio RemoteService, la libreria di backend AIDL generata include interfacce asincrone che possono essere utilizzate per implementare un'implementazione del server asincrono per l'interfaccia AIDL RemoteService.

L'interfaccia del server asincrono IRemoteServiceAsyncServer generata può essere implementata come segue:

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 {
        //Do something interesting...
        Ok(42)
    }

    async fn basicTypes(&self, _: i32, _: i64, _: bool, _: f32, _: f64,_: &str,) -> BinderResult<()> {
        //Do something interesting...
        Ok(())
    }
}

L'implementazione del server asincrono può essere avviata nel seguente modo:

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

Tieni presente che block_in_place è necessario per uscire dal contesto asincrono che consente a join_thread_pool di utilizzare block_on internamente. Questo accade perché #[tokio::main] inserisce il codice in una chiamata a block_on e join_thread_pool potrebbe chiamare block_on durante la gestione di una transazione in arrivo. La chiamata di un block_on all'interno di un block_on provoca un panico. Questo problema può essere anche evitato compilando il runtime tokio manualmente anziché utilizzare #[tokio::main] e chiamare join_thread_pool al di fuori del metodo block_on.

Inoltre, la libreria generata dal backend Rust include un'interfaccia che consente di implementare un client asincrono IRemoteServiceAsync per RemoteService che può essere implementato come segue:

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

Chiamare Rust da C

Questo esempio mostra come chiamare Rust da C.

Libreria Rust di esempio

Definisci il file libsimple_printer inexternal/rust/simple_printer/libsimple_printer.rs come segue:

//! 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!");
}

La libreria Rust deve definire le intestazioni che i moduli C dipendenti possono includere, quindi definisci l'intestazione external/rust/simple_printer/simple_printer.h come segue:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Definisci external/rust/simple_printer/Android.bp come mostrato di seguito:

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: ["."],
}

File binario C di esempio

Definisci external/rust/c_hello_rust/main.c come segue:

#include "simple_printer.h"

int main() {
  print_c_hello_rust();
  return 0;
}

Definisci external/rust/c_hello_rust/Android.bp come segue:

cc_binary {
    name: "c_hello_rust",
    srcs: ["main.c"],
    shared_libs: ["libsimple_c_printer"],
}

Infine, esegui la compilazione chiamando m c_hello_rust.

Interoperabilità Rust-Java

Il crate jni fornisce l'interoperabilità di Rust con Java tramite l'interfaccia JNI (Java Native). Definisce le definizioni di tipo necessarie per consentire a Rust di produrre una libreria cdylib Rust che si collega direttamente a JNI di Java (JNIEnv, JClass, JString e così via). A differenza delle associazioni C++ che eseguono la generazione di codice tramite cxx, l'interoperabilità Java tramite JNI non richiede un passaggio di generazione di codice durante una compilazione. Di conseguenza, non richiede un supporto speciale del sistema di compilazione. Il codice Java carica cdylib fornito da Rust come qualsiasi altra libreria nativa.

Utilizzo

L'utilizzo nel codice Rust e Java è descritto nella documentazione della cassetta jni. Segui l'esempio di Guida introduttiva fornito. Dopo aver scritto src/lib.rs, torna a questa pagina per scoprire come compilare la libreria con il sistema di compilazione di Android.

Definizione build

Java richiede che la libreria Rust venga fornita come cdylib in modo che possa essere caricata dinamicamente. La definizione della libreria Rust in Soong è la seguente:

rust_ffi_shared {
    name: "libhello_jni",
    crate_name: "hello_jni",
    srcs: ["src/lib.rs"],

    // The jni crate is required
    rustlibs: ["libjni"],
}

La libreria Java elenca la libreria Rust come dipendenza required; in questo modo viene installata sul dispositivo insieme alla libreria Java anche se non è una dipendenza di compilazione:

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

In alternativa, se devi includere la libreria Rust in un file AndroidManifest.xml, aggiungila a uses_libs come segue:

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

Interoperabilità Rust-C++ con CXX

Il crate CXX fornisce FFI sicuri tra Rust e un sottoinsieme di C++. La documentazione di CXX fornisce ottimi esempi di come funziona in generale e ti consigliamo di leggerla per prima cosa per acquisire familiarità con la libreria e il modo in cui collega C++ e Rust. L'esempio seguente mostra come utilizzarlo in Android.

Per fare in modo che CXX generi il codice C++ chiamato da Rust, definisci un genrule per invocare CXX e un cc_library_static per raggrupparlo in una libreria. Se prevedi di far chiamare il codice Rust da C++ o di utilizzare tipi condivisi tra C++ e Rust, definisci una seconda regola genrule (per generare un'intestazione C++ contenente le associazioni 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"],
}

Lo strumento cxxbridge viene utilizzato sopra per generare il lato C++ del bridge. La libreria statica libcxx_test_cpp viene utilizzata successivamente come dipendenza per il nostro eseguibile Rust:

rust_binary {
    name: "cxx_test",
    srcs: ["lib.rs"],
    rustlibs: ["libcxx"],
    static_libs: ["libcxx_test_cpp"],
}

Nei file .cpp e .hpp, definisci le funzioni C++ come preferisci, utilizzando i tipi di wrapper CXX come preferisci. Ad esempio, una definizione di cxx_test.hpp contiene quanto segue:

#pragma once

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

int greet(rust::Str greetee);

Mentre cxx_test.cppcontiene

#include "cxx_test.hpp"
#include "lib.rs.h"

#include 

int greet(rust::Str greetee) {
  std::cout << "Hello, " << greetee << std::endl;
  return get_num();
}

Per utilizzarlo da Rust, definisci un bridge CXX come indicato di seguito in 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;
}