Android-Rust-Muster

Diese Seite enthält Informationen zur Android-Protokollierung, ein Rust-AIDL-Beispiel, eine Anleitung zum Aufrufen von Rust aus C und eine Anleitung zur Rust/C++-Interop 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 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 Ihre Rust-Quelle den folgenden Code ein:

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 also die beiden oben gezeigten Abhängigkeiten (liblogger und liblog_rust) hinzu, rufen Sie die Methode init einmal auf (bei Bedarf auch mehrmals) und protokollieren Sie Nachrichten mit den bereitgestellten Makros. Eine Liste der möglichen Konfigurationsoptionen finden Sie im Logger-Crate.

Die Logger-Crate bietet eine API zum Definieren der zu protokollierenden Elemente. Je nachdem, 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 „Hello World“-Beispiel für die Verwendung von AIDL mit Rust.

Erstellen Sie mit dem Abschnitt AIDL Overview (AIDL-Übersicht) im Android Developer Guide als Ausgangspunkt die Datei external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl mit folgendem 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 im external/rust/binder_example/aidl/Android.bp-Modul das aidl_interface-Modul. Sie müssen das Rust-Backend 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-Quellcode-Generator. Es funktioniert also wie andere Rust-Quellcode-Generatoren und erzeugt eine Rust-Bibliothek. Das erstellte Rust-Bibliotheksmodul kann von anderen Rust-Modulen als Abhängigkeit verwendet werden. Ein Beispiel für die Verwendung der erstellten Bibliothek als Abhängigkeit: Eine rust_library kann 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 Modulnamensformat für die AIDL-generierte Bibliothek, die in rustlibs verwendet wird, ist der aidl_interface-Modulname 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(())
    }
}

Starten Sie den Dienst schließlich 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 asynchrones Rust-AIDL

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

Im Beispiel RemoteService enthält die generierte AIDL-Backend-Bibliothek 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. Dadurch kann join_thread_pool intern block_on verwenden. Das liegt daran, dass #[tokio::main] den Code in einen Aufruf von block_on einbettet und join_thread_pool möglicherweise block_on aufruft, wenn eine eingehende Transaktion verarbeitet wird. Wenn Sie eine block_on innerhalb einer block_on aufrufen, führt das zu einem Panic. Das lässt sich auch vermeiden, indem Sie die Tokio-Laufzeit manuell erstellen, anstatt #[tokio::main] zu verwenden, und dann join_thread_pool außerhalb der block_on-Methode aufrufen.

Außerdem enthält die generierte Bibliothek für das Rust-Backend eine Schnittstelle, mit der ein asynchroner Client IRemoteServiceAsync für RemoteService implementiert werden kann. Das kann so aussehen:

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-Funktionen aus C aufrufen

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

Beispiel für eine Rust-Bibliothek

Definieren Sie die Datei libsimple_printer inexternal/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!");
}

In der Rust-Bibliothek müssen Header definiert werden, die von den abhängigen C-Modulen abgerufen werden können. Definieren Sie den external/rust/simple_printer/simple_printer.h-Header so:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

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

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 ein C-Binärprogramm

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

Rufen Sie zum Schluss m c_hello_rust auf, um den Build zu erstellen.

Rust-Java-Interoperabilität

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

Nutzung

Die Verwendung in Rust- und Java-Code wird in der jni-Crate-Dokumentation beschrieben. Folgen Sie dem Beispiel für den Einstieg, das dort bereitgestellt wird. 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

Für Java muss die Rust-Bibliothek als cdylib bereitgestellt werden, damit sie dynamisch geladen werden kann. Die Rust-Bibliotheksdefinition in Soong lautet so:

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

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

In der Java-Bibliothek wird die Rust-Bibliothek als required-Abhängigkeit aufgeführt. Dadurch wird sichergestellt, dass sie zusammen mit der Java-Bibliothek auf dem Gerät installiert wird, obwohl sie keine Build-Zeit-Abhängigkeit ist:

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

Wenn Sie die Rust-Bibliothek in eine AndroidManifest.xml-Datei einfügen müssen, fügen Sie die Bibliothek alternativ so in uses_libs ein:

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

Rust–C++-Interop mit CXX

Das CXX-Crate bietet sicheres FFI zwischen Rust und einer Teilmenge 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 der Art und Weise vertraut zu machen, wie sie C++ und Rust verbindet. Das folgende Beispiel zeigt, wie es in Android verwendet wird.

Damit CXX den C++-Code generiert, den Rust aufruft, definieren Sie genrule, um CXX aufzurufen, und cc_library_static, um den Code in einer Bibliothek zu bündeln. Wenn Sie planen, dass C++-Code Rust-Code aufruft oder Typen verwendet, die zwischen C++ und Rust gemeinsam genutzt werden, definieren Sie eine zweite genrule, um einen C++-Header mit den Rust-Bindungen zu generieren.

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. Die statische Bibliothek libcxx_test_cpp wird als Nächstes 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 die C++-Funktionen in den Dateien .cpp und .hpp nach Bedarf und verwenden Sie dabei 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);

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

Wenn Sie diese Funktion in Rust verwenden möchten, 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;
}