Patrones de Rust en Android

Esta página contiene información sobre el registro de Android, proporciona un ejemplo del AIDL de Rust y te indica cómo llamar a Rust desde C, además de brindar instrucciones para la interoperabilidad de Rust/C++ mediante CXX.

Registro de Android

En el siguiente ejemplo, se muestra cómo puedes registrar mensajes en logcat (en el dispositivo) o stdout (en el host).

En el módulo Android.bp, agrega liblogger y liblog_rust como dependencias:

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

Luego, en tu fuente de Rust, agrega este código:

use log::{debug, error, Level};

fn main() {
    let init_success = logger::init(
        logger::Config::default()
            .with_tag_on_device("mytag")
            .with_min_level(Level::Trace),
    );
    debug!("This is a debug message.");
    error!("Something went wrong!");
}

Es decir, agrega las dos dependencias que se muestran arriba (liblogger y liblog_rust). Llama al método init una vez (puedes llamarlo más de una vez si es necesario) y registra mensajes con las macros proporcionadas. Consulta el contenedor de registro para obtener una lista de opciones de configuración posibles.

El contenedor de registro proporciona una API para definir lo que deseas registrar. Según si el código se ejecuta en el dispositivo o en el host (como parte de una prueba del lado del host), los mensajes se registran mediante android_logger o env_logger.

Ejemplo de AIDL de Rust

En esta sección, se proporciona un ejemplo de estilo Hello World del uso de AIDL con Rust.

Usa la sección Descripción general de AIDL de la Guía para desarrolladores de Android como punto de partida y crea external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl con el siguiente contenido en el archivo 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);
}

Luego, dentro del archivo external/rust/binder_example/aidl/Android.bp, define el módulo aidl_interface. Debes habilitar de forma explícita el backend de Rust porque no está habilitado de forma predeterminada.

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

El backend del AIDL es un generador de fuentes de Rust, por lo que funciona como otros generadores de fuentes de Rust y produce una biblioteca de Rust. Otros módulos de Rust pueden usar como dependencia el módulo de biblioteca de Rust generado. Para ejemplificar cómo usar la biblioteca generada como dependencia, se puede definir un rust_library de la siguiente manera en 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",
    ],
}

Ten en cuenta que el formato de nombre del módulo para la biblioteca generada con AIDL que se usa en rustlibs es el nombre del módulo aidl_interface seguido de -rust; en este caso, com.example.android.remoteservice-rust.

Luego, se puede hacer referencia a la interfaz del AIDL en src/lib.rs de la siguiente manera:

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

Por último, inicia el servicio en un objeto binario de Rust como se muestra a continuación:

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

Ejemplo del AIDL de Rust asíncrono

En esta sección, se proporciona un ejemplo de estilo Hello World del uso del AIDL con Rust asíncrono.

Para continuar con el ejemplo de RemoteService, la biblioteca de backend del AIDL incluye interfaces asíncronas que pueden usarse para implementar un servidor asíncrono para la interfaz del AIDL RemoteService.

El IRemoteServiceAsyncServer de la interfaz de servidor asíncrono generado puede implementarse de la siguiente manera:

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

La implementación del servidor asíncrono puede comenzar de la siguiente manera:

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

Ten en cuenta que el objeto block_in_place es necesario para brindar el contexto asíncrono que permite que join_thread_pool use block_on internamente. Esto se debe a que #[tokio::main] une el código en la llamada a block_on y es posible que join_thread_pool llame a block_on cuando se encargue de una transacción entrante. La llamada a block_on dentro de block_on da como resultado errores. Esto se puede evitar si se crea un tiempo de ejecución tokio manualmente en lugar de usar #[tokio::main] y, luego, llamar a join_thread_pool desde fuera del método block_on.

Además, la biblioteca generada por el backend de Rust incluye una interfaz que permite la implementación de IRemoteServiceAsync de cliente asíncrono para RemoteService, que puede implementarse de la siguiente forma:

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::get_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ómo llamar a Rust desde C

