Работа с WebAssembly и Swift: продвинутый уровень

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

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

Начать курс

Введение в 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, нужно:

  1. Выделить память внутри Wasm через allocate_array.
  2. Записать данные в эту память через прямой доступ к wasm_memory.
  3. Вызвать 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

Блоги

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