Для чего нужна данная статья? :
Узнать каким образом 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))
}