Wzory Android Rust

Na tej stronie znajdziesz informacje o logowaniu w Androidzie, przykład AIDL w Rust, instrukcje wywoływania kodu Rust z C oraz instrukcje dotyczące współdziałania Rust i C++ za pomocą CXX.

Logowanie na Androidzie

Poniższy przykład pokazuje, jak rejestrować wiadomości 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 dodaj ten kod do źródła Rust:

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 musisz dodać 2 zależności pokazane powyżej (libloggerliblog_rust), wywołać metodę init raz (w razie potrzeby możesz wywołać ją więcej razy) i rejestrować wiadomości za pomocą podanych makr. Listę możliwych opcji konfiguracji znajdziesz w logger crate.

Pakiet logger udostępnia interfejs API do określania, co chcesz rejestrować. W zależności od tego, czy kod jest uruchamiany na urządzeniu czy na hoście (np. w ramach testu po stronie hosta), wiadomości są rejestrowane przy użyciu funkcji android_logger lub env_logger.

Przykład AIDL w Rust

W tej sekcji znajdziesz przykład użycia AIDL z Rust w stylu „Hello World”.

Zacznij od sekcji AIDL Overview (Omówienie AIDL) w przewodniku dla deweloperów Androida i utwórz plik external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl z następującą treś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 w Rust, więc działa jak inne generatory kodu źródłowego w Rust i tworzy bibliotekę w Rust. Wygenerowany moduł biblioteki Rust może być używany przez inne moduły Rust jako zależność. Jako przykład użycia wygenerowanej biblioteki jako zależności w pliku external/rust/binder_example/Android.bp można zdefiniować rust_library w ten sposób:

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 przypadku biblioteki wygenerowanej przez AIDL używanej w rustlibs to aidl_interface nazwa modułu, po której następuje -rust; w tym przypadku jest to com.example.android.remoteservice-rust.

Do interfejsu AIDL można się odwołać w 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 binarnym pliku 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 AIDL w Rust

W tej sekcji znajdziesz przykład użycia AIDL z asynchronicznym Rustem w stylu „Hello World”.

W przypadku przykładu RemoteService wygenerowana biblioteka backendu AIDL zawiera interfejsy asynchroniczne, których można użyć do wdrożenia asynchronicznego serwera dla interfejsu AIDL RemoteService.

Wygenerowany asynchroniczny interfejs serwera IRemoteServiceAsyncServer można zaimplementować w ten 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 block_in_place jest potrzebny do opuszczenia kontekstu asynchronicznego, co pozwala join_thread_pool na wewnętrzne użycie block_on. Dzieje się tak, ponieważ #[tokio::main] otacza kod wywołaniem funkcji block_on, a join_thread_pool może wywoływać block_on podczas obsługi transakcji przychodzącej. Dzwonienie do block_on z poziomu block_on wywołuje panikę. Można tego uniknąć, ręcznie tworząc środowisko wykonawcze tokio zamiast używać #[tokio::main], a następnie wywołując join_thread_pool poza metodą block_on.

Biblioteka wygenerowana przez backend Rust zawiera interfejs, który umożliwia implementację klienta asynchronicznego IRemoteServiceAsync dla RemoteService. Można to zrobić 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),
    }
}

Wywoływanie kodu w Rust z C

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

Przykładowa biblioteka Rust

Zdefiniuj plik libsimple_printer wexternal/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 definiować nagłówki, które mogą być używane przez zależne moduły C, więc zdefiniuj nagłówek external/rust/simple_printer/simple_printer.h w ten sposób:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Zdefiniuj external/rust/simple_printer/Android.bp w sposób widoczny poniżej:

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 C w formacie binarnym

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 utwórz projekt, wywołując polecenie m c_hello_rust.

Współdziałanie języków Rust i Java

Pakiet jni zapewnia interoperacyjność Rusta z Javą za pomocą interfejsu Java Native Interface (JNI). Określa niezbędne definicje typów dla Rusta, aby wygenerować bibliotekę Rusta cdylib, która jest bezpośrednio podłączana do JNI Javy (JNIEnv, JClass, JString itp.). W odróżnieniu od powiązań C++, które wykonują generowanie kodu za pomocą cxx, interoperacyjność Javy za pomocą JNI nie wymaga kroku generowania kodu podczas kompilacji. Dlatego nie wymaga specjalnej obsługi przez system kompilacji. Kod Java wczytuje cdylib dostarczony przez Rusta jak każdą inną bibliotekę natywną.

Wykorzystanie

Użycie w kodzie Rust i Java jest opisane w jnidokumentacji pakietu. Postępuj zgodnie z przykładem Wprowadzenie. Po napisaniu src/lib.rs wróć na tę stronę, aby dowiedzieć się, jak utworzyć bibliotekę za pomocą systemu kompilacji Androida.

Definicja kompilacji

Java wymaga, aby biblioteka Rust była udostępniana jako cdylib, dzięki czemu można ją dynamicznie wczytywać. Definicja biblioteki Rust w Soong wygląda tak:

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

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

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

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

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

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

Współdziałanie Rust–C++ za pomocą CXX

Pakiet CXX zapewnia bezpieczny interfejs FFI między Rustem a podzbiorem C++. Dokumentacja CXX zawiera dobre przykłady jego działania. Zalecamy najpierw zapoznać się z nią, aby poznać bibliotekę i sposób, w jaki łączy ona C++ i Rust. Poniższy przykład pokazuje, jak używać tego interfejsu w Androidzie.

Aby CXX wygenerował kod C++, do którego wywołania Rust używa, zdefiniuj genrule, aby wywołać CXX, i cc_library_static, aby połączyć to w bibliotekę. Jeśli planujesz, że kod C++ będzie wywoływać kod Rust lub używać typów wspólnych dla C++ i Rust, zdefiniuj drugą regułę 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"],
}

Powyżej użyto cxxbridgenarzędzia do wygenerowania części mostu w C++. Następnie używamy libcxx_test_cppbiblioteki statycznej jako zależności dla naszego pliku wykonywalnego Rust:

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

W plikach .cpp.hpp zdefiniuj funkcje C++ w dowolny sposób, używając w razie potrzeby typów opakowania 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);

Gdy 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ć tej funkcji w Rust, zdefiniuj most CXX w sposób podany 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;
}