Kompiluję mkbitmap do WebAssembly

W sekcji Co to jest WebAssembly i skąd się wzięło? Wyjaśniałem, dlaczego tak się stało z dzisiejszą wersją WebAssembly. W tym artykule pokażę, jak kompilować istniejący program w języku C (mkbitmap) w WebAssembly. Jest to bardziej skomplikowane niż przykład w języku hello world, ponieważ obejmuje pracę z plikami, komunikację między interfejsami WebAssembly i JavaScript oraz rysowanie w obszarze roboczym. Łatwo można go jednak opanować.

Ten artykuł jest przeznaczony dla programistów stron internetowych, którzy chcą poznać narzędzie WebAssembly. Przedstawia on krok po kroku, co należy zrobić, jeśli chcesz skompilować na przykład mkbitmap do WebAssembly. Brak skompilowania aplikacji lub biblioteki przy pierwszym uruchomieniu jest zupełnie normalnym zjawiskiem. Dlatego niektóre z opisanych poniżej czynności nie przyniosły rezultatów. Muszę jednak zrobić to od początku i spróbować ponownie. Artykuł nie pokazuje końcowego polecenia kompilacji magicznej, tak jakby spadł z nieba. Opisuje on jedynie moje postępy, z pewnymi irytacją.

mkbitmap – informacje

Program mkbitmap C odczytuje obraz i stosuje do niego co najmniej 1 z tych operacji w tej kolejności: odwrócenie, filtrowanie górnoprzepustowe, skalowanie i progowanie. Każdą operację można kontrolować, włączać i wyłączać oddzielnie. Podstawowym zastosowaniem funkcji mkbitmap jest konwertowanie kolorów lub obrazów w skali szarości na format odpowiedni jako dane wejściowe dla innych programów, zwłaszcza programu śledzącego potrace, który stanowi podstawę SVGcode. Jako narzędzie do wstępnego przetwarzania danych narzędzie mkbitmap jest szczególnie przydatne do konwertowania zeskanowanych linii, takich jak kreskówki czy teksty odręczne, na dwupoziomowe obrazy o wysokiej rozdzielczości.

Aby użyć funkcji mkbitmap, przekazujesz mu liczbę opcji i co najmniej 1 nazwę pliku. Szczegółowe informacje znajdziesz na stronie podręcznika tego narzędzia:

$ mkbitmap [options] [filename...]
Obrazek kreskówki w kolorze.
Oryginalny obraz (źródło).
Obraz kreskówki został przekonwertowany na tryb szarości po wstępnym przetwarzaniu.
Pierwsza skala, a potem próg: mkbitmap -f 2 -s 2 -t 0.48 (źródło).

Pobierz kod

Pierwszym krokiem jest uzyskanie kodu źródłowego strony mkbitmap. Znajdziesz go na stronie projektu. W momencie tworzenia tego tekstu najnowsza wersja to potrace-1.16.tar.gz.

Kompilowanie i instalowanie lokalnie

Następnym krokiem jest skompilowanie i zainstalowanie narzędzia lokalnie, aby sprawdzić, jak działa. Plik INSTALL zawiera te instrukcje:

  1. cd do katalogu z kodem źródłowym pakietu i wpisz ./configure, aby skonfigurować pakiet dla swojego systemu.

    Uruchomienie listy configure może chwilę potrwać. W trakcie uruchamiania wyświetlane są komunikaty informujące o funkcjach, które ma sprawdzać.

  2. Wpisz make, aby skompilować pakiet.

  3. Opcjonalnie wpisz make check, aby uruchomić wszystkie testy wewnętrzne dołączone do pakietu, zwykle z użyciem odinstalowanych właśnie utworzonych plików binarnych.

  4. Wpisz make install, aby zainstalować programy, pliki danych i dokumentację. Jeśli instalujesz pakiet w prefiksie należącym do roota, zalecamy skonfigurowanie i skompilowanie pakietu jako zwykłego użytkownika, a także wykonanie tylko fazy make install z uprawnieniami użytkownika root.

Wykonanie tych czynności powinno dać 2 pliki wykonywalne: potrace i mkbitmap – omówiono je w tym artykule. Możesz sprawdzić, czy wszystko zadziałało prawidłowo, uruchamiając mkbitmap --version. Poniżej znajdziesz wyniki z mojego komputera w przypadku wszystkich 4 etapów, w którym zostały mocno skrócone, aby były zwięzłe:

