Введение в WebAssembly и Swift на продвинутом уровне
WebAssembly (Wasm) — это низкоуровневый бинарный формат инструкций, предназначенный для выполнения в браузере и на сервере с производительностью, близкой к нативной. Для разработчиков на Swift, работающих с iOS и macOS, интеграция Wasm открывает возможности переноса высокопроизводительных вычислений (криптография, обработка изображений, симуляции) из веб-среды в нативные приложения. В этой статье мы рассмотрим продвинутые техники: прямой вызов Wasm-функций из Swift, управление памятью через линейную память Wasm, оптимизацию ABI (Application Binary Interface) и обработку сложных типов данных.
Предполагается, что вы уже знакомы с основами Swift и имеете опыт компиляции C/C++ кода в Wasm (например, через Emscripten или wasi-sdk). Мы сосредоточимся на связке Swift (iOS/macOS) и скомпилированного Wasm-модуля, используя минимальные зависимости — только стандартные библиотеки Foundation и базовые возможности Swift для работы с памятью.
1. Компиляция C/C++ кода в WebAssembly и подготовка ABI
Первый шаг — подготовка Wasm-модуля с четко определенным ABI. Для Swift важно, чтобы экспортируемые функции имели плоские сигнатуры (только числа: i32, i64, f32, f64) и не использовали сложные структуры. Далее пример C-функции для вычисления суммы массива:
// sum.c
#include <stdint.h>
int32_t sum_array(int32_t* arr, int32_t len) {
int32_t sum = 0;
for (int32_t i = 0; i < len; i++) {
sum += arr[i];
}
return sum;
}
// Функция для выделения памяти внутри Wasm
int32_t* allocate_array(int32_t size) {
return (int32_t*)malloc(size * sizeof(int32_t));
}
Компилируем с помощью wasi-sdk (CLang для Wasm):
clang \
--target=wasm32-wasi \
-O3 \
-nostartfiles \
-Wl,--export=sum_array \
-Wl,--export=allocate_array \
-Wl,--export=__heap_base \
-o sum.wasm sum.c
Флаг --export явно указывает экспортируемые функции. __heap_base — стандартный символ Wasm, указывающий начало кучи, необходим для управления памятью из Swift.
2. Загрузка Wasm-модуля в Swift и выполнение функций
Для работы с Wasm в Swift на iOS/macOS используем фреймворк WebKit (WKWebView) или низкоуровневый C API через модуль JavaScriptCore. Однако для чистого Swift-решения без UI удобнее применить библиотеку WasmInterpreter из SwiftWasm (доступна через SPM). Рассмотрим вариант с использованием JavaScriptCore — он встроен в iOS/macOS.
2.1. Инициализация и загрузка бинарного файла
import JavaScriptCore
class WasmEngine {
private let context: JSContext
init?(wasmData: Data) {
guard let ctx = JSContext() else { return nil }
self.context = ctx
// Создаем объект WebAssembly
let wasmObj = ctx.evaluateScript("WebAssembly")
guard let wasm = wasmObj else { return nil }
// Компилируем модуль
let compileFunc = wasm.objectForKeyedSubscript("compile")
guard let compile = compileFunc else { return nil }
let promise = compile.call(withArguments: [wasmData])
// Обрабатываем promise (упрощенно для демонстрации)
context.evaluateScript("""
var wasmModule = null;
\(promise?.toString() ?? "").then(m => { wasmModule = m; });
""")
}
func callFunction(name: String, args: [JSValue]) -> JSValue? {
let instance = context.objectForKeyedSubscript("wasmModule")
let exports = instance?.objectForKeyedSubscript("exports")
let funcToCall = exports?.objectForKeyedSubscript(name)
return funcToCall?.call(withArguments: args)
}
}
Важно: Этот код упрощен. В реальном проекте используйте JSVirtualMachine и асинхронную обработку promise. Для production рекомендуется библиотека WasmKit (чистый Swift, без JavaScriptCore).
2.2. Работа с памятью — передача массива
Wasm использует линейную память. Чтобы передать массив из Swift, нужно:
- Выделить память внутри Wasm через
allocate_array. - Записать данные в эту память через прямой доступ к
wasm_memory. - Вызвать
sum_arrayс указателем и длиной.
func sumWithWasm(values: [Int32]) -> Int32? {
guard let engine = wasmEngine else { return nil }
// 1. Выделяем память
let size = JSValue(int32: Int32(values.count), in: context)
let ptr = engine.callFunction(name: "allocate_array", args: [size])
// 2. Получаем доступ к памяти Wasm
let memory = context.objectForKeyedSubscript("wasmModule")
.objectForKeyedSubscript("exports")
.objectForKeyedSubscript("memory")
let buffer = memory.objectForKeyedSubscript("buffer")
let view = context.evaluateScript("new Int32Array(\(buffer?.toString() ?? ""))")
// 3. Записываем данные (ptr — это смещение в байтах)
let offset = ptr?.toInt32() ?? 0
for i in 0..<values.count {
view?.setObject(JSValue(int32: values[i], in: context),
atIndex: offset/4 + i)
}
// 4. Вызываем функцию
let result = engine.callFunction(name: "sum_array",
args: [ptr!, size])
return result?.toInt32()
}
3. Оптимизация производительности: снижение накладных расходов
Каждый вызов функции через JavaScriptCore создает мост, что добавляет задержку. Для высокочастотных вызовов (например, обработка каждого пикселя) используйте следующие техники:
- Пакетная обработка: Передавайте большие блоки данных за один вызов.
- Прямой доступ к памяти: Минимизируйте количество вызовов — записывайте все данные в Wasm-память одной операцией
Uint8Array.set(). - Использование TypedArray: Работайте напрямую с
Int32ArrayилиFloat64Array, избегая преобразований в JSValue. - Асинхронный вызов: Для длительных операций используйте
WKWebViewсevaluateJavaScript