Компиляция mkbitmap в WebAssembly

В разделе Что такое 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 содержит следующие инструкции:

  1. cd в каталог, содержащий исходный код пакета, и введите ./configure , чтобы настроить пакет для вашей системы.

    Запуск configure может занять некоторое время. Во время работы он печатает несколько сообщений, сообщающих, какие функции он проверяет.

  2. Введите make , чтобы скомпилировать пакет.

  3. При желании введите make check , чтобы запустить все самотестирования, поставляемые с пакетом, обычно с использованием только что созданных удаленных двоичных файлов.

  4. Введите команду 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.jsmv 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 показывает приглашение с запросом ввода.

Запретить автоматическое выполнение

Чтобы остановить немедленное выполнение 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 больше не вызывается при запуске.

Приложение mkbitmap с белым экраном, показывающим объект модуля, зарегистрированный в консоли DevTools.

Вручную выполнить основную функцию

Следующий шаг — вручную вызвать функцию 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();

Приложение mkbitmap с белым экраном, на котором отображается номер версии mkbitmap, зарегистрированный в консоли DevTools.

Перенаправить стандартный вывод

Стандартным выводом ( 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, показывающее номер версии mkbitmap.

Получить входной файл в файловую систему памяти

Чтобы поместить входной файл в файловую систему памяти, вам понадобится эквивалент 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 показывает массив файлов в файловой системе памяти, включая example.bmp.

Первое фактическое исполнение

Когда все готово, выполните 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();

Приложение mkbitmap показывает массив файлов в файловой системе памяти, включая example.bmp и example.pbm.

Получить выходной файл из файловой системы памяти

Функция 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();

macOS Finder с предварительным просмотром входного файла .bmp и выходного файла .pbm.

Добавьте интерактивный интерфейс

На данный момент входной файл жестко запрограммирован, и 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, если вы застряли. Приятного составления!

Благодарности

Эта статья была рецензирована Сэмом Клеггом и Рэйчел Эндрю .