Введение в 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 имеет накладные расходы на передачу управления и конвертацию типов. Для критичных участков используйте пакетный вызов