Wzorce Android Rust

Ta strona zawiera informacje na temat rejestrowania w Androidzie, przykład Rust AIDL, wyjaśnia, Zadzwoń do Rust z C i podaje instrukcje dla współpracy Rust/C++ z 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 liblogger i liblog_rust jako zależności:

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

Musisz dodać 2 zależności wymienione powyżej (liblogger i liblog_rust): wywołaj metodę init raz (w razie potrzeby możesz ją wywołać więcej niż raz) i komunikatów logu za pomocą dostarczonych makr. 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 aidl_interface. Musisz jednoznacznie włączyć backend Rust, ponieważ nie jest domyślnie włączone.

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 źródła Rust, więc działa jak inne źródło 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 w bibliotece wygenerowanej przez AIDL używany w rustlibs to nazwa modułu aidl_interface, po którym następuje ciąg -rust; W tym przypadku com.example.android.remoteservice-rust

Interfejs AIDL może zostać następnie odwołany w src/lib.rs w następujący 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 asynchronicznej wersji Rust AIDL

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

Kontynuując przykład z językiem RemoteService, wygenerowana biblioteka backendu AIDL zawiera interfejsy asynchroniczne, które mogą służyć do implementacji serwera asynchronicznego 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 rozpocząć w następujący 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że to też spowodować można uniknąć, tworząc środowisko wykonawcze tokio ręcznie zamiast używać #[tokio::main], a następnie wywołaj join_thread_pool poza metodą block_on.

Co więcej, generowana przez Rust biblioteka backendu zawiera interfejs, który umożliwia implementuję klienta asynchronicznego IRemoteServiceAsync dla środowiska RemoteService, który może należy wdrożyć w następujący 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

Określ plik libsimple_printer w: external/rust/simple_printer/libsimple_printer.rs w następujący 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

Podaj definicję słowa external/rust/c_hello_rust/main.c w ten sposób:

#include "simple_printer.h"

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

Podaj definicję słowa 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). Definiuje niezbędne definicje typów, które Rust może wygenerować biblioteka Rust cdylib, która podłącza się bezpośrednio do JNI Javy (JNIEnv, JClass, JString i tak dalej). W przeciwieństwie do wiązań C++, które generują codegen za pomocą cxx, Interoperacyjność Javy przez JNI nie wymaga kroku generowania kodu podczas budowy. Nie wymaga więc specjalnej pomocy w systemie kompilacji. Jawa wczytuje cdylib udostępniony przez Rust tak samo jak każda inna biblioteka natywna.

Wykorzystanie

Użytkowanie w kodzie Rust i Javy jest opisane w dokumencie jni Utwórz dokumentację. Proszę postępuj zgodnie z instrukcjami w artykule Pierwsze kroki. podany na stronie. 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 Song jest taka:

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

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

W bibliotece Java jest wymieniona biblioteka Rust jako zależność required. Dzięki temu będzie ona zainstalowana na urządzeniu wraz z biblioteką Java, chociaż nie jest to zależność od czasu kompilacji.

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

Możesz też dodać bibliotekę Rust do biblioteki AndroidManifest.xml dodaj ją do uses_libs w ten sposób:

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

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

Konto CXX zapewnia bezpieczne FFI między Rust a podzbiorem języka C++. Dokumentacja CXX zawiera dobre przykłady jej działania. Zalecamy najpierw zapoznanie się z nią. aby zapoznać się z biblioteką i sposobem, w jaki łączy ona języki C++ i Rust. 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 aby wywoływać kod C++ w języku Rust lub używać typów wspólnych między językami C++ i Rust, zdefiniuj drugi genrule (aby wygenerować nagłówek C++ zawierający 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ędzie cxxbridge jest używany powyżej do wygenerowania strony mostu w języku C++. libcxx_test_cpp Biblioteka statyczna jest używana jako zależność dla naszego pliku wykonywalnego w wersji 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. Na przykład definicja cxx_test.hpp zawiera te elementy:

#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;
}