Пост

Интегрируем Rust в Android-приложение (Первая часть)

Интегрируем Rust в Android-приложение (Первая часть)

Rust — это язык системного программирования общего назначения, который существует уже довольно давно. Его можно использовать для выполнения задач, которые реализуются сейчас на C и C++, но с гораздо большей безопасностью памяти. Это позволяет использовать Rust для написания программ или скриптов для многих операционных систем, включая Android. Вы можете задаться вопросом, как это возможно и есть ли простой способ сделать это. Вот об этом эта статья!

В настоящее время не так много информации о том, как писать код на Rust в Android-приложениях. Есть некоторая информация, предоставленная Google, но она сложна для понимания новичком. Цель этого пошагового руководства — предоставить простое, но эффективное руководство по интеграции кода Rust в разработку для Android. Никаких предварительных знаний C или C++ или JNI не требуется.

Настройка ⚙️

Мы будем использовать Android Studio. Начнем с его настройки.

Плагин Rust 🌱

Во-первых, нам нужен плагин Rust. Откройте Android Studio. Откроется диалоговое окно, подобное показанному ниже. Выберите вкладку Плагины, найдите Rust, а затем установите официальный плагин Rust от JetBrains. В качестве альтернативы, если у вас уже открыт проект, нажмите Файл в верхнем левом углу Android Studio и выберите Настройки. Нажмите Плагины.

“Плагин Rust требует, чтобы Rust был установлен в вашей системе. Вы можете перейти по этой ссылке, чтобы установить Rust в вашей системе.”

1-4.webp

Создаем пустое приложение 📱

Теперь давайте создадим новое пустое приложение. Начните со стандартной настройки нового проекта.

2-2.webp

Назовем наш проект Rust Application и нажмем «Готово».

Появится окно, похожее на показанное ниже.

3-4.webp

Включаем Cargo Project 💼

Давайте теперь добавим проект Cargo (менеджер пакетов для Rust) следуя этому руководству. Чтобы внедрить его в проект Rust, просто щелкните Терминал в Android Studio и введите это:

C:\Users\USER1\AndroidStudioProjects\RustApplication>cargo new rust_lib --lib.

Это создает новую библиотеку, чтобы ее можно было использовать из нашего приложения. Мы назвали библиотеку rust_lib. Обратите внимание, где создается папка.

Чтобы просмотреть папку в Android Studio, перейдите на панель Project и выберите Project в раскрывающемся меню вместо Android.

4-2.webp

Вы увидите новую папку с именем rust_lib. Эта папка по умолчанию содержит новый репозиторий git, файл Cargo.toml и папку src, содержащую lib.rs. Теперь давайте изменим тип библиотеки, которую мы только что создали, используя файл Cargo.toml. Для разработки мобильных приложений библиотека должна быть динамической.

Добавьте следующее к содержимому файла Cargo.toml.

1
2
3
[lib]
name = "rust_lib"
crate-type = ["cdylib"]

Добавление зависимостей 🧶

Давайте добавим зависимости, которые создадут необходимые файлы, чтобы сделать связывание нашего Rust кода с Android гладким процессом.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[package]
name = "rust_lib"
version = "0.1.0"
edition = "2021"

# Смотрите больше ключей и их определений по адресу https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "rust_lib"
crate-type = ["cdylib"]

[dependencies]
rifgen = "*"
jni-sys = "*"
#логирование
log = "*"
log-panics="*"
android_logger = "*"

[build-dependencies]
flapigen = { git = "https://github.com/Dushistov/flapigen-rs", rev = "d79a34f22e73d90fe9f2423148a7421d39b8ed69" }
rifgen = "*"

Используйте *, чтобы получить последнюю версию зависимости.

Flapigen — это основная зависимость сборки для генерации соответствующего кода из нашего Rust кода и использования в Android-приложении. Flapigen работает с файлом интерфейса, но необходимость изменять файл интерфейса каждый раз при изменении кода может стать утомительной. Вот тут-то и появляется rifgen. Это упрощает создание файла интерфейса.

Дополнительные зависимости предназначены для ведения логов.

Создание файла сборки 📄

Как упоминалось ранее, flapigen и rifgen являются зависимостями сборки и взаимодействуют с файлом build.rs. Щелкните правой кнопкой мыши папку rust_lib и выберите New > Rust File.

5-2.webp

Назовите файл build.rs.

