Padrões Rust no Android

Esta página contém informações sobre a geração de registros do Android, fornece um exemplo da AIDL da biblioteca Rust, informa como chamar a Rust no código C, e fornece instruções para Interoperabilidade Rust/C++ usando CXX.

Geração de registros do Android

O exemplo a seguir mostra como registrar mensagens no logcat (no dispositivo) ou stdout (no host).

No módulo Android.bp, adicione liblogger e liblog_rust como dependências:

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

Em seguida, na sua origem do Rust, adicione 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!");
}

Ou seja, adicione as duas dependências mostradas acima (liblogger e liblog_rust), chame o método init uma vez (você pode o chamar mais de uma vez, se necessário) e registre as mensagens usando as macros fornecidas. Consulte a caixa de geração de registros para ver uma lista das possíveis opções de configuração.

A caixa de geração de registro fornece uma API para definir o que você quer registrar. Dependendo se o código está sendo executado no dispositivo ou no host (como parte de um teste do lado do host), as mensagens são registradas usando o android_logger ou o env_logger (links em inglês).

Exemplo de AIDL do Rust

Veja um exemplo no estilo "Hello World" de uso da AIDL com o Rust nesta seção.

Usando a seção Visão geral da AIDL do Guia para desenvolvedores Android como ponto de partida, crie external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl com o conteúdo abaixo no arquivo 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);
}

Em seguida, no arquivo external/rust/binder_example/aidl/Android.bp, defina o módulo aidl_interface. Você precisa ativar explicitamente o back-end do Rust porque ele não é ativado por padrão.

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

O back-end da AIDL é um gerador de origem Rust. Por isso, ele opera como outros geradores de mesma origem e produz uma biblioteca Rust. O módulo produzido dessa biblioteca pode ser usado por outros módulos Rust como uma dependência. Como exemplo de uso da biblioteca produzida como uma dependência, uma rust_library pode ser definida desta maneira em 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",
    ],
}

O formato do nome do módulo para a biblioteca gerada pela AIDL usada em rustlibs é o nome do módulo aidl_interface seguido por -rust. Neste caso, com.example.android.remoteservice-rust.

A interface da AIDL pode ser referenciada em src/lib.rs desta maneira:

// 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 fim, inicie o serviço em um binário Rust, conforme mostrado abaixo:

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

Exemplo de AIDL do Rust assíncrono

Nesta seção, incluímos um exemplo no estilo "Hello World" de uso da AIDL com o Rust assíncrono.

Continuando com o exemplo de RemoteService, a biblioteca de back-end gerada por AIDL inclui interfaces assíncronas que podem ser usadas para implementar um servidor assíncrono para a interface de AIDL RemoteService.

A interface IRemoteServiceAsyncServer do servidor assíncrono gerada pode ser implementada da seguinte maneira:

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

A implementação do servidor assíncrono pode começar assim:

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

Observe que o block_in_place é necessário para sair do contexto assíncrono que permite que join_thread_pool use block_on (links em inglês) internamente. Isso ocorre porque #[tokio::main] envolve o código em uma chamada para block_on, e join_thread_pool pode chamar block_on ao processar uma transação recebida. Chamar um block_on de dentro de block_on gera pânico. Isso também pode ser evitado criando o ambiente de execução "tokio" manualmente (link em inglês) em vez de usar #[tokio::main] e chamar join_thread_pool fora do método block_on.

Além disso, a biblioteca de back-end gerada por Rust inclui uma interface que permite implementar um cliente assíncrono IRemoteServiceAsync para RemoteService que pode ser realizado assim:

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

Chamar o Rust no código C

Este exemplo mostra como chamar o Rust do código C.

Biblioteca de exemplo do Rust

Defina o arquivo libsimple_printer em external/rust/simple_printer/libsimple_printer.rs desta maneira:

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

A biblioteca Rust precisa definir cabeçalhos que os módulos C dependentes podem extrair, portanto, defina o cabeçalho external/rust/simple_printer/simple_printer.h desta maneira:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Defina external/rust/simple_printer/Android.bp como mostrado abaixo:

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

Exemplo de binário C

Defina o external/rust/c_hello_rust/main.c desta maneira:

#include "simple_printer.h"

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

Defina o external/rust/c_hello_rust/Android.bp desta maneira:

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

Por fim, chame m c_hello_rust para criar.

Interoperabilidade entre Rust e Java

A caixa jni fornece interoperabilidade entre Rust e Java na Java Native Interface (JNI). Ela configura as definições de tipo necessárias para que o Rust produza uma biblioteca cdylib que se conecta diretamente à JNI do Java (JNIEnv, JClass, JString e assim por diante). Ao contrário das vinculações em C++, que executam o codegen na caixa cxx, a interoperabilidade do Java na JNI não requer uma etapa de geração de código durante uma criação. Por isso, ela não precisa de suporte especial para o sistema de build. O código Java carrega a cdylib fornecida pelo Rust como qualquer outra biblioteca nativa.

Uso

O uso nos códigos Rust e Java é abordado na documentação da caixa jni. Siga o exemplo de Como começar (links em inglês) disponível nela. Depois de programar src/lib.rs, volte a esta página para aprender a criar a biblioteca com o sistema de build do Android.

Definição do build

O Java exige que a biblioteca do Rust seja fornecida como uma cdylib para que possa ser carregada dinamicamente. Veja a definição da biblioteca do Rust no Soong:

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

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

A biblioteca Java lista a do Rust como uma dependência required. Isso garante que ela seja instalada no dispositivo junto à biblioteca Java, mesmo que não seja uma dependência do tempo de build.

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

Como alternativa, se você precisar incluir a biblioteca do Rust em um arquivo AndroidManifest.xml, adicione-a a uses_libs desta maneira:

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

Interoperabilidade entre Rust e C++ usando CXX

A caixa CXX fornece uma FFI segura entre o Rust e um subconjunto de C++. A documentação da CXX (links em inglês) dá bons exemplos de como ela funciona no geral, e sugerimos a leitura para entender a biblioteca e a maneira como ela faz a ponte entre o C++ e o Rust. O exemplo abaixo mostra o uso no Android.

Para que a CXX gere o código C++ chamado pelo Rust, defina uma genrule para invocar a CXX e uma cc_library_static para inclusão em uma biblioteca. Se você planeja usar C++ para chamar um código Rust ou se usa tipos compartilhados entre C++ e Rust, defina uma segunda genrule para gerar um cabeçalho C++ com as vinculações do 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"],
}

A ferramenta cxxbridge (link em inglês) é usada acima para gerar o lado C++ da ponte. A biblioteca estática libcxx_test_cpp é usada abaixo como uma dependência do executável Rust:

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

Nos arquivos .cpp e .hpp, defina as funções C++ como quiser, usando os tipos de wrapper CXX (link em inglês) como preferir. Por exemplo, uma definição cxx_test.hpp contém o seguinte:

#pragma once

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

int greet(rust::Str greetee);

Enquanto cxx_test.cpp contém:

#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 essas definições com o Rust, defina uma ponte CXX como mostrado abaixo 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;
}