В статье «Что такое WebAssembly и откуда он взялся?» я объяснил, как мы пришли к современному WebAssembly. В этой статье я покажу вам свой подход к компиляции существующей программы на C, mkbitmap
, в WebAssembly. Это сложнее, чем пример «Hello World» , поскольку включает в себя работу с файлами, взаимодействие между WebAssembly и JavaScript-областями, а также рисование на холсте, но всё же достаточно понятно, чтобы не перегрузить вас.
Эта статья написана для веб-разработчиков, желающих изучить WebAssembly, и показывает пошаговые инструкции для компиляции чего-то вроде mkbitmap
в WebAssembly. Сразу предупреждаю: отсутствие компиляции приложения или библиотеки при первом запуске — это совершенно нормально. Именно поэтому некоторые из описанных ниже шагов не сработали, поэтому мне пришлось вернуться и попробовать ещё раз. Статья не показывает волшебную команду финальной компиляции, как будто она упала с неба, а скорее описывает мой реальный прогресс, включая некоторые разочарования.
О mkbitmap
Программа mkbitmap
на языке C считывает изображение и применяет к нему одну или несколько из следующих операций в указанном порядке: инверсию, фильтрацию верхних частот, масштабирование и пороговую обработку. Каждую операцию можно контролировать индивидуально, включая и отключая. Основное применение mkbitmap
— преобразование цветных или полутоновых изображений в формат, подходящий для ввода в другие программы, в частности, в программу трассировки potrace
, лежащую в основе SVGcode . В качестве инструмента предварительной обработки mkbitmap
особенно полезен для преобразования отсканированных штриховых изображений, таких как мультфильмы или рукописный текст, в двухуровневые изображения высокого разрешения.
Для использования mkbitmap
необходимо передать ему ряд параметров и одно или несколько имён файлов. Подробности см. на странице руководства инструмента:
$ mkbitmap [options] [filename...]