Следуя руководству, предоставленному flapigen и rifgen, наш файл build.rs должен выглядеть примерно так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::{env, fs};
use std::path::Path;
use flapigen::{JavaConfig, LanguageConfig};
use rifgen::{Generator, Language, TypeCases};

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();
    let in_src = "src/java_glue.rs.in";
    let out_src = Path::new(&out_dir).join("java_glue.rs");
    Generator::new(TypeCases::CamelCase, Language::Java, "src")
        .generate_interface(in_src);
    //delete the lib folder then create it again to prevent obsolete files from staying
    let java_folder = Path::new("../app/src/main/java/com/example/rustapplication/lib");
    if java_folder.exists() {
        fs::remove_dir_all(java_folder);
    }
    fs::create_dir(java_folder).unwrap();
    let swig_gen = flapigen::Generator::new(LanguageConfig::JavaConfig(
        JavaConfig::new(java_folder.into(), "com.example.rustapplication.lib".into())
            .use_null_annotation_from_package("androidx.annotation".into()),
    )).rustfmt_bindings(true);
    swig_gen.expand("android bindings", &in_src, &out_src);
    println!("cargo:rerun-if-changed=src");
}

Вам может быть интересно, что происходит в файле build.rs. Flapigen преобразует то, что находится в файле интерфейса, в Rust файл java_glue. Поэтому мы сначала указываем исходный файл для файла интерфейса (in_src), а затем выходной файл (java_glue.rs). Каталог java_glue.rs должен совпадать с переменной среды OUT_DIR. После этого используется rifgen для генерации файла интерфейса, с нашими предпочтениями в зависимости от языка, к которому мы добавляем проект Rust. Последний параметр Generator::new указывает начало папки, содержащей наш Rust код, а функция generate_interface принимает путь к файлу интерфейса, то есть in_scr. Папка java_folder указывает, куда должны помещаться созданные java-файлы. Вызов swig_gen.expand указывает flapigen сгенерировать соответствующие файлы. Обратите внимание на эти параметры, поскольку мы используем их в сборке Gradle.

Наконец, создайте файл с именем java_glue.rs в папке rust_lib/src и поместите в него следующее содержимое:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#![allow(
clippy::enum_variant_names,
clippy::unused_unit,
clippy::let_and_return,
clippy::not_unsafe_ptr_arg_deref,
clippy::cast_lossless,
clippy::blacklisted_name,
clippy::too_many_arguments,
clippy::trivially_copy_pass_by_ref,
clippy::let_unit_value,
clippy::clone_on_copy
)]

include!(concat!(env!("OUT_DIR"), "/java_glue.rs"));

И добавьте:

1
2
mod java_glue;
pub use crate::java_glue::*;

в ваш файл lib.rs. Это соединит ваш Rust код со сгенерированным.

Добавляем Android Toolchain и линкеры ⛓

Чтобы скомпилировать Rust для Android, нам нужно добавить инструменты Android в rustup. Для этого просто запустите в Терминале следующее:

1
2
>rustup default nightly 
>rustup target add aarch64-linux-android armv7-linux-androideabi

Поскольку rifgen крейт работает с nightly, вам нужно сначала установить Rust nightly.

Затем вы добавляете наборы инструментов Rust и стандартную библиотеку для 64-битной и 32-битной версий Android соответственно. Теперь давайте добавим компоновщики компилятора. Его нужно добавить в файл rust_lib/.cargo/config.toml.

Щелкните правой кнопкой мыши папку rust_lib, создайте новый каталог с именем .cargo, затем создайте новый файл в каталоге .cargo с именем config.toml.

Соответствующие компоновщики поставляются с Android NDK, поэтому их следует загрузить, прежде чем продолжить.

Добавьте следующее в созданный файл конфигурации.

1
2
3
4
5
[target.aarch64-linux-android]
linker = "{ANDROID SDK PATH}//ndk-bundle/toolchains/llvm/prebuilt/{OS VERSION}/bin/aarch64-linux-android21-clang++"

[target.armv7-linux-androideabi]
linker = "{ANDROID SDK PATH}/ndk-bundle/toolchains/llvm/prebuilt/{OS VERSION}/bin/armv7a-linux-androideabi21-clang++"

Замените ANDROID SDK на путь к Android SDK (или папку, содержащую пакет NDK). Кроме того, замените OS VERSION на вашу версию ОС. Например, на моем компьютере с Windows полный путь:

1
2
[target.aarch64-linux-android]
linker = "C:\\Users\\dev3java\\AppData\\Local\\Android\\Sdk\\ndk-bundle\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\aarch64-linux-android21-clang++.cmd"

Обратите внимание на .cmd в конце сборки Windows.

Если вы используете Mac OS с NDK v25, вы можете столкнуться с проблемой. Если это так — попробуйте предыдущие версии NDK.

Gradle для автоматизации сборки 🐘

