Mẫu Rust cho Android

Trang này chứa thông tin về Ghi nhật ký Android, cung cấp ví dụ về AIDL Rust, cho bạn biết cách gọi Rust từ C và cung cấp hướng dẫn về Khả năng tương tác Rust/C++ bằng CXX.

Ghi nhật ký Android

Ví dụ sau đây cho thấy cách bạn có thể ghi nhật ký thông báo vào logcat (trên thiết bị) hoặc stdout (trên máy chủ lưu trữ).

Trong mô-đun Android.bp, hãy thêm libloggerliblog_rust làm phần phụ thuộc:

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

Tiếp theo, trong nguồn Rust, hãy thêm mã sau:

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

Tức là hãy thêm 2 phần phụ thuộc nêu trên (libloggerliblog_rust), gọi phương thức init một lần (bạn có thể gọi phương thức này nhiều lần nếu cần) và ghi lại thông báo bằng cách sử dụng các macro được cung cấp. Hãy xem logger crate để biết danh sách các lựa chọn cấu hình có thể có.

Thùng ghi nhật ký cung cấp một API để xác định nội dung bạn muốn ghi nhật ký. Tuỳ thuộc vào việc mã đang chạy trên thiết bị hay trên máy chủ lưu trữ (chẳng hạn như một phần của kiểm thử phía máy chủ lưu trữ), các thông báo sẽ được ghi nhật ký bằng android_logger hoặc env_logger.

Ví dụ về AIDL Rust

Phần này cung cấp một ví dụ theo kiểu Xin chào thế giới về cách sử dụng AIDL với Rust.

Sử dụng phần Tổng quan về AIDL trong Hướng dẫn dành cho nhà phát triển Android làm điểm bắt đầu, hãy tạo external/rust/binder_example/aidl/com/example/android/IRemoteService.aidl với nội dung sau trong tệp 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);
}

Sau đó, trong tệp external/rust/binder_example/aidl/Android.bp, hãy xác định mô-đun aidl_interface. Bạn phải bật một cách rõ ràng phần phụ trợ Rust vì phần phụ trợ này không được bật theo mặc định.

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

Phần phụ trợ AIDL là một trình tạo nguồn Rust, vì vậy, nó hoạt động giống như các trình tạo nguồn Rust khác và tạo ra một thư viện Rust. Các mô-đun thư viện Rust được tạo có thể được các mô-đun Rust khác sử dụng làm phần phụ thuộc. Ví dụ về cách sử dụng thư viện đã tạo làm phần phụ thuộc, bạn có thể xác định rust_library như sau trong 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",
    ],
}

Xin lưu ý rằng định dạng tên mô-đun cho thư viện do AIDL tạo được dùng trong rustlibs là tên mô-đun aidl_interface, theo sau là -rust; trong trường hợp này là com.example.android.remoteservice-rust.

Sau đó, bạn có thể tham chiếu giao diện AIDL trong src/lib.rs như sau:

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

Cuối cùng, hãy bắt đầu dịch vụ trong một tệp nhị phân Rust như minh hoạ dưới đây:

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

Ví dụ về AIDL Rust không đồng bộ

Phần này cung cấp một ví dụ theo kiểu Hello World về cách sử dụng AIDL với Rust không đồng bộ.

Tiếp tục với ví dụ về RemoteService, thư viện phụ trợ AIDL đã tạo bao gồm các giao diện không đồng bộ có thể dùng để triển khai một phương thức triển khai máy chủ không đồng bộ cho giao diện AIDL RemoteService.

Bạn có thể triển khai giao diện máy chủ không đồng bộ IRemoteServiceAsyncServer đã tạo như sau:

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

Bạn có thể bắt đầu triển khai máy chủ không đồng bộ như sau:

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

Xin lưu ý rằng bạn cần block_in_place để rời khỏi ngữ cảnh không đồng bộ, cho phép join_thread_pool sử dụng block_on nội bộ. Điều này là do #[tokio::main] bao bọc mã trong một lệnh gọi đến block_onjoin_thread_pool có thể gọi block_on khi xử lý một giao dịch đến. Việc gọi block_on từ bên trong block_on sẽ dẫn đến lỗi hoảng loạn. Bạn cũng có thể tránh điều này bằng cách tạo thời gian chạy tokio theo cách thủ công thay vì dùng #[tokio::main], rồi gọi join_thread_pool bên ngoài phương thức block_on.

