Wzory Android Rust

Ta strona zawiera informacje o logowaniu w Androidzie, przykład pliku AIDL w Rust, instrukcje dotyczące wywołania Rust z języka C oraz instrukcje dotyczące interoperacyjności Rust/C++ za pomocą CXX.

Logowanie w Androidzie

Poniższy przykład pokazuje, jak zarejestrować wiadomości w logcat (na urządzeniu) lub stdout (na serwerze).

W module Android.bp dodaj jako zależności pliki libloggerliblog_rust:

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

Następnie w źródle Rust dodaj ten kod:

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

Oznacza to, że należy dodać 2 zależności widoczne powyżej (libloggerliblog_rust), wywołać metodę init raz (w razie potrzeby można ją wywołać więcej niż raz) oraz rejestrować wiadomości za pomocą podanych makro. Zobacz skrzynka na dziennik , aby zobaczyć listę możliwych opcji konfiguracji.

Skrzynia z rejestratorem udostępnia interfejs API, który określa, co ma być zapisywane. W zależności od czy kod działa na urządzeniu czy na hoście (np. testu po stronie hosta), wiadomości są rejestrowane za pomocą tagu android_logger. lub env_logger.

Przykład Rust AIDL

Ta sekcja zawiera przykład użycia AIDL w stylu Hello World w środowisku Rust.

Korzystanie z Przewodnika dla programistów aplikacji na Androida Omówienie AIDL jako punktu wyjścia, utwórz external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl z tą zawartością w pliku 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);
}

Następnie w pliku external/rust/binder_example/aidl/Android.bp zdefiniuj moduł aidl_interface. Musisz wyraźnie włączyć backend Rust, ponieważ nie jest on domyślnie włączony.

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

Backend AIDL to generator kodu źródłowego Rust, więc działa jak inne generatory kodu źródłowego Rust i tworzy bibliotekę Rust. Wytworzony moduł biblioteki Rust można używane przez inne moduły Rusta jako zależność. Jako przykład wykorzystania wygenerowanego jako zależność, funkcję rust_library można zdefiniować w ten sposób 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",
    ],
}

Pamiętaj, że format nazwy modułu biblioteki wygenerowanej za pomocą AIDL używanej w rustlibs to nazwa modułu aidl_interface, po której następuje -rust. W tym przypadku jest to com.example.android.remoteservice-rust.

Do interfejsu AIDL można się odwoływać w pliku src/lib.rs w ten sposób:

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

Na koniec uruchom usługę w pliku binarnym Rust, jak pokazano poniżej:

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

Przykład asynchronicznego Rust AIDL

Ta sekcja zawiera przykład użycia AIDL w stylu Hello World z asynchronicznym systemem Rust.

Wracając do przykładu RemoteService, wygenerowana biblioteka backendowa AIDL zawiera interfejsy asynchroniczne, które można wykorzystać do implementacji asynchronicznej implementacji serwera dla interfejsu AIDL RemoteService.

Wygenerowany interfejs serwera asynchronicznego IRemoteServiceAsyncServer może być są zaimplementowane w następujący sposób:

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

Implementację serwera asynchronicznego można uruchomić w ten sposób:

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

Pamiętaj, że parametr block_in_place jest konieczne, aby opuścić kontekst asynchroniczny, który umożliwia usłudze join_thread_pool korzystanie z block_on. Wynika to z faktu, że kod #[tokio::main] opakowuje kod w trakcie połączenia z numerem block_on, a join_thread_pool może zadzwonić block_on podczas obsługi transakcji przychodzącej. Wywołuję block_on z poziomu block_on wywołuje panikę. Można tego uniknąć, budując środowisko wykonawcze Tokio ręcznie zamiast używać #[tokio::main], a potem wywoływać join_thread_pool poza metodą block_on.

Ponadto biblioteka wygenerowana przez backend Rust zawiera interfejs, który umożliwia implementację klienta asynchronicznego IRemoteServiceAsync dla RemoteService. Można go zaimplementować w ten sposób:

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

Zadzwoń do Rust z C

