Android-Rostmuster

Diese Seite enthält Informationen zu Android-Protokollierung, ein Rust-AIDL-Beispiel, eine Anleitung zum Aufrufen von Rust aus C und eine Anleitung für die Interoperabilität von Rust/C++ mit CXX.

Android-Logging

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

Fügen Sie im Modul Android.bp 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-Quelldatei den folgenden Code hinzu:

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

Fügen Sie dazu die beiden oben genannten Abhängigkeiten (liblogger und liblog_rust) hinzu, rufen Sie die Methode init einmal auf (bei Bedarf auch mehrmals) und loggen Sie Meldungen mit den bereitgestellten Makros. Eine Liste der möglichen Konfigurationsoptionen finden Sie in der logger-Krate.

Die „logger“-Kiste bietet eine API zum Definieren der zu protokollierenden Daten. Je nachdem, ob der Code auf dem Gerät oder auf dem Host ausgeführt wird (z. B. bei einem hostseitigen Test), 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.

Verwenden Sie als Ausgangspunkt den Abschnitt AIDL-Übersicht im Android-Entwicklerhandbuch und erstellen Sie external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl mit den folgenden Inhalten 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-Back-End explizit aktivieren, da es nicht standardmäßig 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-Quellcodegenerator. Es funktioniert also wie andere Rust-Quellcodegeneratoren und erzeugt eine Rust-Bibliothek. Das erzeugte Rust-Bibliotheksmodul kann von anderen Rust-Modulen als Abhängigkeit verwendet werden. Als Beispiel für die Verwendung der erstellten Bibliothek als Abhängigkeit kann eine rust_library in external/rust/binder_example/Android.bp so definiert werden:

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

Das Format des Modulnamens für die AIDL-generierte Bibliothek, die in rustlibs verwendet wird, ist der Modulname aidl_interface gefolgt von -rust; in diesem Fall com.example.android.remoteservice-rust.

Auf die AIDL-Schnittstelle kann dann in src/lib.rs so verwiesen 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(())
    }
}

Abschließend starten Sie den Dienst in einem Rust-Binärprogramm, 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()
}

Beispiel für asynchrone Rust-AIDL

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

Im Beispiel RemoteService enthält die generierte AIDL-Backendbibliothek asynchrone Schnittstellen, mit denen eine asynchrone Serverimplementierung für die AIDL-Schnittstelle RemoteService implementiert werden kann.

Die generierte asynchrone Serverschnittstelle IRemoteServiceAsyncServer kann so 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 asynchrone Serverimplementierung kann so 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 erforderlich ist, um den asynchronen Kontext zu verlassen, damit join_thread_pool intern block_on verwenden kann. Das liegt daran, dass #[tokio::main] den Code in einen Aufruf von block_on einbettet und join_thread_pool block_on möglicherweise beim Bearbeiten einer eingehenden Transaktion aufruft. Der Aufruf eines block_on innerhalb eines block_on führt zu einer Panik. Dies lässt sich auch vermeiden, indem Sie die Tokio-Laufzeit manuell statt #[tokio::main] erstellen und dann join_thread_pool außerhalb der Methode block_on aufrufen.

Darüber hinaus enthält die mit dem Rut-Back-End generierte Bibliothek eine Schnittstelle, die die Implementierung eines asynchronen Clients IRemoteServiceAsync für RemoteService ermöglicht, der so 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::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),
    }
}

Rust aus C aufrufen

In diesem Beispiel wird gezeigt, wie Rust aus C aufgerufen wird.

Beispiel für eine Rust-Bibliothek

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

//! 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 von den abhängigen C-Modulen abgerufen werden können. Definieren Sie den external/rust/simple_printer/simple_printer.h-Header also so:

#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 export_include_dirs so cc_binary knows where the headers are.
    export_include_dirs: ["."],
}

Beispiel für eine C-Binärdatei

Definieren Sie external/rust/c_hello_rust/main.c so:

#include "simple_printer.h"

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

Definieren Sie external/rust/c_hello_rust/Android.bp so:

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

Zuletzt erstellen Sie den Build durch Aufrufen von m c_hello_rust.

Rust-Java-Interoperabilität

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

Nutzung

Informationen zur Verwendung in Rust- und Java-Code finden Sie in der jni-Crate-Dokumentation. Folgen Sie dem dort zur Verfügung gestellten Beispiel Erste Schritte. Nachdem Sie src/lib.rs eingegeben 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, dass die Rust-Bibliothek als cdylib bereitgestellt wird, damit sie dynamisch geladen werden kann. Die Definition der Rust-Bibliothek 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 sie zusammen mit der Java-Bibliothek auf dem Gerät installiert wird, auch wenn sie keine Abhängigkeit zur Buildzeit ist:

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

Wenn Sie die Rust-Bibliothek in einer AndroidManifest.xml-Datei einbinden müssen, fügen Sie sie uses_libs so hinzu:

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

Rust-C++-Interoperabilität mit CXX

Die CXX-Kiste bietet ein sicheres FFI zwischen Rust und einem Teil von C++. Die CXX-Dokumentation enthält gute Beispiele für die allgemeine Funktionsweise. Wir empfehlen, sie zuerst zu lesen, um sich mit der Bibliothek und ihrer Verbindung zwischen C++ und Rust vertraut zu machen. Das folgende Beispiel zeigt, wie sie in Android verwendet wird.

Wenn CXX den C++-Code generieren soll, der von Rust aufgerufen wird, definieren Sie eine genrule, um CXX aufzurufen, und eine cc_library_static, um sie in einer Bibliothek zu bündeln. Wenn Sie planen, dass C++ Rust-Code aufruft oder Typen verwenden, die von C++ und Rust gemeinsam genutzt werden, definieren Sie eine zweite Genrule, 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 cxxbridge-Tool wird oben verwendet, um die C++-Seite der Bridge zu generieren. Als Nächstes wird die statische Bibliothek libcxx_test_cpp als Abhängigkeit für unsere Rust-Programmdatei verwendet:

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

Definieren Sie in den Dateien .cpp und .hpp die C++-Funktionen nach Belieben und verwenden Sie dabei die CXX-Wrappertypen. Eine cxx_test.hpp-Definition enthält beispielsweise Folgendes:

#pragma once

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

int greet(rust::Str greetee);

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 diese aus Rust zu verwenden, definieren Sie eine CXX-Bridge 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;
}