WebAssembly и Swift: продвинутая интеграция на iOS/macOS

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в WebAssembly для Swift-разработчиков

WebAssembly (Wasm) — это низкоуровневая бинарная инструкция, которая работает в изолированной виртуальной машине. Для Swift-разработчика на iOS/macOS интеграция Wasm открывает новые горизонты: перенос кода с других языков, песочница для сторонних библиотек и запуск высокопроизводительных вычислений без нативного кода. В этой статье мы рассмотрим продвинутые техники работы с WebAssembly в Swift: от компиляции до прямой работы с памятью и вызова функций.

В отличие от базовых туториалов, мы сосредоточимся на реальных задачах: передача сложных типов данных, управление памятью вручную и оптимизация производительности. Предполагается, что вы уже знакомы с основами Swift и базовым понятием WebAssembly.

Компиляция кода в WebAssembly для Swift

Инструменты и настройка

Для компиляции стороннего кода (C/C++/Rust) в Wasm используйте Emscripten или wasm-pack (для Rust). Для чистого Swift есть проект SwiftWasm, но он ограничен. Чаще всего вы будете интегрировать готовые модули.

Пример компиляции простой C-функции в Wasm:

// math.c
int add(int a, int b) {
    return a + b;
}

Команда:

emcc math.c -o math.wasm -s WASM=1 -s EXPORTED_FUNCTIONS='["_add"]' -Os

Ключевые флаги: EXPORTED_FUNCTIONS — указываем, какие функции экспортируем, -Os — оптимизация по размеру. Полученный math.wasm готов к загрузке в Swift.

Загрузка модуля в Swift

На iOS/macOS для работы с Wasm используем WasmKit или WKWebView. Рассмотрим продвинутый вариант — WasmKit с прямым управлением памятью.

import WasmKit

guard let wasmData = try? Data(contentsOf: URL(fileURLWithPath: "math.wasm")) else {
    fatalError("Не удалось загрузить wasm")
}

let module = try WasmModule(data: wasmData)
let engine = WasmEngine()
let instance = try engine.instantiate(module: module)

// Получаем экспортированную функцию
guard let addFunc = instance.export("add")?.function else {
    fatalError("Функция add не найдена")
}

Обратите внимание: WasmKit требует ручного управления импортами и памятью. Это даёт полный контроль, но увеличивает сложность.

Прямая работа с памятью: передача массивов и строк

Линейная память Wasm

WebAssembly использует линейную память (linear memory) — плоский массив байтов. Для передачи сложных структур (массивы, строки) вы должны выделять память внутри Wasm и копировать данные.

Рассмотрим пример: передача массива целых чисел из Swift в Wasm-функцию, которая вычисляет сумму.

// sum_array.c
int sum_array(int* arr, int len) {
    int sum = 0;
    for (int i = 0; i < len; i++) {
        sum += arr[i];
    }
    return sum;
}

Компилируем с экспортом malloc и free:

emcc sum_array.c -o sum_array.wasm -s WASM=1 -s EXPORTED_FUNCTIONS='["_sum_array", "_malloc", "_free"]' -s ALLOW_MEMORY_GROWTH=1

В Swift выделяем память, копируем данные и вызываем функцию:

let numbers = [10, 20, 30, 40, 50]
let count = numbers.count

// Выделяем память под массив (4 байта на int)
guard let mallocFunc = instance.export("malloc")?.function else { return }
let ptrResult = mallocFunc.call(Int32(count * MemoryLayout<Int32>.size))
guard let ptr = ptrResult?.i32 else { return }

// Копируем данные в память Wasm
let memory = instance.memory!
for i in 0..<count {
    let offset = ptr + Int32(i * MemoryLayout<Int32>.size)
    memory.store(offset: Int(offset), value: Int32(numbers[i]))
}

// Вызываем sum_array
guard let sumArrayFunc = instance.export("sum_array")?.function else { return }
let result = sumArrayFunc.call(ptr, Int32(count))
print("Сумма: \(result?.i32 ?? 0)") // 150

// Освобождаем память
guard let freeFunc = instance.export("free")?.function else { return }
_ = freeFunc.call(ptr)

Важно: Всегда освобождайте память через free. Иначе произойдёт утечка в линейной памяти Wasm.

Работа со строками

Строки в Wasm требуют кодировки UTF-8. Пример передачи строки из Swift:

// string_echo.c
#include <string.h>
char* echo_string(char* input) {
    return input;
}

В Swift преобразуем строку в UTF-8 байты и копируем:

let swiftString = "Привет, WebAssembly!"
let utf8Data = Data(swiftString.utf8)
let byteCount = utf8Data.count + 1 // +1 для нуль-терминатора

guard let mallocFunc = instance.export("malloc")?.function else { return }
let ptrResult = mallocFunc.call(Int32(byteCount))
guard let ptr = ptrResult?.i32 else { return }

// Копируем байты
let memory = instance.memory!
for (index, byte) in utf8Data.enumerated() {
    memory.store(offset: Int(ptr) + index, value: byte)
}
memory.store(offset: Int(ptr) + utf8Data.count, value: UInt8(0)) // нуль-терминатор

// Вызываем
guard let echoFunc = instance.export("echo_string")?.function else { return }
let resultPtr = echoFunc.call(ptr)?.i32 ?? 0

// Читаем результат
var resultBytes: [UInt8] = []
var offset = Int(resultPtr)
while true {
    let byte = memory.load(offset: offset) as UInt8
    if byte == 0 { break }
    resultBytes.append(byte)
    offset += 1
}
let resultString = String(bytes: resultBytes, encoding: .utf8) ?? ""
print("Эхо: \(resultString)") // Привет, WebAssembly!

freeFunc.call(ptr)

Оптимизация производительности и бенчмаркинг

Измерение накладных расходов

Вызов Wasm-функции из Swift имеет накладные расходы на передачу управления и конвертацию типов. Для критичных участков используйте пакетный вызов

Блоги

Рекомендаций по книгам