WebAssembly Hello World на C/C++
Table of contents
Introduction
WebAssembly (сокращенно wasm) - это бинарный формат инструкций для стековой виртуальной машины. WebAssembly спроектирован как портативная цель компиляции для высокоуровневых языков, таких как C/C++/Rust/Go, которую можно развертывать в web для клиентских и серверных приложений.
Если сильно упростить, то wasm
позволяет использовать в web
код, написанный на таких языках программирования, как C/C++
и другие (мы будем использовать C/C++
).
Делается это путем компилирования исходного кода в байт-код, который подгружается на web-страницу. И с этим кодом можно взаимодействовать через JavaScript
.
Чтобы скомпилировать код, написанный на высокоуровневых языках, в wasm
необходим компилятор.
Для кода на C/C++
мы будем использовать Emscripten
- это набор инструментов, который позволяет компилировать C/C++
в wasm
, используя LLVM.
Напишем простое приложение Hello World на языке C/C++
и скомпилируем его в wasm
с дальнейшим запуском через node
и в web
(браузере).
Репозиторий с полным кодом проекта: https://github.com/superrosko/example-wasm-hello-world.
Demo: https://pages.rdevelab.ru/example-wasm-hello-world/index.html.
Установка Emscripten
Для установки воспользуемся инструкциями с официального сайта.
Установку будем производить в директорию /opt
:
cd /opt
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
После установки нам будут доступны команды для компиляции, но нужно обратить внимание на последнюю команду, которая подгружает окружение - ее необходимо выполнять после каждого логина, если нам необходимо пользоваться Emscripten
:
source /opt/emsdk/emsdk_env.sh
Компилятор установлен и мы переходим к созданию проекта.
Структура проекта
./hello-world
├── build директория для сборки, которая игнорируется в .gitignore
├── include директория для заголовочных файлов .h/.hpp
├── public директория с собранным проектом
│ ├── index.html файл с демонстрацией работы нашего проекта в браузере
│ └── index.js файл с демонстрацией работы нашего проекта в консоли через node
├── src исходные файлы проекта .c/.cpp
│ └── core.cpp точка входа с функцией main
├── .gitignore
├── CMakeLists.txt файл с командами cmake для сборки
├── LICENSE
├── README.md
└── build.sh файл, управляющий сборкой проекта
Исходный код проекта
Код проекта достаточно прост:
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
extern "C"
{
EMSCRIPTEN_KEEPALIVE
const char *getHelloMessage()
{
const char *str = "Hello, world!";
return str;
}
}
Вначале мы подключаем заголовочный файл emscripten/emscripten.h
для использования функционала сборки в wasm
, такого как макрос EMSCRIPTEN_KEEPALIVE
.
Данный макрос необходимо указать у функции, которая не вызывается в коде, для того, чтобы она осталась при компиляции, т.к. LLVM
удаляет весь мертвый код.
Далее объявлена функция getHelloMessage
, которая возвращает указатель на строку "Hello, world!"
.
Данная функция объявлена в блоке с модификатором extern "C"
, который указывает, что необходимо использовать соглашение о вызовах на языке C
.
Это важно, т.к. в C++
есть перегрузка функций и при сборке к названию функции добавляется случайный префикс, из-за которого мы не сможем вызвать функцию из JavaScript
(если только не подсмотрим этот префикс, но при каждой сборке он будет разный).
Сборка проекта
Описание сборки проекта с файла build.sh
:
rm -rf build/
mkdir build
cd build
emcmake cmake ../
make
Вначале мы удаляем директорию build
со старой сборкой и создаем новую пустую директорию для текущей.
Далее переходим в директорию build
и запускаем сборку проекта, указывая для cmake
путь к корню проекта, в котором лежит файл CMakeLists.txt
с правилами компиляции.
Мы устанавливаем название проекта, далее проверяем соответствие версии cmake
в системе с описанной в конфигурации:
set(project "hello_world")
project(${project})
cmake_minimum_required(VERSION 3.10)
Следом устанавливаются параметры компилятора C++
, директория с заголовочными файлами и исходным кодом, а также указывается директория, в которую будут помещены собранные файлы проекта:
add_definitions(-std=c++11 -O3)
include_directories(include)
file(GLOB SOURCES src/*.cpp)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/public")
add_executable(${project} ${SOURCES})
В заключение устанавливаются параметры для самой сборки:
message(STATUS "Setting compilation target to WASM")
set(CMAKE_EXECUTABLE_SUFFIX ".wasm.js")
set_target_properties(${project} PROPERTIES LINK_FLAGS "-s WASM=1 -O3 -s EXTRA_EXPORTED_RUNTIME_METHODS='[cwrap]' -s FILESYSTEM=0 -s --llvm-lto 1 -flto")
Описание флагов:
WASM=1
- данный флаг указывает на то, что мы хотим получить на выходе wasm
-файл
-O3
- флаг параметров оптимизации
EXTRA_EXPORTED_RUNTIME_METHODS='[cwrap]'
- флаг, указывающий на необходимость обернуть функцию на C
и вернуть JavaScript
функцию для вызова с web-страницы.
FILESYSTEM=0
- флаг для оптимизации, указывающий, что мы не будем использовать файловую систему, т.е. не нужно подключать ее поддержку.
--llvm-lto 1
и -flto
- флаги оптимизации LTO
Итак, для сборки проекта нам необходимо выполнить build.sh
, после чего в директории public
появятся файлы hello_world.wasm.js
и hello_world.wasm.wasm
.
После того, как мы собрали проект, нам необходимо посмотреть на его выполнение. В данном проекте рассмотрим запуск через node и запуск в web (браузере).
Запуск проекта в node
Для запуска через node добавим следующий код в файл ./public/index.js
:
const Module = require("./hello_world.wasm.js")
const {performance} = require('perf_hooks');
Module['onRuntimeInitialized'] = onRuntimeInitialized;
function onRuntimeInitialized() {
const t0 = performance.now();
const getHelloMessage = Module.cwrap('getHelloMessage', 'string', []);
const result = getHelloMessage();
const t1 = performance.now();
console.log(`Result : "${result}"`)
console.log("Run time " + (t1 - t0) + " milliseconds.")
}
Мы сначала подключаем модуль hello_world.wasm.js
и затем, как он загрузится, запускаем выполнение. Получим нашу функцию, которую экспортировали из C/C++
:
const getHelloMessage = Module.cwrap('getHelloMessage', 'string', []);
Вызовем ее и выведем результат выполнения в консоль:
const result = getHelloMessage();
console.log(`Result : "${result}"`)
Для запуска проекта выполним команду node ./public/index.js
и получим следующий результат:
Result : "Hello, world!"
Run time 10.143100000917912 milliseconds.
Запуск проекта в web
Для запуска через web
добавим следующий код в файл ./public/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<title>WebAssembly Hello World!</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.row {
display: flex;
height: 100vh;
justify-content: center;
align-items: center;
}
#output {
font-size: 40px;
}
</style>
</head>
<body>
<div class="row">
<div id="output"></div>
</div>
<script src="hello_world.wasm.js"></script>
<script>
Module['onRuntimeInitialized'] = onRuntimeInitialized;
function onRuntimeInitialized() {
const helloMessage = Module.cwrap('getHelloMessage', 'string', [])();
const element = document.getElementById('output');
element.textContent = helloMessage;
}
</script>
</body>
</html>
Мы сначала подключили файл hello_world.wasm.js
:
<script src="hello_world.wasm.js"></script>
Затем сразу получили и выполнили нашу экспортируемую функцию, а также записали результат в переменную:
const helloMessage = Module.cwrap('getHelloMessage', 'string', [])();
После этого вывели в div
с id
output
:
const element = document.getElementById('output');
element.textContent = helloMessage;
Если мы просто откроем файл index.html
в браузере, то получим ошибку, т.к. загрузка wasm должна осуществляться по протоколу https
:
Fetch API cannot load file:///xxxxx/hello-world/public/hello_world.wasm.wasm. URL scheme must be "http" or "https" for CORS request.
Поэтому мы установим глобально (если еще не был установлен) npm
-пакет http-server
:
npm install http-server -g
Затем перейдем в директорию ./public
и запустим в ней сервер:
cd ./public
http-server
Получим информацию о запущенном сервере:
Starting up http-server, serving ./public
Available on:
http://127.0.0.1:8080
Hit CTRL-C to stop the server
Откроем в браузере адрес http://127.0.0.1:8080 и увидим строку: Hello, world!
Дата редактирования : 2020-11-12 00:36:33
Автор : Rosko