В разделе Что такое 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, если вы застряли. Приятного составления!
Благодарности
Эта статья была рецензирована Сэмом Клеггом и Рэйчел Эндрю .