Введение: Почему Kotlin и WebAssembly — это прорыв для веб-разработки
WebAssembly (Wasm) уже несколько лет перестал быть экзотикой и превратился в мощный инструмент для высокопроизводительных веб-приложений. Kotlin, в свою очередь, зарекомендовал себя как лаконичный и безопасный язык для JVM, Android и бэкенда. Однако связка Kotlin и WebAssembly открывает новые горизонты: компиляция Kotlin-кода в Wasm-модули позволяет запускать сложные вычислительные задачи прямо в браузере с производительностью, близкой к нативной.
В этой статье мы рассмотрим продвинутые техники работы с WebAssembly и Kotlin. Вы узнаете, как настроить проект, передавать сложные структуры данных, оптимизировать производительность и интегрировать Wasm-модули с существующим JavaScript-кодом. Материал рассчитан на разработчиков, уже знакомых с основами Kotlin и имеющих базовое представление о WebAssembly.
1. Настройка проекта и компиляция Kotlin в WebAssembly
Для работы с Kotlin/Wasm вам понадобится плагин org.jetbrains.kotlin.multiplatform и целевая платформа wasm. Рассмотрим пошаговую настройку Gradle-проекта.
1.1. Конфигурация build.gradle.kts
plugins {
kotlin("multiplatform") version "1.9.22"
}
kotlin {
wasm {
binaries.executable()
browser {
commonWebpackConfig {
cssSupport.enabled = true
}
}
}
sourceSets {
val wasmMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
}
}
}
Обратите внимание на binaries.executable() — это создаёт исполняемый Wasm-модуль. Для библиотечного модуля используйте binaries.library().
1.2. Первый экспортируемый модуль
Создадим простую функцию для вычисления чисел Фибоначчи, которая будет доступна из JavaScript:
// src/wasmMain/kotlin/com/example/MathUtils.kt
package com.example
@JsExport
fun fibonacci(n: Int): Long {
if (n <= 1) return n.toLong()
var a = 0L
var b = 1L
for (i in 2..n) {
val temp = a + b
a = b
b = temp
}
return b
}
Аннотация @JsExport гарантирует, что функция будет доступна в глобальном пространстве имён Wasm-модуля после компиляции.
2. Передача сложных данных между Kotlin и JavaScript
Простые типы (Int, Double, String) передаются через WebAssembly легко. Но как быть с массивами, списками или пользовательскими объектами? Здесь нужны продвинутые техники сериализации.
2.1. Работа с массивами байтов
Самый эффективный способ передачи бинарных данных — использовать ByteArray и Linear Memory Wasm:
// Kotlin side
@JsExport
fun processImageData(data: ByteArray): ByteArray {
// Пример: инвертировать цвета
return data.map { (255 - it.toInt()).toByte() }.toByteArray()
}
На стороне JavaScript вам потребуется скопировать данные в память модуля:
// JavaScript side
const module = await MyKotlinModule();
const input = new Uint8Array([100, 150, 200]);
const ptr = module._malloc(input.length);
module.HEAPU8.set(input, ptr);
const resultPtr = module._processImageData(ptr, input.length);
const result = module.HEAPU8.slice(resultPtr, resultPtr + input.length);
module._free(ptr);
module._free(resultPtr);
Этот подход даёт максимальную производительность, но требует ручного управления памятью.
2.2. Использование kotlinx.serialization
Для передачи JSON-объектов можно использовать библиотеку kotlinx.serialization. Это удобно, но добавляет оверхед на парсинг:
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class User(val name: String, val age: Int)
@JsExport
fun processUser(jsonString: String): String {
val user = Json.decodeFromString<User>(jsonString)
val updatedUser = user.copy(age = user.age + 1)
return Json.encodeToString(updatedUser)
}
3. Оптимизация производительности Wasm-модулей на Kotlin
Производительность — главная причина использования WebAssembly. Однако неоптимизированный код на Kotlin может работать медленнее, чем ожидалось. Рассмотрим ключевые приёмы.
3.1. Избегайте аллокаций внутри горячего цикла
Каждое создание объекта в Kotlin приводит к выделению памяти в Wasm-куче. В критических участках используйте примитивные типы и переиспользуйте буферы:
// Плохо: аллокация внутри цикла
fun sumSquaresBad(n: Int): Long {
var sum = 0L
for (i in 1..n) {
val square = i.toLong() * i // аллокация неявная, но OK
sum += square
}
return sum
}
// Хорошо: без лишних объектов
fun sumSquaresGood(n: Int): Long {
var sum = 0L
var i = 1
while (i <= n) {
sum += i.toLong() * i
i++
}
return sum
}
3.2. Используйте inline-функции для устранения оверхеда вызовов
В Kotlin/Wasm вызов функции имеет небольшую стоимость. Для микро-операций применяйте inline:
inline fun fastAdd(a: Int, b: Int): Int = a + b
@JsExport
fun computeWithInline(n: Int): Int {
var result = 0
for (i in 0 until n) {
result = fastAdd(result, i)
}
return result
}
3.3. Настройка компилятора
В build.gradle.kts можно включить оптимизации:
kotlin {
wasm {
binaries.executable()
compilerOptions {
freeCompilerArgs.add("-Xwasm-optimize")
freeCompilerArgs.add("-Xwasm-generate-source-map")
}
}
}
Флаг -Xwasm-optimize включает агрессивную оптимизацию на уровне Wasm-байткода, что может дать прирост скорости до 20-30%.