mkbitmap -f 2 -s 2 -t 0.48
( Источник ).Получить код
Первый шаг — получить исходный код mkbitmap
. Вы можете найти его на сайте проекта . На момент написания статьи последней версией является potrace-1.16.tar.gz .
Скомпилировать и установить локально
Следующий шаг — скомпилировать и установить инструмент локально, чтобы понять, как он работает. Файл INSTALL
содержит следующие инструкции:
cd
в каталог, содержащий исходный код пакета, и введите./configure
, чтобы настроить пакет для вашей системы.Запуск
configure
может занять некоторое время. Во время работы он выводит сообщения о проверяемых функциях.Введите
make
, чтобы скомпилировать пакет.При желании можно ввести
make check
для запуска любых самотестирований, поставляемых вместе с пакетом, обычно с использованием только что собранных неустановленных двоичных файлов.Введите команду
make install
, чтобы установить программы, файлы данных и документацию. При установке в префикс, принадлежащий пользователю root, рекомендуется настроить и собрать пакет от имени обычного пользователя, а только этапmake install
выполнять с правами root.
Выполнив эти шаги, вы получите два исполняемых файла: potrace
и mkbitmap
— последний из них и является темой этой статьи. Вы можете убедиться в корректной работе, выполнив команду mkbitmap --version
. Вот вывод всех четырёх шагов с моей машины, сильно обрезанный для краткости:
Шаг 1, ./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
Шаг 2, make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
Шаг 3, make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
Шаг 4, sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
Чтобы проверить, сработало ли это, запустите mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Если вы получили информацию о версии, значит, вы успешно скомпилировали и установили mkbitmap
. Теперь выполните те же действия, но с WebAssembly.
Скомпилировать mkbitmap
в WebAssembly
Emscripten — это инструмент для компиляции программ на C/C++ в WebAssembly. В документации по проектам Emscripten указано следующее:
Собирать крупные проекты с Emscripten очень просто. Emscripten предоставляет два простых скрипта, которые настраивают ваши make-файлы для использования
emcc
в качестве готовой заменыgcc
— в большинстве случаев остальная часть текущей системы сборки вашего проекта остаётся неизменной.
Далее следует документация (немного отредактированная для краткости):
Рассмотрим случай, когда вы обычно выполняете сборку с помощью следующих команд:
./configure
make
Для сборки с помощью Emscripten вам следует использовать следующие команды:
emconfigure ./configure
emmake make
Таким образом, по сути, ./configure
превращается emconfigure ./configure
, а make
в emmake make
. Ниже показано, как это сделать с помощью mkbitmap
.
Шаг 0, make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
Шаг 1, emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
Шаг 2, emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
Если всё прошло успешно, где-то в каталоге должны быть файлы .wasm
. Найти их можно, выполнив команду find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Два последних выглядят многообещающе, поэтому cd
в каталог src/
. Теперь там также есть два новых соответствующих файла: mkbitmap
и potrace
. Для этой статьи важен только mkbitmap
. Тот факт, что у них нет расширения .js
, немного сбивает с толку, но на самом деле это файлы JavaScript, что можно проверить с помощью быстрого вызова head
:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
Переименуйте файл JavaScript в mkbitmap.js
, вызвав mv mkbitmap mkbitmap.js
(и mv potrace potrace.js
соответственно, если хотите). Теперь пора провести первый тест, чтобы проверить работоспособность. Для этого запустите файл с Node.js в командной строке, выполнив команду node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Вы успешно скомпилировали mkbitmap
в WebAssembly. Теперь нужно заставить его работать в браузере.
mkbitmap
с WebAssembly в браузере
Скопируйте файлы mkbitmap.js
и mkbitmap.wasm
в новый каталог с именем mkbitmap
и создайте шаблонный HTML-файл index.html
, который загружает файл JavaScript mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
Запустите локальный сервер, обслуживающий каталог mkbitmap
, и откройте его в браузере. Вы должны увидеть приглашение с запросом на ввод данных. Это ожидаемо, поскольку, согласно странице руководства инструмента , «[i]если аргументы имени файла не указаны, mkbitmap действует как фильтр, считывая данные со стандартного ввода» , который в Emscripten по умолчанию представляет собой prompt()
.
Предотвратить автоматическое выполнение
Чтобы предотвратить немедленное выполнение mkbitmap
и вместо этого дождаться ввода данных пользователем, необходимо разобраться в объекте Module
в Emscripten. Module
— это глобальный объект JavaScript с атрибутами, которые код Emscripten вызывает на различных этапах своего выполнения. Вы можете реализовать Module
для управления выполнением кода. При запуске приложения Emscripten оно проверяет значения объекта Module
и применяет их.
В случае mkbitmap
установите для Module.noInitialRun
значение true
, чтобы предотвратить начальный запуск, приведший к появлению запроса. Создайте скрипт script.js
, подключите его перед тегом <script src="mkbitmap.js"></script>
в index.html
и добавьте следующий код в script.js
. После перезагрузки приложения запрос должен исчезнуть.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Создайте модульную сборку с дополнительными флагами сборки
Для предоставления входных данных приложению можно использовать поддержку файловой системы Emscripten в Module.FS
. В разделе документации «Включение поддержки файловой системы» указано:
Emscripten автоматически решает, включать ли поддержку файловой системы. Многим программам не нужны файлы, а поддержка файловой системы имеет существенный размер, поэтому Emscripten избегает её включения, когда не видит в этом необходимости. Это означает, что если ваш код на C/C++ не обращается к файлам, то объект
FS
и другие API файловой системы не будут включены в вывод. И наоборот, если ваш код на C/C++ использует файлы, поддержка файловой системы будет автоматически включена.
К сожалению, mkbitmap
— один из случаев, когда Emscripten не включает поддержку файловой системы автоматически, поэтому вам нужно явно указать ему это. Это означает, что вам нужно выполнить шаги emconfigure
и emmake
, описанные ранее, с добавлением пары дополнительных флагов через аргумент CFLAGS
. Следующие флаги могут пригодиться и для других проектов.
- Установите
-sFILESYSTEM=1
, чтобы включить поддержку файловой системы. - Установите
-sEXPORTED_RUNTIME_METHODS=FS,callMain
чтобы экспортироватьModule.FS
иModule.callMain
. - Установите
-sMODULARIZE=1
и-sEXPORT_ES6
для создания современного модуля ES6. - Установите
-sINVOKE_RUN=0
, чтобы предотвратить первоначальный запуск, вызвавший появление приглашения.
Кроме того, в этом конкретном случае вам необходимо установить флаг --host
на wasm32
, чтобы указать скрипту configure
, что вы компилируете для WebAssembly.
Окончательная команда emconfigure
выглядит так:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
Не забудьте снова запустить emmake make
и скопировать только что созданные файлы в папку mkbitmap
.
Измените index.html
так, чтобы он загружал только модуль ES script.js
, из которого затем импортируйте модуль mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
Если вы теперь откроете приложение в браузере, вы увидите объект Module
, зарегистрированный в консоли DevTools, а приглашение исчезнет, поскольку функция main()
mkbitmap
больше не вызывается при запуске.
Вручную выполнить основную функцию
Следующий шаг — вручную вызвать функцию main()
объекта mkbitmap
, выполнив Module.callMain()
. Функция callMain()
принимает массив аргументов, которые по одному соответствуют аргументам, переданным в командной строке. Если в командной строке вы выполните mkbitmap -v
, в браузере будет вызвана Module.callMain(['-v'])
. Это запишет номер версии mkbitmap
в консоль DevTools.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
Перенаправить стандартный вывод
Стандартный вывод ( stdout
) по умолчанию — это консоль. Однако вы можете перенаправить его в другое место, например, в функцию, которая сохраняет вывод в переменную. Это означает, что вы можете добавить вывод в HTML, установив свойство Module.print
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
Загрузите входной файл в файловую систему памяти.
Чтобы поместить входной файл в файловую систему памяти, вам потребуется эквивалент mkbitmap filename
в командной строке. Чтобы понять мой подход, сначала немного о том, как mkbitmap
ожидает входные данные и создаёт выходные.
Поддерживаемые форматы входных данных mkbitmap
: PNM ( PBM , PGM , PPM ) и BMP . Выходные форматы — PBM для растровых изображений и PGM для полутоновых изображений. Если указан аргумент filename
, mkbitmap
по умолчанию создаст выходной файл, имя которого будет получено из имени входного файла путём изменения его суффикса на .pbm
. Например, для входного файла example.bmp
имя выходного файла будет example.pbm
.
Emscripten предоставляет виртуальную файловую систему, имитирующую локальную файловую систему, что позволяет компилировать и запускать машинный код, использующий синхронные файловые API, практически без изменений. Чтобы mkbitmap
читал входной файл, как если бы он был передан в качестве аргумента командной строки filename
, необходимо использовать объект FS
, предоставляемый Emscripten.
Объект FS
поддерживается файловой системой в оперативной памяти (обычно называемой MEMFS ) и имеет функцию writeFile()
, которая используется для записи файлов в виртуальную файловую систему. Функция writeFile()
используется, как показано в следующем примере кода.
Чтобы убедиться, что операция записи файла выполнена успешно, запустите функцию readdir()
объекта FS
с параметром '/'
. Вы увидите example.bmp
и ряд файлов по умолчанию, которые всегда создаются автоматически .
Обратите внимание, что предыдущий вызов Module.callMain(['-v'])
для вывода номера версии был удалён. Это связано с тем, что Module.callMain()
— это функция, которая обычно должна быть выполнена только один раз.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
Первая фактическая казнь
Когда всё готово, выполните mkbitmap
, выполнив команду Module.callMain(['example.bmp'])
. Запишите содержимое папки ' '/'
в MEMFS, и вы увидите созданный выходной файл example.pbm
рядом с входным файлом example.bmp
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
Получить выходной файл из файловой системы памяти
Функция readFile()
объекта FS
позволяет извлечь файл example.pbm
, созданный на последнем этапе, из файловой системы в памяти. Функция возвращает массив Uint8Array
, который вы преобразуете в объект File
и сохраняете на диск, поскольку браузеры, как правило, не поддерживают PBM- файлы для прямого просмотра в браузере. (Существуют более элегантные способы сохранения файла , но использование динамически создаваемого тега <a download>
является наиболее распространённым.) После сохранения файла вы можете открыть его в любом удобном средстве просмотра изображений.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
Добавить интерактивный пользовательский интерфейс
На этом этапе входной файл жёстко задан, и mkbitmap
запускается с параметрами по умолчанию . Последний шаг — предоставить пользователю возможность динамически выбирать входной файл, настраивать параметры mkbitmap
, а затем запустить инструмент с выбранными параметрами.
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
Формат изображения PBM не так уж и сложен для анализа, поэтому с помощью JavaScript-кода можно даже показать предварительный просмотр выходного изображения. Один из способов сделать это представлен в исходном коде встроенной демонстрации ниже.
Заключение
Поздравляю, вы успешно скомпилировали mkbitmap
в WebAssembly и заработали в браузере! Были некоторые тупиковые ситуации, и вам пришлось компилировать инструмент несколько раз, прежде чем он заработал, но, как я уже писал выше, это часть опыта. Также запомните тег webassembly
на StackOverflow, если у вас возникнут затруднения. Удачной компиляции!
Благодарности
Эту статью рецензировали Сэм Клегг и Рэйчел Эндрю .