Компиляция C в WASM без Emscripten: Учимся собирать
Когда перед вами стоит задача использовать код С/С++ в WebAssembly, то первое, что вы услышите - это Emscripten. Это не компилятор, а целый набор инструментов, который не только скомпонует вам C/C++ код, но и добавит JavaScript прослойку и даже реализацию стандартной библиотеки. Это действительно очень удобное решение. Тем более ранние LLVM версии не умели выдавать wasm сборку. Поэтому Emscripten имеет очень богатую историю своих решений. Но что, если ваш код будет сразу ориентирован на запуск и работу в браузере? Или вы действительно хотите разобраться как работает wasm+js? Поэтому отбросим весь лишний контекст и начнём с чего-то очень простого.
Установка LLVM
Для компиляции мы будем использовать LLVM (он так же используется в Emscripten под капотом). Как установить готовую сборку LLVM на разные системы вы найдёте в официальном гайде.
Достаточно просто найти на странице с официальными сборками подходящий для вашей платформы заранее собранный LLVM. Дальше скачиваем и распаковываем, вам необходимо чтобы была папка bin, а в ней хотя бы лежал clang компилятор. Я лично установил llvm 16.0.2 через homebrew на macos простой командой: brew install llvm
.
Для удобства мы добавим папку "/path/to/LLVM/bin" в переменную окружения PATH, чтобы напрямую работать с компилятором. Проверим, что всё правильно:
$ clang --version
Homebrew clang version 16.0.2
Target: x86_64-apple-darwin22.4.0
Thread model: posix
InstalledDir: /usr/local/Cellar/llvm/16.0.2/bin
Пишем main.c
Для теста напишем простейшую функцию add(x, y)
которая будет возвращать сумму двух чисел
double add(double x, double y) {
return x + y;
}
Компилируем main.c
Один файл можно собрать очень быстро одной строчкой кода
clang --target=wasm32 -nostdlib -Wl,--no-entry -Wl,--export-all -o main.wasm main.c
Краткое описание используемых флагов:
--target=wasm32
- указываем, что нужно собрать wasm для браузера-nostdlib
- не используем никаких стандартных библиотек языка (libc), нам не нужныmemcpy
,malloc
,printf
, и так далее-Wl,
- все эти флаги указаны для компоновщика--no-entry
- нам не нужна точка входа в программуint main(void)
--export-all
- пока экспортируем из нашего модуля всё что есть (вернёмся к этому флагу в разделе оптимизации размера сборки)
Запускаем main.wasm в браузере
Для этого оформим простейший index.html
, добавим загрузку и инициализацию нашего модуля
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<script type="module">
WebAssembly.instantiateStreaming(fetch("main.wasm")).then(source => {
const add = source.instance.exports.add;
console.info(`add(1.1, 1.9) = ${add(1.1, 1.9)}`);
});
</script>
</body>
</html>
Если вы запустите этот файл в браузере, то получите ошибку, мы не можем загрузить main.wasm
из файловой системы, поэтому воспользуемся локальным сервером:
npx serve
Теперь когда мы откроем http://localhost:3000
- мы можем посмотреть в консоль, и увидим следующий результат:
add(1.1, 1.9) = 3
Простая оптимизация размера
У меня получился main.wasm
размером 484 байт. Мы уже указали не использовать стандартную библиотеку, но не используем никаких флагов оптимизации. Дальше постараемся разобраться как оптимизировать размер нашей сборки.
Если мы просто добавим флаг -Oz
в команду сборки, то файл немного уменьшается, до 367 байт.
Дальше попробуем обрезать всё лишнее из файла флагом для компановщика -Wl,--strip-all
. Теперь наш файл занимает уже 272 байта.
Если мы посмотрим какие символы main.wasm
доступны в JavaScript, то увидим много полезных вещей, но нами они не используются, это происходит потому что мы используем для простоты флаг --export-all
.
console.table(source.instance.exports)
Заменим -Wl,--export-all
на -Wl,--export-dynamic
- теперь мы сами будем выбирать какие функции мы хотим экспортировать в JavaScript. Если вы пересоберете нашу сборку и запустите, то увидите ошибку, что метода add
не существует. Поэтому давайте просто укажем что мы хотим его экспортировать, изменим наш main.c
__attribute__((visibility("default")))
double add(double x, double y) {
return x + y;
}
Теперь пересобираем с флагом -Wl,--export-dynamic
clang -Oz --target=wasm32 -nostdlib -Wl,--strip-all -Wl,--no-entry -Wl,--export-dynamic -o main.wasm main.c
Размер main.wasm
теперь всего 55 байт! А таблица экспортов содержит только нашу функцию.
Ура! Теперь мы видим только нашу Си функцию
Эту сборку 55 байт по размеру теперь можно сравнить с аналогичным примером из AssemblyScript (из-за того что они включают ссылку на sourceMap внутри wasm - их размер идентичного примера занимает аж 96 байт).
Заключение
Теперь у нас есть установленный LLVM. Мы научились компилировать наш C код в минимальную, компактную и чистую WebAssembly сборку. Можем загрузить wasm модуль и вызвать Си функцию. Познакомились с некоторыми флагами компиляции и компоновки для оптимизации размера.
В следующем уроке попробуем написать целый тест, узнаем как вызывать JavaScript функцию из C, будем использовать встроенные инструкции WebAssembly без <math.h>
Надеюсь, что у вас всё получилось и статья вам понравилась!