Найти тему
Один Rust не п...Rust

Rust и SwiftUI

Оглавление

Для чего нужна данная статья? :

Узнать каким образом Rust-код может быть скомпилирован как динамическая библиотека (или статическая с биндингами) с интерфейсом C. С вызовом из Swift с использованием FFI.

Узнать каким образом Rust может быть использован для реализации вычислительных частей приложения с использованием ML, и результаты могут быть переданы в SwiftUI-интерфейс через FFI. Например, если у вас есть высокопроизводительный алгоритм, который лучше реализовать на Rust, его результаты могут быть отображены в SwiftUI.

Для чего Вам это уметь? :

Создать библиотеку на Rust, которая выполняет вычисления или взаимодействует с внешними источниками данных асинхронно, вы можете использовать эту библиотеку в своем SwiftUI-приложении.

Создайте новый проект Rust с помощью команды:

cargo new rust_ffi_example
cd rust_ffi_example

Отредактируйте файл src/lib.rs следующим образом:

#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}

После успешной сборки, скомпилированный файл библиотеки будет находиться в
target/release/librust_ffi_example.so (или .dylib в macOS).

Создайте новый проект в Xcode и создайте Swift-файл. Затем введите следующий код:

import Foundation
// Определение типа для функции из библиотеки Rust
typealias AddNumbersFunc = @convention(c) (Int32, Int32) -> Int32
// Загрузка библиотеки Rust
let rustLibrary = dlopen("путь_к_вашей_библиотеке/librust_ffi_example.so", RTLD_NOW)
// Проверка успешной загрузки библиотеки
if let rustLibrary = rustLibrary {
// Получение указателя на функцию из библиотеки
if let addNumbers = dlsym(rustLibrary, "add_numbers") {
// Приведение указателя к соответствующему типу
let addNumbersFunction = unsafeBitCast(addNumbers, to: AddNumbersFunc.self)
// Вызов функции из библиотеки Rust
let result = addNumbersFunction(5, 7)
print("Результат сложения: \(result)")
// Закрытие библиотеки
dlclose(rustLibrary)
} else {
print("Не удалось получить указатель на функцию из библиотеки Rust.")
}
} else {
print("Не удалось загрузить библиотеку Rust.")
}


Классификация изображения, с использованием модели ResNet

Подготовка модели: Скачайте предварительно обученную модель ResNet-50 в формате .pt и укажите путь в коде Rust.

  • Запуск сервера:Выполните cargo run в директории Rust-проекта.
  • Запуск приложения:Откройте SwiftUI проект в Xcode и запустите на симуляторе или устройстве.
  • Тестирование:Выберите изображение в приложении, нажмите "Классифицировать" и получите результат.

Frontend (SwiftUI)

Интерфейс и логика

Создадим сложное приложение с асинхронной загрузкой, выбором изображения и обработкой ошибок:

import SwiftUI
import Combine

struct Prediction: Codable {

let classId: Int

let confidence: Double

enum CodingKeys: String, CodingKey {

case classId = "class_id"

case confidence

}

}

class ImageClassifierViewModel: ObservableObject {

@Published var image: UIImage?

@Published var result: String = ""

@Published var isLoading = false

private var cancellables = Set<AnyCancellable>()

func classifyImage() {

guard let image = image,

let imageData = image.jpegData(compressionQuality: 0.8) else {

result = "Ошибка: нет изображения"

return

}

isLoading = true

let base64String = imageData.base64EncodedString()

let url = URL(string: "http://127.0.0.1:3030/classify")!

var request = URLRequest(url: url)

request.httpMethod = "POST"

request.setValue("application/json", forHTTPHeaderField: "Content-Type")

let body: [String: String] = ["image": base64String]

request.httpBody = try? JSONSerialization.data(withJSONObject: body)

URLSession.shared.dataTaskPublisher(for: request)

.map { $0.data }

.decode(type: Prediction.self, decoder: JSONDecoder())

.receive(on: DispatchQueue.main)

.sink { [weak self] completion in

self?.isLoading = false

if case .failure(let error) = completion {

self?.result = "Ошибка: \(error.localizedDescription)"

}

} receiveValue: { [weak self] prediction in

self?.result = "Класс: \(prediction.classId), Уверенность: \(String(format: "%.2f", prediction.confidence))"

}

.store(in: &cancellables)

}

}

struct ContentView: View {

@StateObject private var viewModel = ImageClassifierViewModel()

@State private var showingImagePicker = false

var body: some View {

NavigationView {

VStack(spacing: 20) {

if let image = viewModel.image {

Image(uiImage: image)

.resizable()

.scaledToFit()

.frame(maxHeight: 300)

.cornerRadius(10)

.shadow(radius: 5)

} else {

Rectangle()

.fill(Color.gray.opacity(0.3))

.frame(height: 300)

.overlay(Text("Выберите изображение"))

}

Button(action: { showingImagePicker = true }) {

Text("Выбрать изображение")

.font(.headline)

.padding()

.frame(maxWidth: .infinity)

.background(Color.blue)

.foregroundColor(.white)

.cornerRadius(10)

}

Button(action: { viewModel.classifyImage() }) {

Text("Классифицировать")

.font(.headline)

.padding()

.frame(maxWidth: .infinity)

.background(viewModel.isLoading ? Color.gray : Color.green)

.foregroundColor(.white)

.cornerRadius(10)

}

.disabled(viewModel.isLoading || viewModel.image == nil)

Text(viewModel.result)

.font(.body)

.padding()

.multilineTextAlignment(.center)

if viewModel.isLoading {

ProgressView()

}

}

.padding()

.navigationTitle("Классификатор изображений")

.sheet(isPresented: $showingImagePicker) {

ImagePicker(image: $viewModel.image)

}

}

}

}