Krok 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

Krok 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'.

Krok 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'.

Krok 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'.

Aby sprawdzić, czy udało się rozwiązać problem, uruchom polecenie mkbitmap --version:

$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

Jeśli wyświetli się szczegóły wersji, oznacza to, że mkbitmap został skompilowany i zainstalowany. Następnie skonfiguruj odpowiednik tych kroków w WebAssembly.

Kompilowanie pliku mkbitmap do WebAssembly

Emscripten to narzędzie służące do kompilowania programów w języku C/C++ w WebAssembly. Dokumentacja Building Projects firmy Emscripten zawiera te informacje:

Tworzenie dużych projektów przy użyciu Emscripten jest bardzo łatwe. Emscripten udostępnia 2 proste skrypty, które konfigurują pliki programu emcc do użytku bez konieczności użycia gcc – w większości przypadków reszta bieżącego systemu kompilacji projektu pozostaje bez zmian.

Dokumentacja jest kontynuowana (zmodyfikowana, aby zwiększyć zwięzłość):

Weź pod uwagę przypadek, w którym zwykle tworzysz treści za pomocą tych poleceń:

./configure
make

Aby utworzyć kompilację za pomocą Emscripten, użyj tych poleceń:

emconfigure ./configure
emmake make

W zasadzie ./configure zmienia się w emconfigure ./configure, a make zmienia się w emmake make. Poniżej pokazujemy, jak to zrobić za pomocą mkbitmap.

Krok 0. make clean.

$ make clean
Making clean in src
 rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo

Krok 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

Krok 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'.

Jeśli wszystko poszło dobrze, w katalogu powinny być teraz pliki (.wasm). Możesz je znaleźć, uruchamiając find . -name "*.wasm":

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

Ostatnie 2 odpowiedzi wyglądają obiecująco, więc cd do katalogu src/. Dostępne są też 2 nowe powiązane pliki: mkbitmap i potrace. W tym artykule istotne jest tylko mkbitmap. To, że nie mają rozszerzenia .js, może być mylące, ale w rzeczywistości są to pliki JavaScript, które można sprawdzić za pomocą szybkiego wywołania 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)

Zmień nazwę pliku JavaScript na mkbitmap.js, wywołując odpowiednio mv mkbitmap mkbitmap.js (i w razie potrzeby odpowiednio mv potrace potrace.js). Czas przeprowadzić pierwszy test, aby sprawdzić, czy plik zadziałał w środowisku Node.js w wierszu poleceń, uruchamiając node mkbitmap.js --version:

$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

Udało Ci się skompilować plik mkbitmap do WebAssembly. Teraz skonfiguruj ją w przeglądarce.

mkbitmap za pomocą WebAssembly w przeglądarce

Skopiuj pliki mkbitmap.js i mkbitmap.wasm do nowego katalogu o nazwie mkbitmap i utwórz stały plik HTML index.html, który wczytuje plik 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>

Uruchom serwer lokalny, który obsługuje katalog mkbitmap, i otwórz go w przeglądarce. Zobaczysz prośbę o podanie danych. Jest to zgodne z oczekiwaniami, ponieważ zgodnie ze stroną man narzędzia „[i]jeśli nie są podane żadne argumenty nazwy pliku, wówczas program mkbitmap działa jako filtr, odczyt ze standardowych danych wejściowych”. W przypadku Emscripten domyślnie ustawiona jest wartość prompt().

Aplikacja mkbitmap z prośbą o podanie danych.

Zapobiegaj automatycznemu wykonywaniu

Aby natychmiast zatrzymać wykonanie polecenia mkbitmap i zamiast niego zaczekać na dane wejściowe użytkownika, musisz zrozumieć obiekt Emscripten Module. Module to globalny obiekt JavaScript z atrybutami, które kod wygenerowany przez Emscripten wywołuje w różnych punktach wykonywania. Aby kontrolować wykonywanie kodu, możesz udostępnić implementację Module. Po uruchomieniu aplikacja Emscripten sprawdza wartości w obiekcie Module i je stosuje.

