Android Rust-Muster

Diese Seite enthält Informationen zur Android-Protokollierung , stellt ein Rust-AIDL-Beispiel bereit, erklärt Ihnen, wie Sie Rust aus C aufrufen , und bietet Anweisungen für Rust/C++-Interop mit CXX .

Android-Protokollierung

Das folgende Beispiel zeigt, wie Sie Nachrichten in logcat (auf dem Gerät) oder stdout (auf dem Host) protokollieren können.

Fügen Sie in Ihrem Android.bp Modul liblogger und liblog_rust als Abhängigkeiten hinzu:

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

Fügen Sie als Nächstes in Ihrer Rust-Quelle diesen Code hinzu:

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

Das heißt, fügen Sie die beiden oben gezeigten Abhängigkeiten hinzu ( liblogger und liblog_rust ), rufen Sie die init Methode einmal auf (Sie können sie bei Bedarf auch mehrmals aufrufen) und protokollieren Sie Nachrichten mithilfe der bereitgestellten Makros. Eine Liste möglicher Konfigurationsoptionen finden Sie in der Logger-Kiste .

Die Logger-Kiste bietet eine API zum Definieren, was Sie protokollieren möchten. Abhängig davon, ob der Code auf dem Gerät oder auf dem Host ausgeführt wird (z. B. als Teil eines hostseitigen Tests), werden Nachrichten entweder mit android_logger oder env_logger protokolliert.

Rust AIDL-Beispiel

Dieser Abschnitt enthält ein Beispiel im Hello World-Stil für die Verwendung von AIDL mit Rust.

Erstellen Sie ausgehend vom Abschnitt AIDL-Übersicht des Android-Entwicklerhandbuchs als Ausgangspunkt external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl mit dem folgenden Inhalt in der Datei 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);
}

Definieren Sie dann in der Datei external/rust/binder_example/aidl/Android.bp das Modul aidl_interface . Sie müssen das Rust-Backend explizit aktivieren , da es standardmäßig nicht aktiviert ist.

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

Das AIDL-Backend ist ein Rust-Quellgenerator, funktioniert also wie andere Rust-Quellgeneratoren und erstellt eine Rust-Bibliothek. Das erzeugte Rust-Bibliotheksmodul kann von anderen Rust-Modulen als Abhängigkeit verwendet werden. Als Beispiel für die Verwendung der produzierten Bibliothek als Abhängigkeit kann eine rust_library wie folgt in external/rust/binder_example/Android.bp definiert werden:

rust_library {
    name: "libmyservice",
    srcs: ["src/lib.rs"],
    crate_name: "myservice",
    rustlibs: [
        "com.example.android.remoteservice-rust",
        "libbinder_rs",
    ],
}

Beachten Sie, dass das Modulnamenformat für die in rustlibs verwendete AIDL-generierte Bibliothek der Modulname aidl_interface gefolgt von -rust ist; in diesem Fall com.example.android.remoteservice-rust .

Die AIDL-Schnittstelle kann dann in src/lib.rs wie folgt referenziert werden:

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

Starten Sie abschließend den Dienst in einer Rust-Binärdatei, wie unten gezeigt:

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

Async Rust AIDL-Beispiel

Dieser Abschnitt enthält ein Beispiel im Hello World-Stil für die Verwendung von AIDL mit asynchronem Rust.

In Fortsetzung des RemoteService Beispiels umfasst die generierte AIDL-Backend-Bibliothek asynchrone Schnittstellen, die zum Implementieren einer asynchronen Serverimplementierung für die AIDL-Schnittstelle RemoteService verwendet werden können.

Die generierte asynchrone Serverschnittstelle IRemoteServiceAsyncServer kann wie folgt implementiert werden:

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

Die Async-Server-Implementierung kann wie folgt gestartet werden:

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

Beachten Sie, dass block_in_place benötigt wird, um den asynchronen Kontext zu verlassen, der es join_thread_pool ermöglicht, block_on intern zu verwenden. Dies liegt daran, dass #[tokio::main] den Code in einen Aufruf von block_on einschließt und join_thread_pool möglicherweise block_on aufruft, wenn eine eingehende Transaktion verarbeitet wird. Der Aufruf eines block_on aus einem block_on heraus führt zu einer Panik. Dies könnte auch vermieden werden, indem die Tokio-Laufzeit manuell erstellt wird, anstatt #[tokio::main] zu verwenden und dann join_thread_pool außerhalb der block_on Methode aufzurufen.