En este ejemplo, se muestra cómo llamar a Rust desde C.

Ejemplo de la biblioteca de Rust

Define el archivo libsimple_printer en external/rust/simple_printer/libsimple_printer.rs de la siguiente manera:

//! 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 biblioteca de Rust debe definir los encabezados que los módulos C dependientes puedan extraer, por lo que debes definir el encabezado external/rust/simple_printer/simple_printer.h de la siguiente manera:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Define external/rust/simple_printer/Android.bp como se muestra aquí:

rust_ffi {
    name: "libsimple_c_printer",
    crate_name: "simple_c_printer",
    srcs: ["libsimple_c_printer.rs"],

    // Define include_dirs so cc_binary knows where the headers are.
    include_dirs: ["."],
}

Ejemplo de un objeto binario de C

Define external/rust/c_hello_rust/main.c de la siguiente manera:

#include "simple_printer.h"

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

Define external/rust/c_hello_rust/Android.bp de la siguiente manera:

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

Por último, para compilar, llama a m c_hello_rust.

Interoperabilidad de Rust y Java

El contenedor jni proporciona interoperabilidad de Rust con Java mediante la interfaz nativa de Java (JNI). Establece las definiciones de tipo necesarias para que Rust produzca una biblioteca cdylib de Rust que se conecta directamente a la JNI de Java (JNIEnv, JClass, JString y así sucesivamente). A diferencia de las vinculaciones de C++ que realizan codegen por medio de cxx, la interoperabilidad de Java a través de la JNI no requiere un paso de generación de código durante una compilación. Por eso, no necesita compatibilidad especial con el sistema de compilación. El código Java carga cdylib que proporciona Rust como cualquier otra biblioteca nativa.

Uso

El uso de los códigos Rust y Java se incluye en la documentación del contenedor jni. Sigue el ejemplo que se proporciona en los primeros pasos. Después de escribir src/lib.rs, regresa a esta página para aprender a compilar la biblioteca con el sistema de compilación de Android.

Definición de compilación

Java requiere que la biblioteca de Rust se proporcione como un cdylib para que se pueda cargar de forma dinámica. La definición de la biblioteca Rust en Soong es la siguiente:

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

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

La biblioteca Java clasifica la biblioteca Rust como una dependencia de required; esto garantiza que se instale en el dispositivo junto con la biblioteca Java aunque no sea una dependencia del tiempo de compilación:

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

Como alternativa, si debes incluir la biblioteca de Rust en un archivo AndroidManifest.xml, agrégala a uses_libs de la siguiente manera:

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

Interoperabilidad de Rust–C++ con CXX

El contenedor CXX proporciona una FFI segura entre Rust y un subconjunto de C++. En la documentación de CXX, se brindan ejemplos útiles sobre su funcionamiento general. Te recomendamos que la leas primero para familiarizarte con la biblioteca y la forma en que crea un puente entre C++ y Rust. En el siguiente ejemplo, se muestra su uso en Android.

Si deseas que CXX genere el código de C++ al que llama Rust, define un genrule para invocar a CXX y una cc_library_static para empaquetarlo en una biblioteca. Si planeas que C++ llame al código de Rust o usas tipos compartidos entre C++ y Rust, define una segunda genrule (para generar un encabezado de C++ que contenga las vinculaciones de 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"],
}

La herramienta cxxbridge se usa más arriba para generar el lado C++ del puente. La biblioteca estática libcxx_test_cpp se usa a continuación como dependencia de nuestro ejecutable de Rust:

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

En los archivos .cpp y .hpp, define las funciones de C++ como quieras, con los tipos de wrapper de CXX que desees. Por ejemplo, una definición de cxx_test.hpp contiene lo siguiente:

#pragma once

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

int greet(rust::Str greetee);

Mientras que cxx_test.cpp contiene

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

#include <iostream>

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

Para usar esto desde Rust, define un puente CXX como se muestra a continuación en 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;
}