Wzory Android Rust

Ta strona zawiera informacje na temat rejestrowania Androida , dostarcza przykładu Rust AIDL , mówi jak wywołać Rust z C i dostarcza instrukcje dla Rust/C++ Interop przy użyciu CXX .

Rejestrowanie Androida

Poniższy przykład pokazuje, jak można rejestrować komunikaty w logcat (na urządzeniu) lub stdout (na hoście).

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 Rusta dodaj ten kod:

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

Oznacza to, że dodaj dwie zależności pokazane powyżej ( liblogger i liblog_rust ), wywołaj metodę init raz (możesz ją wywołać więcej niż raz, jeśli to konieczne) i zarejestruj wiadomości za pomocą dostarczonych makr. Listę możliwych opcji konfiguracji można znaleźć na opakowaniu rejestratora .

Skrzynka rejestratora zapewnia interfejs API umożliwiający zdefiniowanie tego, co chcesz rejestrować. W zależności od tego, czy kod działa na urządzeniu, czy na hoście (np. w ramach testu po stronie hosta), komunikaty są rejestrowane przy użyciu android_logger lub env_logger .

Przykład rdzy AIDL

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

Korzystając z sekcji Przegląd AIDL w Przewodniku programisty Androida jako punktu wyjścia, utwórz plik external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl z następującą 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 jawnie włączyć backend Rusta, 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 jest generatorem źródeł Rusta, więc działa jak inne generatory źródeł Rusta i tworzy bibliotekę Rust. Wytworzony moduł biblioteki Rusta może być używany przez inne moduły Rusta w ramach zależności. Jako przykład wykorzystania utworzonej biblioteki jako zależności, rust_library można zdefiniować w następujący sposób w 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",
    ],
}

Należy zauważyć, że format nazwy modułu biblioteki generowanej przez AIDL używanej w rustlibs to nazwa modułu aidl_interface , po której następuje -rust ; w tym przypadku com.example.android.remoteservice-rust .

Do interfejsu AIDL można następnie odwoływać się 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 Async Rust AIDL

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

Kontynuując przykład RemoteService , wygenerowana biblioteka zaplecza AIDL zawiera interfejsy asynchroniczne, których można użyć do zaimplementowania implementacji serwera asynchronicznego dla interfejsu AIDL RemoteService .

Wygenerowany interfejs serwera asynchronicznego IRemoteServiceAsyncServer można zaimplementować 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 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();
    });
}

Zauważ, że block_in_place jest potrzebne, aby opuścić kontekst asynchroniczny, który pozwala join_thread_pool na wewnętrzne używanie block_on . Dzieje się tak, ponieważ #[tokio::main] zawija kod w wywołaniu block_on , a join_thread_pool może wywołać block_on podczas obsługi transakcji przychodzącej. Wywołanie block_on z poziomu block_on powoduje panikę. Można tego również uniknąć, budując ręcznie środowisko wykonawcze Tokio zamiast używać #[tokio::main] , a następnie wywołując join_thread_pool poza metodą block_on .

Co więcej, biblioteka wygenerowana przez rdzę zawiera interfejs, który umożliwia implementację klienta asynchronicznego IRemoteServiceAsync dla RemoteService , który można zaimplementować 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::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),
    }
}

Dzwonienie do Rusta z C

Ten przykład pokazuje, jak wywołać Rust z C.

Przykładowa biblioteka Rust

Zdefiniuj plik libsimple_printer w pliku 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 definiować nagłówki, które mogą pobierać zależne moduły C, dlatego zdefiniuj nagłówek external/rust/simple_printer/simple_printer.h w następujący sposób:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Zdefiniuj external/rust/simple_printer/Android.bp jak widzisz tutaj:

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

Przykład C. plik binarny

Zdefiniuj external/rust/c_hello_rust/main.c w następujący sposób:

#include "simple_printer.h"

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

Zdefiniuj external/rust/c_hello_rust/Android.bp w następujący sposób:

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

Na koniec zbuduj, wywołując m c_hello_rust .

Współpraca Rust-Java

Skrzynka jni zapewnia interoperacyjność Rusta z Javą poprzez Java Native Interface (JNI). Definiuje niezbędne definicje typów dla Rusta, aby utworzyć bibliotekę cdylib Rusta, którą można podłączyć bezpośrednio do JNI Javy ( JNIEnv , JClass , JString i tak dalej). W przeciwieństwie do powiązań C++, które wykonują kodowanie za pomocą cxx , interoperacyjność Java za pośrednictwem JNI nie wymaga etapu generowania kodu podczas kompilacji. Dlatego nie potrzebuje specjalnego wsparcia systemu kompilacji. Kod Java ładuje dostarczony przez Rusta cdylib jak każda inna biblioteka natywna.

Stosowanie

Użycie zarówno w kodzie Rust, jak i Java jest omówione w dokumentacji jni crate . Postępuj zgodnie z podanym tam przykładem wprowadzającym . Po napisaniu src/lib.rs wróć na tę stronę, aby dowiedzieć się, jak zbudować bibliotekę za pomocą systemu kompilacji Androida.

Definicja kompilacji

Java wymaga, aby biblioteka Rust była dostarczona w formie cdylib , aby można było ją ładować dynamicznie. Definicja biblioteki Rust w Soong jest następująca:

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 required zależność; gwarantuje to, że zostanie zainstalowany na urządzeniu wraz z biblioteką Java, nawet jeśli nie jest to zależność w czasie kompilacji:

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

Alternatywnie, jeśli musisz dołączyć bibliotekę Rust do pliku AndroidManifest.xml , dodaj bibliotekę do uses_libs w następujący sposób:

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

Współpraca Rust–C++ przy użyciu CXX

Skrzynia CXX zapewnia bezpieczną FFI pomiędzy Rustem a podzbiorem C++. Dokumentacja CXX zawiera dobre przykłady tego, jak to działa ogólnie i sugerujemy przeczytanie jej w pierwszej kolejności, aby zapoznać się z biblioteką i sposobem, w jaki łączy ona C++ i Rust. Poniższy przykład pokazuje, jak go używać w systemie Android.

Aby CXX wygenerował kod C++, do którego wywołuje Rust, zdefiniuj genrule do wywołania CXX i cc_library_static , aby spakować ją do biblioteki. Jeśli planujesz, aby C++ wywoływał kod Rust lub używał typów współdzielonych pomiędzy C++ i Rust, zdefiniuj drugą regułę genrulną (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 zostało użyte powyżej do wygenerowania strony mostu w języku C++. Biblioteka statyczna libcxx_test_cpp jest następnie 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, używając opcjonalnie typów opakowań CXX . Na przykład definicja cxx_test.hpp zawiera następujące elementy:

#pragma once

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

int greet(rust::Str greetee);

Chociaż cxx_test.cpp zawiera

#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 Rusta, zdefiniuj most CXX jak poniżej w 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;
}