Ngoài ra, thư viện do phần phụ trợ Rust tạo bao gồm một giao diện cho phép triển khai ứng dụng IRemoteServiceAsync không đồng bộ cho RemoteService. Bạn có thể triển khai ứng dụng này như sau:

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

Gọi Rust từ C

Ví dụ này minh hoạ cách gọi Rust từ C.

Thư viện Rust mẫu

Xác định tệp libsimple_printer trong external/rust/simple_printer/libsimple_printer.rs như sau:

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

Thư viện Rust phải xác định các tiêu đề mà các mô-đun C phụ thuộc có thể kéo vào, vì vậy, hãy xác định tiêu đề external/rust/simple_printer/simple_printer.h như sau:

#ifndef SIMPLE_PRINTER_H
#define SIMPLE_PRINTER_H

void print_c_hello_rust();


#endif

Xác định external/rust/simple_printer/Android.bp như bạn thấy ở đây:

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

Ví dụ về tệp nhị phân C

Xác định external/rust/c_hello_rust/main.c như sau:

#include "simple_printer.h"

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

Xác định external/rust/c_hello_rust/Android.bp như sau:

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

Cuối cùng, hãy tạo bằng cách gọi m c_hello_rust.

Khả năng tương tác Rust-Java

Thùng jni cung cấp khả năng tương tác Rust với Java thông qua Giao diện gốc của Java (JNI). Thư viện này xác định các định nghĩa kiểu cần thiết để Rust tạo ra một thư viện cdylib Rust cắm trực tiếp vào JNI của Java (JNIEnv, JClass, JString, v.v.). Không giống như các liên kết C++ thực hiện codegen thông qua cxx, khả năng tương tác Java thông qua JNI không yêu cầu bước tạo mã trong quá trình tạo bản dựng. Do đó, bạn không cần có sự hỗ trợ đặc biệt của hệ thống bản dựng. Mã Java tải cdylib do Rust cung cấp giống như mọi thư viện gốc khác.

Cách sử dụng

Cách sử dụng trong cả mã Rust và Java đều được đề cập trong tài liệu về crate jni. Vui lòng làm theo ví dụ Bắt đầu được cung cấp tại đó. Sau khi bạn viết src/lib.rs, hãy quay lại trang này để tìm hiểu cách tạo thư viện bằng hệ thống xây dựng của Android.

Định nghĩa bản dựng

Java yêu cầu cung cấp thư viện Rust dưới dạng cdylib để có thể tải động. Định nghĩa thư viện Rust trong Soong như sau:

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

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

Thư viện Java liệt kê thư viện Rust dưới dạng một phần phụ thuộc required; điều này đảm bảo rằng thư viện Rust được cài đặt vào thiết bị cùng với thư viện Java ngay cả khi đó không phải là phần phụ thuộc trong thời gian xây dựng:

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

Ngoài ra, nếu bạn phải đưa thư viện Rust vào tệp AndroidManifest.xml, hãy thêm thư viện vào uses_libs như sau:

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

Khả năng tương tác giữa Rust và C++ bằng CXX

Crate CXX cung cấp FFI an toàn giữa Rust và một phần của C++. Tài liệu CXX đưa ra các ví dụ hay về cách hoạt động của crate này nói chung và bạn nên đọc trước để làm quen với thư viện cũng như cách thư viện này kết nối C++ và Rust. Ví dụ sau đây minh hoạ cách sử dụng phương thức này trong Android.

Để CXX tạo mã C++ mà Rust gọi vào, hãy xác định genrule để gọi CXX và cc_library_static để gói mã đó vào một thư viện. Nếu bạn dự định để C++ gọi mã Rust hoặc sử dụng các loại được chia sẻ giữa C++ và Rust, hãy xác định một genrule thứ hai (để tạo tiêu đề C++ chứa các liên kết 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"],
}

Công cụ cxxbridge được dùng ở trên để tạo phía C++ của cầu nối. Thư viện tĩnh libcxx_test_cpp sẽ được dùng tiếp theo làm phần phụ thuộc cho tệp thực thi Rust:

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

Trong các tệp .cpp.hpp, hãy xác định các hàm C++ theo ý muốn, sử dụng các loại trình bao bọc CXX nếu muốn. Ví dụ: định nghĩa cxx_test.hpp chứa những nội dung sau:

#pragma once

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

int greet(rust::Str greetee);

Trong khi cxx_test.cpp chứa

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

#include <iostream>

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

Để sử dụng tính năng này từ Rust, hãy xác định một cầu CXX như dưới đây trong 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;
}