Padrões Android Rust

Esta página contém informações sobre o Android Logging , fornece um exemplo de Rust AIDL , informa como chamar Rust de C e fornece instruções para interoperabilidade Rust/C++ usando CXX .

Registro do Android

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

No seu 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",
    ],
}

A seguir, em sua fonte 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 chamá-lo mais de uma vez, se necessário) e registre mensagens usando as macros fornecidas. Consulte a caixa do logger para obter uma lista de possíveis opções de configuração.

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

Exemplo de ferrugem AIDL

Esta seção fornece um exemplo no estilo Hello World de uso de AIDL com Rust.

Usando a seção Visão geral do AIDL do Guia do desenvolvedor Android como ponto de partida, crie external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl com o seguinte conteúdo 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, dentro do arquivo external/rust/binder_example/aidl/Android.bp , defina o módulo aidl_interface . Você deve habilitar explicitamente o back-end do Rust porque ele não está habilitado 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 backend AIDL é um gerador de origem Rust, portanto opera como outros geradores de origem Rust e produz uma biblioteca Rust. O módulo da biblioteca Rust produzido pode ser usado por outros módulos Rust como uma dependência. Como exemplo de uso da biblioteca produzida como dependência, uma rust_library pode ser definida como segue 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",
    ],
}

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

A interface AIDL pode então ser referenciada em src/lib.rs da seguinte forma:

// 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 ferrugem assíncrona AIDL

Esta seção fornece um exemplo no estilo Hello World de uso de AIDL com Rust assíncrono.

Continuando no exemplo RemoteService , a biblioteca backend AIDL gerada inclui interfaces assíncronas que podem ser usadas para implementar uma implementação de servidor assíncrono para a interface AIDL RemoteService .

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

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 ser iniciada da seguinte forma:

#[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 block_in_place é necessário para sair do contexto assíncrono que permite que join_thread_pool use block_on 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 lidar com uma transação recebida. Chamar um block_on de dentro de um block_on resulta em pânico. Isso também poderia ser evitado construindo o tempo de execução do Tokio manualmente em vez de usar #[tokio::main] e então chamar join_thread_pool fora do método block_on .

Além disso, a biblioteca gerada pelo backend de ferrugem inclui uma interface que permite implementar um cliente assíncrono IRemoteServiceAsync para RemoteService que pode ser implementado da seguinte 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),
    }
}

Chamando Rust de C

Este exemplo mostra como chamar Rust de C.

Exemplo de biblioteca Rust

Defina o arquivo libsimple_printer em external/rust/simple_printer/libsimple_printer.rs da seguinte forma:

//! 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 deve definir cabeçalhos que os módulos C dependentes podem extrair, então defina o cabeçalho external/rust/simple_printer/simple_printer.h da seguinte forma:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Defina external/rust/simple_printer/Android.bp como você vê aqui:

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

Exemplo C binário

Defina external/rust/c_hello_rust/main.c da seguinte forma:

#include "simple_printer.h"

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

Defina external/rust/c_hello_rust/Android.bp da seguinte forma:

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

Finalmente, construa chamando m c_hello_rust .

Interoperabilidade Rust-Java

A caixa jni fornece interoperabilidade Rust com Java por meio da Java Native Interface (JNI). Ele define as definições de tipo necessárias para Rust produzir uma biblioteca Rust cdylib que se conecta diretamente ao JNI do Java ( JNIEnv , JClass , JString e assim por diante). Ao contrário das ligações C++ que executam codegen por meio de cxx , a interoperabilidade Java por meio do JNI não requer uma etapa de geração de código durante uma construção. Portanto, não precisa de suporte especial ao sistema de compilação. O código Java carrega o cdylib fornecido pelo Rust como qualquer outra biblioteca nativa.

Uso

O uso em código Rust e Java é abordado na documentação jni crate . Siga o exemplo de primeiros passos fornecido lá. Depois de escrever src/lib.rs , retorne a esta página para aprender como construir a biblioteca com o sistema de compilação do Android.

Definição de compilação

Java requer que a biblioteca Rust seja fornecida como cdylib para que possa ser carregada dinamicamente. A definição da biblioteca Rust em Soong é a seguinte:

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 biblioteca Rust como uma dependência required ; isso garante que ele seja instalado no dispositivo junto com a biblioteca Java, mesmo que não seja uma dependência em tempo de construção:

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

Alternativamente, se você precisar incluir a biblioteca Rust em um arquivo AndroidManifest.xml , adicione a biblioteca a uses_libs da seguinte maneira:

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

Interoperabilidade Rust-C++ usando CXX

A caixa CXX fornece FFI seguro entre Rust e um subconjunto de C++. A documentação do CXX dá bons exemplos de como funciona em geral e sugerimos lê-la primeiro para se familiarizar com a biblioteca e como ela une C++ e Rust. O exemplo a seguir mostra como usá-lo no Android.

Para que o CXX gere o código C++ que Rust chama, defina uma genrule para invocar o CXX e um cc_library_static para agrupá-lo em uma biblioteca. Se você planeja que o C++ chame o código Rust ou use tipos compartilhados entre C++ e Rust, defina uma segunda genrule (para gerar um cabeçalho C++ contendo as ligações 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 é usada acima para gerar o lado C++ da ponte. A biblioteca estática libcxx_test_cpp é usada a seguir como uma dependência para nosso 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 desejar, usando os tipos de wrapper CXX conforme desejado. 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);

Embora cxx_test.cpp contenha

#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 isso no Rust, defina uma ponte CXX conforme abaixo em 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;
}