struct ImagePicker: UIViewControllerRepresentable {

@Binding var image: UIImage?

@Environment(\.presentationMode) var presentationMode

func makeUIViewController(context: Context) -> UIImagePickerController {

let picker = UIImagePickerController()

picker.delegate = context.coordinator

return picker

}

func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

func makeCoordinator() -> Coordinator {

Coordinator(self)

}

class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

let parent: ImagePicker

init(_ parent: ImagePicker) {

self.parent = parent

}

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

if let uiImage = info[.originalImage] as? UIImage {

parent.image = uiImage

}

parent.presentationMode.wrappedValue.dismiss()

}

}

}

struct ContentView_Previews: PreviewProvider {

static var previews: some View {

ContentView()

}

}

Backend (Rust)

Создадим файл Cargo.toml с необходимыми зависимостями:

[dependencies]

tch = "0.7.0"

warp = "0.3"

serde = { version = "1.0", features = ["derive"] }

serde_json = "1.0"

tokio = { version = "1", features = ["full"] }

anyhow = "1.0"

image = "0.24"

Код сервера

use tch::{nn, vision, Device, Tensor};

use warp::{Filter, Rejection, Reply};

use serde::{Deserialize, Serialize};

use anyhow::Result;

use image::DynamicImage;

use std::sync::Arc;

use tokio::sync::Mutex;

#[derive(Clone)]

struct Classifier {

model: Arc<vision::resnet::ResNet>,

device: Device,

}

impl Classifier {

fn new() -> Result<Self> {

let vs = nn::VarStore::new(Device::Cuda(0)); // Используем GPU, если доступно

let model = vision::resnet::resnet50(&vs.root(), 1000); // ResNet-50 для 1000 классов

vs.load("path/to/pretrained_resnet50.pt")?; // Загружаем веса модели

Ok(Classifier {

model: Arc::new(model),

device: Device::Cuda(0),

})

}

fn preprocess_image(&self, image_data: &[u8]) -> Result<Tensor> {

let img = image::load_from_memory(image_data)?;

let img = img.resize_exact(224, 224, image::imageops::FilterType::Lanczos3)

.to_rgb8();

let tensor = Tensor::from_slice(&img.into_raw())

.view((1, 224, 224, 3))

.to_device(self.device)

.to_kind(tch::Kind::Float) / 255.0;

Ok(tensor.permute((0, 3, 1, 2))?) // Переводим в формат [N, C, H, W]

}

fn predict(&self, image: &Tensor) -> Result<i64> {

let output = self.model.forward_t(image, false);

Ok(output.argmax(1, false).int64_value(&[]))

}

}

#[derive(Deserialize)]

struct ImageRequest {

image: String, // Base64-encoded изображение

}

#[derive(Serialize)]

struct PredictionResponse {

class_id: i64,

confidence: f64,

}

async fn classify_image(

req: ImageRequest,

classifier: Arc<Mutex<Classifier>>,

) -> Result<impl Reply, Rejection> {

let image_data = base64::decode(&req.image).map_err(|e| warp::reject::custom(e))?;

let classifier = classifier.lock().await;

let image_tensor = classifier.preprocess_image(&image_data)

.map_err(|e| warp::reject::custom(e))?;

let class_id = classifier.predict(&image_tensor)

.map_err(|e| warp::reject::custom(e))?;

// Для усложнения: вычисляем уверенность

let logits = classifier.model.forward_t(&image_tensor, false);

let probs = logits.softmax(-1, tch::Kind::Float);

let confidence = probs.double_value(&[0, class_id as usize]);

Ok(warp::reply::json(&PredictionResponse {

class_id,

confidence,

}))

}

#[tokio::main]

async fn main() -> Result<()> {

let classifier = Arc::new(Mutex::new(Classifier::new()?));

let classifier_filter = warp::any().map(move || classifier.clone());

let classify_route = warp::post()

.and(warp::path("classify"))

.and(warp::body::json())

.and(classifier_filter)

.and_then(classify_image)

.recover(handle_rejection);

println!("Server running at http://127.0.0.1:3030");

warp::serve(classify_route).run(([127, 0, 0, 1], 3030)).await;

Ok(())

}

async fn handle_rejection(err: Rejection) -> Result<impl Reply, std::convert::Infallible> {

let code = warp::http::StatusCode::INTERNAL_SERVER_ERROR;

let message = format!("Error: {:?}", err);

Ok(warp::reply::with_status(message, code))

}