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>

<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-06-17 22:47:47
Автор :

Cookies and IP addresses allow us to deliver and improve our web content, resolve technical errors, and provide you with a personalized experience. Our website uses cookies and collects your IP address for these purposes.