Введение: зачем iOS/macOS разработчику WebAssembly?
WebAssembly (Wasm) — это бинарный формат инструкций, который изначально создавался для браузеров, но быстро нашёл применение в нативной разработке. Для Swift-разработчиков под iOS и macOS интеграция WebAssembly открывает уникальные возможности: вы можете переиспользовать высокопроизводительный код на C/C++, Rust или Go, не переписывая его на Swift, и запускать его с почти нативной скоростью. Это особенно актуально для работы с графикой, криптографией, симуляциями и сложными алгоритмами.
В этой статье мы разберём, как скомпилировать код в Wasm, интегрировать его в Swift-проект для iOS/macOS, передавать данные и обрабатывать ошибки. Все примеры написаны на Swift 5.9+ и ориентированы на Xcode 15+.
1. Компиляция кода в WebAssembly для Swift-проекта
Первый шаг — получить Wasm-модуль. Самый популярный способ — скомпилировать код на C/C++ с помощью Emscripten или WasmEdge. Рассмотрим пример на C.
Создайте файл math_ops.c:
// math_ops.c
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
EMSCRIPTEN_KEEPALIVE
double multiply(double a, double b) {
return a * b;
}
EMSCRIPTEN_KEEPALIVE
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
Скомпилируйте его в Wasm с помощью Emscripten (установите через Homebrew: brew install emscripten):
emcc math_ops.c -o math_ops.wasm -s WASM=1 -s EXPORTED_FUNCTIONS='["_add", "_multiply", "_factorial"]' -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' -O3
В результате вы получите файл math_ops.wasm и math_ops.js (js-обёртка не понадобится, мы будем работать напрямую с Wasm).
2. Интеграция Wasm-модуля в Swift (iOS/macOS)
Для работы с WebAssembly в нативном Swift мы будем использовать фреймворк WasmKit или SwiftWasm. Рекомендуем WasmKit — он поддерживает iOS 15+ и macOS 12+.
2.1. Установка WasmKit через Swift Package Manager
Добавьте зависимость в Package.swift или через Xcode: File → Add Packages → https://github.com/swiftwasm/WasmKit.
2.2. Загрузка и выполнение Wasm-модуля
Создайте Swift-файл WasmEngine.swift:
import WasmKit
import Foundation
class WasmEngine {
private var instance: Instance?
private var store: Store?
/// Загружает Wasm-модуль из файла
func loadModule(from url: URL) throws {
let data = try Data(contentsOf: url)
let module = try Module(data: data)
let engine = Engine()
store = Store(engine: engine)
let imports = Imports()
// Здесь можно добавить импорт функций из Swift (см. раздел 3)
instance = try Instance(module: module, imports: imports, store: store!)
}
/// Вызывает экспортированную функцию
func callFunction(_ name: String, args: [Value]) throws -> [Value] {
guard let instance = instance else {
throw WasmError.moduleNotLoaded
}
let function = instance.exports[name] as! Function
return try function(args)
}
}
enum WasmError: Error {
case moduleNotLoaded
case functionNotFound
}
2.3. Использование в SwiftUI-приложении
import SwiftUI
struct ContentView: View {
@State private var result: String = "0"
private let engine = WasmEngine()
var body: some View {
VStack(spacing: 20) {
Text("Результат: \(result)")
.font(.title)
Button("Вычислить факториал 5") {
do {
let wasmURL = Bundle.main.url(forResource: "math_ops", withExtension: "wasm")!
try engine.loadModule(from: wasmURL)
let output = try engine.callFunction("factorial", args: [.i32(5)])
if case let .i32(value) = output.first {
result = "\(value)"
}
} catch {
result = "Ошибка: \(error.localizedDescription)"
}
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding()
}
}
Не забудьте добавить файл math_ops.wasm в Bundle вашего приложения (Build Phases → Copy Bundle Resources).
3. Передача сложных данных между Swift и WebAssembly
В Wasm нет прямого доступа к памяти Swift, поэтому для передачи строк, массивов или структур используется линейная память модуля. Рассмотрим пример передачи строки.
3.1. Экспорт функции для работы со строкой (C-код)
// string_ops.c
#include <emscripten.h>
#include <string.h>
EMSCRIPTEN_KEEPALIVE
int get_string_length(char* str) {
return strlen(str);
}
EMSCRIPTEN_KEEPALIVE
void to_uppercase(char* str) {
for (int i = 0; str[i]; i++) {
if (str[i] >= 'a' && str[i] <= 'z') {
str[i] -= 32;
}
}
}
Скомпилируйте: emcc string_ops.c -o string_ops.wasm -s WASM=1 -O3
3.2. Работа с памятью Wasm из Swift
extension WasmEngine {
/// Записывает строку в память Wasm-модуля
func writeString(_ str: String) throws -> Int32 {
guard let instance = instance, let store = store else {
throw WasmError.moduleNotLoaded
}
let memory = instance.exports["memory"] as! Memory
let bytes = Array(str.utf8)
let ptr = try instance.exports["malloc"] as! Function([.i32(Int32(bytes.count + 1))])
guard case let .i32(address) = ptr.first else {
throw WasmError.allocationFailed
}
// Копируем байты в память
var data = bytes + [0] // нуль-терминатор
data.withUnsafeMutableBytes { rawPtr in
memory.data.withUnsafeMutableBytes { wasmPtr in
wasmPtr.copyMemory(from: rawPtr, byteCount: data.count)
}
}
return address
}