Darüber hinaus enthält die vom Rust-Backend generierte Bibliothek eine Schnittstelle, die die Implementierung eines asynchronen Clients IRemoteServiceAsync für RemoteService ermöglicht, der wie folgt implementiert werden kann:

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

Rufen Sie Rust von C aus auf

Dieses Beispiel zeigt, wie man Rust von C aus aufruft.

Beispiel einer Rust-Bibliothek

Definieren Sie die Datei libsimple_printer in external/rust/simple_printer/libsimple_printer.rs wie folgt:

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

Die Rust-Bibliothek muss Header definieren, die die abhängigen C-Module abrufen können. Definieren Sie daher den Header external/rust/simple_printer/simple_printer.h wie folgt:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Definieren Sie external/rust/simple_printer/Android.bp wie hier gezeigt:

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

Beispiel C-Binärdatei

Definieren Sie external/rust/c_hello_rust/main.c wie folgt:

#include "simple_printer.h"

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

Definieren Sie external/rust/c_hello_rust/Android.bp wie folgt:

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

Zum Schluss erstellen Sie den Build, indem Sie m c_hello_rust aufrufen.

Rust-Java-Interop

Die jni Kiste bietet Rust-Interoperabilität mit Java über das Java Native Interface (JNI). Es definiert die notwendigen Typdefinitionen für Rust, um eine Rust- cdylib Bibliothek zu erstellen, die sich direkt in Javas JNI ( JNIEnv , JClass , JString usw.) einfügt. Im Gegensatz zu C++-Bindungen, die die Codegenerierung über cxx durchführen, erfordert die Java-Interoperabilität über die JNI keinen Codegenerierungsschritt während eines Builds. Daher ist keine spezielle Build-System-Unterstützung erforderlich. Der Java-Code lädt die von Rust bereitgestellte cdylib wie jede andere native Bibliothek.

Verwendung

Die Verwendung in Rust- und Java-Code wird in der jni Crate-Dokumentation behandelt. Bitte folgen Sie dem dort bereitgestellten „Erste Schritte“ -Beispiel. Nachdem Sie src/lib.rs geschrieben haben, kehren Sie zu dieser Seite zurück, um zu erfahren, wie Sie die Bibliothek mit dem Build-System von Android erstellen.

Build-Definition

Java erfordert die Bereitstellung der Rust-Bibliothek als cdylib , damit sie dynamisch geladen werden kann. Die Rust-Bibliotheksdefinition in Soong lautet wie folgt:

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

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

Die Java-Bibliothek listet die Rust-Bibliothek als required Abhängigkeit auf; Dadurch wird sichergestellt, dass es zusammen mit der Java-Bibliothek auf dem Gerät installiert wird, auch wenn es sich nicht um eine Build-Zeit-Abhängigkeit handelt:

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

Wenn Sie alternativ die Rust-Bibliothek in eine AndroidManifest.xml Datei einschließen müssen, fügen Sie die Bibliothek wie folgt zu uses_libs hinzu:

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

Rust–C++-Interop mit CXX

Die CXX- Kiste bietet sichere FFI zwischen Rust und einer Teilmenge von C++. Die CXX-Dokumentation enthält gute Beispiele dafür, wie sie im Allgemeinen funktioniert. Wir empfehlen, sie zuerst zu lesen, um sich mit der Bibliothek und der Art und Weise, wie sie C++ und Rust verbindet, vertraut zu machen. Das folgende Beispiel zeigt, wie es in Android verwendet wird.

Damit CXX den C++-Code generiert, den Rust aufruft, definieren Sie eine genrule zum Aufrufen von CXX und eine cc_library_static , um diesen in einer Bibliothek zu bündeln. Wenn Sie planen, dass C++ Rust-Code aufruft oder von C++ und Rust gemeinsam genutzte Typen verwendet, definieren Sie eine zweite Regel (um einen C++-Header zu generieren, der die Rust-Bindungen enthält).

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

Das Tool cxxbridge wird oben verwendet, um die C++-Seite der Brücke zu generieren. Als nächstes wird die statische Bibliothek libcxx_test_cpp als Abhängigkeit für unsere ausführbare Rust-Datei verwendet:

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

Definieren Sie in den .cpp und .hpp Dateien die C++-Funktionen nach Ihren Wünschen und verwenden Sie dabei nach Bedarf die CXX-Wrapper-Typen . Eine cxx_test.hpp Definition enthält beispielsweise Folgendes:

#pragma once

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

int greet(rust::Str greetee);

Während cxx_test.cpp enthält

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

#include <iostream>

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

Um dies von Rust aus zu verwenden, definieren Sie eine CXX-Brücke wie unten in 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;
}