Ten przykład pokazuje, jak zadzwonić do Rusta z aplikacji C.

Przykładowa biblioteka Rust

Zdefiniuj plik libsimple_printer w pliku external/rust/simple_printer/libsimple_printer.rs w ten sposób:

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

Biblioteka Rust musi określać nagłówki, które zależne moduły C mogą pobierać, więc zdefiniuj nagłówek external/rust/simple_printer/simple_printer.h w taki sposób:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Podaj definicję słowa external/rust/simple_printer/Android.bp tak jak tutaj:

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

Przykład pliku binarnego C

Zdefiniuj external/rust/c_hello_rust/main.c w ten sposób:

#include "simple_printer.h"

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

Zdefiniuj external/rust/c_hello_rust/Android.bp w ten sposób:

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

Na koniec kompiluj, wywołując funkcję m c_hello_rust.

Interoperacyjność Rust-Java

Kontener jni zapewnia interoperacyjność platformy Rust z Javą dzięki systemowi natywnemu w Javie Interfejs (JNI). Określa ona niezbędne definicje typów w Rust, aby wygenerować bibliotekę Rust cdylib, która łączy się bezpośrednio z JNI w Javie (JNIEnv, JClass, JString itd.). W odróżnieniu od połączeń C++, które wykonują generowanie kodu za pomocą cxx, współdziałanie Java za pomocą JNI nie wymaga generowania kodu podczas kompilacji. Dlatego nie wymaga specjalnego wsparcia systemu kompilacji. Kod Java wczytuje cdylib udostępniony przez Rust, tak jak każdą inną natywną bibliotekę.

Wykorzystanie

Zastosowanie w kodzie Rust i Java jest opisane w dokumentacji jnicrate. Postępuj zgodnie z podanym tam opisem procesu rozpoczęcia. Gdy napiszesz „src/lib.rs”, wróć na tę stronę, aby: dowiedz się, jak utworzyć bibliotekę w systemie kompilacji Androida.

Definicja kompilacji

Java wymaga udostępnienia biblioteki Rust jako biblioteki cdylib, aby można było ją ładowane dynamicznie. Definicja biblioteki Rust w Soong:

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

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

Biblioteka Java wymienia bibliotekę Rust jako zależność required. Dzięki temu zostanie ona zainstalowana na urządzeniu wraz z biblioteką Java, mimo że nie jest zależnością w czasie kompilacji:

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

Jeśli musisz uwzględnić bibliotekę Rust w pliku AndroidManifest.xml , dodaj ją do pliku uses_libs w ten sposób:

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

Interoperacyjność Rust–C++ wykorzystująca CXX

Pakiet CXX zapewnia bezpieczne FFI między Rustem a podzbiorem C++. Dokumentacja CXX zawiera dobre przykłady ogólnego działania tego pakietu. Zalecamy zapoznanie się z nią, aby poznać bibliotekę i sposób, w jaki łączy ona C++ z Rustem. Ten przykład pokazuje, jak używać tego narzędzia na Androidzie.

Aby umożliwić CXX wygenerowanie kodu C++ używanego przez Rust, zdefiniuj genrule do wywołaj CXX i cc_library_static, by połączyć to w bibliotekę. Jeśli planujesz wywoływać kod Rust z poziomu kodu C++, lub używać typów wspólnych dla C++ i Rust, zdefiniuj drugą regułę generowania (aby wygenerować nagłówek C++, który zawiera powiązania 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"],
}

Narzędzia cxxbridge używa się powyżej do wygenerowania strony C++ mostu. libcxx_test_cpp Biblioteka statyczna jest używana jako zależność dla naszego pliku wykonywalnego Rust:

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

W plikach .cpp i .hpp zdefiniuj funkcje C++ według własnego uznania, za pomocą typów otoki CXX. Definicja cxx_test.hpp zawiera na przykład:

#pragma once

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

int greet(rust::Str greetee);

Chociaż cxx_test.cppzawiera

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

#include <iostream>

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

Aby użyć tego z platformy Rust, zdefiniuj most CXX w sposób opisany poniżej w narzędziu 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;
}