Modèles Android Rust

Cette page contient des informations sur la journalisation Android, fournit un exemple d'AIDL Rust, explique comment appeler Rust à partir de C et fournit des instructions pour l'interopérabilité Rust/C++ à l'aide de CXX.

Journalisation Android

L'exemple suivant montre comment consigner des messages dans logcat (sur l'appareil) ou stdout (sur l'hôte).

Dans votre module Android.bp, ajoutez liblogger et liblog_rust en tant que dépendances :

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

Ensuite, ajoutez le code suivant à votre source 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!");
}

Autrement dit, ajoutez les deux dépendances indiquées ci-dessus (liblogger et liblog_rust), appelez la méthode init une fois (vous pouvez l'appeler plusieurs fois si nécessaire) et enregistrez les messages à l'aide des macros fournies. Consultez la caisse d'enregistrement pour obtenir la liste des options de configuration possibles.

La crate du journaliseur fournit une API permettant de définir ce que vous souhaitez consigner. Selon que le code s'exécute sur l'appareil ou sur l'hôte (par exemple, dans le cadre d'un test côté hôte), les messages sont enregistrés à l'aide de android_logger ou env_logger.

Exemple Rust AIDL

Cette section fournit un exemple de type "Hello World" d'utilisation d'AIDL avec Rust.

En utilisant la section Présentation d'AIDL du guide du développeur Android comme point de départ, créez external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl avec le contenu suivant dans le fichier 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);
}

Ensuite, dans le fichier external/rust/binder_example/aidl/Android.bp, définissez le module aidl_interface. Vous devez activer explicitement le backend Rust, car il n'est pas activé par défaut.

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

Le backend AIDL est un générateur de sources Rust. Il fonctionne donc comme les autres générateurs de sources Rust et produit une bibliothèque Rust. Le module de bibliothèque Rust produit peut être utilisé par d'autres modules Rust en tant que dépendance. Pour illustrer l'utilisation de la bibliothèque produite en tant que dépendance, un rust_library peut être défini comme suit dans 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",
    ],
}

Notez que le format du nom de module pour la bibliothèque générée par AIDL utilisée dans rustlibs est le nom de module aidl_interface suivi de -rust, soit com.example.android.remoteservice-rust dans ce cas.

L'interface AIDL peut ensuite être référencée dans src/lib.rs comme suit :

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

Enfin, démarrez le service dans un binaire Rust, comme indiqué ci-dessous :

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

Exemple d'AIDL Rust asynchrone

Cette section fournit un exemple de type "Hello World" d'utilisation d'AIDL avec Rust asynchrone.

En reprenant l'exemple RemoteService, la bibliothèque backend AIDL générée inclut des interfaces asynchrones qui peuvent être utilisées pour implémenter une implémentation de serveur asynchrone pour l'interface AIDL RemoteService.

L'interface de serveur asynchrone générée IRemoteServiceAsyncServer peut être implémentée comme suit :

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

L'implémentation du serveur asynchrone peut être démarrée comme suit :

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

Notez que block_in_place est nécessaire pour quitter le contexte asynchrone, ce qui permet à join_thread_pool d'utiliser block_on en interne. En effet, #[tokio::main] encapsule le code dans un appel à block_on, et join_thread_pool peut appeler block_on lors du traitement d'une transaction entrante. L'appel d'un block_on depuis un block_on entraîne une panique. Vous pouvez également éviter cela en créant le runtime tokio manuellement au lieu d'utiliser #[tokio::main], puis en appelant join_thread_pool en dehors de la méthode block_on.

De plus, la bibliothèque générée par le backend Rust inclut une interface qui permet d'implémenter un client asynchrone IRemoteServiceAsync pour RemoteService, qui peut être implémenté comme suit :

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

Appeler Rust depuis C

Cet exemple montre comment appeler Rust à partir de C.

Exemple de bibliothèque Rust

Définissez le fichier libsimple_printer dansexternal/rust/simple_printer/libsimple_printer.rscomme suit :

//! 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 bibliothèque Rust doit définir des en-têtes que les modules C dépendants peuvent extraire. Définissez donc l'en-tête external/rust/simple_printer/simple_printer.h comme suit :

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Définissez external/rust/simple_printer/Android.bp comme indiqué ici :

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

Exemple de fichier binaire C

Définissez external/rust/c_hello_rust/main.c comme suit :

#include "simple_printer.h"

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

Définissez external/rust/c_hello_rust/Android.bp comme suit :

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

Enfin, créez le fichier en appelant m c_hello_rust.

Interopérabilité Rust-Java

La crate jni assure l'interopérabilité de Rust avec Java via l'interface Java Native Interface (JNI). Il définit les définitions de type nécessaires pour que Rust produise une bibliothèque cdylib Rust qui s'intègre directement à JNI de Java (JNIEnv, JClass, JString, etc.). Contrairement aux liaisons C++ qui effectuent la génération de code via cxx, l'interopérabilité Java via JNI ne nécessite pas d'étape de génération de code lors d'une compilation. Il n'a donc pas besoin d'une prise en charge spéciale du système de compilation. Le code Java charge le cdylib fourni par Rust comme n'importe quelle autre bibliothèque native.

Utilisation

L'utilisation dans le code Rust et Java est décrite dans la documentation du crate jni. Veuillez suivre l'exemple de prise en main fourni. Après avoir écrit src/lib.rs, revenez sur cette page pour découvrir comment compiler la bibliothèque avec le système de compilation d'Android.

Définition de compilation

Java exige que la bibliothèque Rust soit fournie sous la forme d'un cdylib afin qu'elle puisse être chargée de manière dynamique. La définition de la bibliothèque Rust dans Soong est la suivante :

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

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

La bibliothèque Java liste la bibliothèque Rust comme dépendance required. Cela garantit qu'elle est installée sur l'appareil en même temps que la bibliothèque Java, même s'il ne s'agit pas d'une dépendance au moment de la compilation :

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

Vous pouvez également ajouter la bibliothèque Rust à un fichier AndroidManifest.xml dans uses_libs comme suit :

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

Interopérabilité Rust–C++ avec CXX

La crate CXX fournit une FFI sécurisée entre Rust et un sous-ensemble de C++. La documentation CXX fournit de bons exemples de son fonctionnement en général. Nous vous suggérons de la lire en premier pour vous familiariser avec la bibliothèque et la façon dont elle fait le pont entre C++ et Rust. L'exemple suivant montre comment l'utiliser dans Android.

Pour que CXX génère le code C++ dans lequel Rust appelle, définissez un genrule pour appeler CXX et un cc_library_static pour regrouper ce code dans une bibliothèque. Si vous prévoyez d'appeler du code Rust à partir de C++, ou d'utiliser des types partagés entre C++ et Rust, définissez une deuxième genrule (pour générer un en-tête C++ contenant les liaisons 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"],
}

L'outil cxxbridge est utilisé ci-dessus pour générer le côté C++ du pont. La bibliothèque statique libcxx_test_cpp est ensuite utilisée comme dépendance pour notre exécutable Rust :

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

Dans les fichiers .cpp et .hpp, définissez les fonctions C++ comme vous le souhaitez, en utilisant les types d'encapsuleur CXX si nécessaire. Par exemple, une définition cxx_test.hpp contient les éléments suivants :

#pragma once

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

int greet(rust::Str greetee);

Tandis que cxx_test.cppcontient

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

#include <iostream>

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

Pour l'utiliser depuis Rust, définissez un pont CXX comme ci-dessous dans 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;
}