Gradle можно использовать для автоматического запуска сборки cargo всякий раз, когда мы хотим протестировать приложение. Это приведет к тому, что изменения, которые мы сделали на стороне Rust, будут автоматически обновлены в нашем приложении.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.example.rustapplication"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
        ndk.abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64' // add this line
    }
    
    sourceSets {
        main {
            jniLibs.srcDirs = ['src/main/libs']
        }
    }
    
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {
  
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}

// add the following
def rustBasePath = "../rust_lib"
def archTriplets = [
        'armeabi-v7a': 'armv7-linux-androideabi',
        'arm64-v8a': 'aarch64-linux-android',
]

archTriplets.each { arch, target ->
    project.ext.cargo_target_directory = rustBasePath + "/target"
    // Build with cargo
    tasks.create(name: "cargo-build-${arch}", type: Exec, description: "Building core for ${arch}") {
        workingDir rustBasePath
        commandLine 'cargo', 'build', "--target=${target}", '--release'
    }
    // Sync shared native dependencies
    tasks.create(name: "sync-rust-deps-${arch}", type: Sync, dependsOn: "cargo-build-${arch}") {
        from "${rustBasePath}/src/libs/${arch}"
        include "*.so"
        into "src/main/libs/${arch}"
    }
    // Copy build libs into this app's libs directory
    tasks.create(name: "rust-deploy-${arch}", type: Copy, dependsOn: "sync-rust-deps-${arch}", description: "Copy rust libs for (${arch}) to jniLibs") {
        from "${project.ext.cargo_target_directory}/${target}/release"
        include "*.so"
        into "src/main/libs/${arch}"
    }

    // Hook up tasks to execute before building java
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn "rust-deploy-${arch}"
    }
    preBuild.dependsOn "rust-deploy-${arch}"

    // Hook up clean tasks
    tasks.create(name: "clean-${arch}", type: Delete, description: "Deleting built libs for ${arch}", dependsOn: "cargo-output-dir-${arch}") {
        delete fileTree("${project.ext.cargo_target_directory}/${target}/release") {
            include '*.so'
        }
    }
    clean.dependsOn "clean-${arch}"
}

Обратите внимание, что это файл Gradle уровня приложения или модуля, а не файл Gradle уровня проекта. Добавьте строки с 20 по 27 и строку 65 и далее. Строка 65 и далее просто указывает Gradle запускать сборку cargo всякий раз, когда мы запускаем приложение. После запуска команды сборки cargo мы копируем созданные java-файлы и файл динамической библиотеки (.so) и вставляем их в каталог, который может быть прочитан Android и использован в нашем коде Kotlin.

Теперь приложение должно работать 😄.

Бонус: ведение логов 📃

Давайте быстро реализуем ведение журнала, чтобы облегчить отладку нашего кода на Rust. В файл lib.rs добавьте следующее содержимое:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mod java_glue;
pub use crate::java_glue::*;

use android_logger::Config;
use log::Level;
use rifgen::rifgen_attr::*;

pub struct RustLog;

impl RustLog {
    //set up logging
    #[generate_interface]
    pub fn initialise_logging() {
        #[cfg(target_os = "android")]
            android_logger::init_once(
            Config::default()
                .with_min_level(Level::Trace)
                .with_tag("Rust"),
        );
        log_panics::init();
        log::info!("Logging initialised from Rust");
    }
}

Мы используем крейт android_logger для создания журналов, а затем вызываем log_panics::init(), чтобы перенаправить все паники для регистрации, а не для вывода стандартной ошибки. Обратите внимание на атрибут #[generate_interface]. Это сообщает rifgen, что мы будем вызывать эту функцию из Kotlin, поэтому он должен добавить этот вызов метода в файл интерфейса.

Загрузка библиотеки из Kotlin 📲

Теперь, когда мы настроили сторону Rust, давайте перейдем к стороне Kotlin. Теперь мы загрузим библиотеку из синглтона:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.rustapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.rustapplication.lib.RustLog
import com.example.rustapplication.ui.theme.RustApplicationTheme

class MainActivity : ComponentActivity() {

    companion object {
        init {
            System.loadLibrary("rust_lib")
            RustLog.initialiseLogging()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RustApplicationTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android With Rust")
                }
            }
        }
    }
}


@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

Добавьте различный импорт для созданного нами класса Logs. Обратите внимание, хотя мы и назвали функцию initialise_logging, мы можем использовать ее как initialiseLogging. Это потому, что мы указали CamelCase при настройке rifgen.

Запустите программу, чтобы увидеть информацию из логов кода Rust.

Источник

Репозиторий

Следующие шаги 🍀

На этом мы завершаем первую часть этой серии. В следующей части мы обсудим наш код, чтобы завершить это пошаговое руководство.

Авторский пост защищен лицензией CC BY 4.0 .

© dev3java. Некоторые права защищены.