W przypadku polecenia mkbitmap ustaw Module.noInitialRun na true, aby zapobiec pierwszemu uruchomieniu, które spowodowało wyświetlenie komunikatu. Utwórz skrypt o nazwie script.js, umieść go przed poleceniem <script src="mkbitmap.js"></script> w elemencie index.html i dodaj poniższy kod do script.js. Po ponownym załadowaniu aplikacji komunikat powinien zniknąć.

var Module = {
  // Don't run main() at page load
  noInitialRun: true,
};

Utwórz kompilację modułową z większą liczbą flag kompilacji

Aby podać dane wejściowe do aplikacji, możesz skorzystać z obsługi systemu plików Emscripten w Module.FS. Sekcja Obejmuje obsługę systemu plików w dokumentacji zawiera te informacje:

Aplikacja Emscripten decyduje, czy automatycznie włączyć obsługę systemu plików. Wiele programów nie potrzebuje plików, a obsługa systemu plików ma niewielki rozmiar, więc Emscripten unika uwzględniania takich plików, jeśli nie ma powodu do tego. Oznacza to, że jeśli Twój kod w C/C++ nie uzyskuje dostępu do plików, obiekt FS i interfejsy API innych systemów plików nie zostaną uwzględnione w danych wyjściowych. Jeśli natomiast kod w C/C++ używa plików, obsługa systemu plików zostanie dołączona automatycznie.

Funkcja mkbitmap to jeden z przypadków, w których Emscripten nie włącza automatycznie obsługi systemu plików, więc musisz to wyraźnie o to poprosić. Oznacza to, że musisz wykonać opisane wcześniej kroki emconfigure i emmake, podając jeszcze kilka flag za pomocą argumentu CFLAGS. Poniższe flagi mogą być przydatne również w innych projektach.

W tym konkretnym przypadku musisz też ustawić flagę --host na wasm32, aby poinformować skrypt configure o kompilacji na potrzeby WebAssembly.

Ostateczne polecenie emconfigure wygląda tak:

$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'

Nie zapomnij ponownie uruchomić polecenia emmake make i skopiować nowo utworzone pliki do folderu mkbitmap.

Zmień index.html tak, aby ładował tylko moduł script.js ES, z którego następnie zaimportujesz moduł 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();

Gdy otworzysz aplikację teraz w przeglądarce, powinien być widoczny obiekt Module zapisany w konsoli Narzędzi deweloperskich. Prośba zniknęła, ponieważ funkcja main() w tabeli mkbitmap nie jest już wywoływana na początku.

Aplikacja mkbitmap z białym ekranem z obiektem Module zarejestrowanym w konsoli narzędzi deweloperskich.

Ręczne wykonywanie głównej funkcji

Następnym krokiem jest ręczne wywołanie funkcji main() funkcji mkbitmap przez uruchomienie Module.callMain(). Funkcja callMain() przyjmuje tablicę argumentów, które pasują po kolei do tego, co można przekazać w wierszu poleceń. Jeśli w wierszu poleceń użyjesz polecenia mkbitmap -v, wywołasz w przeglądarce Module.callMain(['-v']). Spowoduje to zarejestrowanie numeru wersji mkbitmap w konsoli Narzędzi deweloperskich.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  Module.callMain(['-v']);
};

run();

Aplikacja mkbitmap z białym ekranem z numerem wersji mkbitmap zapisanym w konsoli Narzędzi deweloperskich.

Przekieruj standardowe dane wyjściowe

Domyślnie standardowe dane wyjściowe (stdout) to konsola. Możesz go jednak przekierować do innej funkcji, np. do funkcji, która przechowuje dane wyjściowe w zmiennej. Oznacza to, że możesz dodać dane wyjściowe do kodu HTML, ustawiając właściwość 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();

Aplikacja mkbitmap z numerem wersji mkbitmap.

Pobierz plik wejściowy do systemu plików pamięci

Aby pobrać plik wejściowy do systemu plików pamięci, potrzebujesz odpowiednika mkbitmap filename w wierszu poleceń. Aby zrozumieć, co do tego podejmuję, najpierw przedstawię podstawowe informacje o tym, jak usługa mkbitmap oczekuje danych wejściowych i tworzy dane wyjściowe.

Obsługiwane formaty wejściowe funkcji mkbitmap to PNM (PBM, PGM, PPM) i BMP. Formaty wyjściowe to PBM dla map bitowych i PGM dla map szarych. Jeśli podany zostanie argument filename, mkbitmap domyślnie utworzy plik wyjściowy, którego nazwę uzyskano z nazwy pliku wejściowego, zmieniając jego sufiks na .pbm. Na przykład nazwa pliku wejściowego example.bmp będzie miała nazwę example.pbm.

Emscripten udostępnia wirtualny system plików, który symuluje lokalny system plików, dzięki czemu kod natywny korzystający z interfejsów API plików synchronicznych może być kompilowany i uruchamiany bez żadnych zmian lub przy niewielkich zmianach. Aby usługa mkbitmap mogła odczytywać plik wejściowy tak, jakby był przekazywany jako argument wiersza poleceń filename, musisz użyć obiektu FS dostarczonego przez Emscripten.

Obiekt FS jest obsługiwany przez system plików w pamięci (powszechnie określany jako MEMFS) i ma funkcję writeFile() służącą do zapisywania plików w wirtualnym systemie plików. Używasz writeFile() w sposób opisany w przykładowym kodzie poniżej.

Aby sprawdzić, czy operacja zapisu pliku działa, uruchom funkcję readdir() obiektu FS z parametrem '/'. Zobaczysz example.bmp i wiele plików domyślnych, które są zawsze tworzone automatycznie.

Uwaga: poprzednie wywołanie metody Module.callMain(['-v']) dotyczące wydrukowania numeru wersji zostało usunięte. Wynika to z faktu, że Module.callMain() jest funkcją, która zwykle oczekuje na uruchomienie tylko raz.

// 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();

Aplikacja mkbitmap z tabelą plików w systemie plików pamięci, w tym example.bmp.

Pierwsze faktyczne wykonanie

Gdy wszystko jest gotowe, uruchom polecenie mkbitmap, uruchamiając polecenie Module.callMain(['example.bmp']). Zapisz zawartość folderu '/' MEMFS. Nowo utworzony plik wyjściowy example.pbm powinien pojawić się obok pliku wejściowego 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();

Aplikacja mkbitmap z tabelą plików w systemie plików pamięci, w tym example.bmp i example.pbm.

Pobierz plik wyjściowy z systemu plików pamięci

Funkcja readFile() obiektu FS umożliwia pobranie zasobu example.pbm utworzonego w ostatnim kroku z systemu plików pamięci. Ta funkcja zwraca obiekt Uint8Array przekonwertowany na obiekt File i zapisany na dysku, ponieważ przeglądarki zwykle nie obsługują plików PBM do bezpośredniego wyświetlania w przeglądarce. (Są bardziej eleganckie sposoby zapisywania pliku, ale najpopularniejszym z nich jest używanie dynamicznie tworzonego pliku <a download>). Po zapisaniu pliku możesz go otworzyć w ulubionej przeglądarce obrazów.

// 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();

Finder z systemem macOS z podglądem wejściowego pliku .bmp i wyjściowego pliku .pbm.

Dodaj interaktywny interfejs

Na tym etapie plik wejściowy jest zakodowany na stałe, a mkbitmap działa z parametrami domyślnymi. Ostatnim krokiem jest umożliwienie użytkownikowi dynamicznego wyboru pliku wejściowego, dostosowania parametrów mkbitmap i uruchomienia narzędzia z wybranymi opcjami.

// 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']);

Format obrazu PBM nie jest szczególnie trudny do analizy, więc korzystając z kodu JavaScript, możesz nawet wyświetlić podgląd obrazu wyjściowego. Aby dowiedzieć się, jak to zrobić, zapoznaj się z kodem źródłowym umieszczonej wersji demonstracyjnej.

Podsumowanie

Gratulacje! Udało Ci się skompilować plik mkbitmap do WebAssembly i zacząć działać w przeglądarce. Były tam ślepe zaułki i trzeba było skompilować narzędzie więcej niż raz, aż zadziałało, ale jak wspomniałem powyżej, jest to częścią procesu. Jeśli napotkasz problemy, pamiętaj też o tagu webassembly usługi StackOverflow. Powodzenia!

Podziękowania

Ten artykuł napisali Sam Clegg i Rachel Andrew.