W artykule Czym jest WebAssembly i skąd się wziął? Wyjaśniłem, jak doszliśmy do obecnej wersji WebAssembly. W tym artykule pokażę, jak skompilować istniejący program w języku C, mkbitmap
, do WebAssembly. Jest to bardziej skomplikowane niż przykład hello world, ponieważ obejmuje pracę z plikami, komunikację między WebAssembly a JavaScript oraz rysowanie na płótnie, ale jest wystarczająco proste, aby Cię nie przytłoczyć.
Artykuł jest przeznaczony dla programistów stron internetowych, którzy chcą poznać WebAssembly. Zawiera on szczegółowe instrukcje, jak postępować w przypadku kompilowania kodu takiego jak mkbitmap
do WebAssembly. Ostrzegam, że brak możliwości skompilowania aplikacji lub biblioteki za pierwszym razem jest całkowicie normalny. Dlatego niektóre z opisanych poniżej kroków nie zadziałały, więc musiałem się cofnąć i spróbować jeszcze raz w inny sposób. Artykuł nie przedstawia magicznego polecenia kompilacji końcowej jako czegoś, co spadło z nieba, ale opisuje moje rzeczywiste postępy, w tym niektóre frustracje.
mkbitmap
– informacje
Program mkbitmap
C odczytuje obraz i wykonuje na nim w tej kolejności co najmniej 1 z tych działań: odwracanie, filtrowanie górnoprzepustowe, skalowanie i progowanie. Każda operacja może być sterowana indywidualnie i włączana lub wyłączana. Głównym zastosowaniem mkbitmap
jest konwertowanie obrazów kolorowych lub w odcieniach szarości na format odpowiedni jako dane wejściowe dla innych programów, w szczególności programu do śledzenia potrace
, który stanowi podstawę SVGcode. Jako narzędzie do przetwarzania wstępnego mkbitmap
jest szczególnie przydatne do konwertowania zeskanowanych grafik wektorowych, takich jak kreskówki lub odręczny tekst, na obrazy dwupoziomowe o wysokiej rozdzielczości.
Używasz polecenia mkbitmap
, przekazując mu liczbę opcji i co najmniej 1 nazwę pliku. Szczegółowe informacje znajdziesz na stronie man narzędzia:
$ mkbitmap [options] [filename...]


mkbitmap -f 2 -s 2 -t 0.48
(Źródło).Pobierz kod
Pierwszym krokiem jest uzyskanie kodu źródłowego mkbitmap
. Znajdziesz go na stronie internetowej projektu. W momencie pisania tego artykułu najnowszą wersją jest 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:
cd
do katalogu zawierającego kod źródłowy pakietu i wpisz./configure
, aby skonfigurować pakiet w swoim systemie.Uruchomienie
configure
może chwilę potrwać. Podczas działania wyświetla komunikaty informujące o sprawdzanych funkcjach.Aby skompilować pakiet, wpisz
make
.Opcjonalnie wpisz
make check
, aby uruchomić testy własne dołączone do pakietu, zwykle przy użyciu właśnie skompilowanych, niezainstalowanych plików binarnych.Wpisz
make install
, aby zainstalować programy oraz pliki danych i dokumentację. W przypadku instalacji w prefiksie należącym do roota zaleca się skonfigurowanie i zbudowanie pakietu jako zwykły użytkownik, a tylko fazamake install
powinna być wykonywana z uprawnieniami roota.
Po wykonaniu tych czynności powinny powstać 2 pliki wykonywalne: potrace
i mkbitmap
. Ten drugi jest tematem tego artykułu. Możesz sprawdzić, czy wszystko działa prawidłowo, uruchamiając polecenie mkbitmap --version
. Oto wyniki wszystkich 4 kroków na moim komputerze, znacznie skrócone dla zwięzłości:
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 to zadziałało, uruchom polecenie mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Jeśli zobaczysz szczegóły wersji, oznacza to, że mkbitmap
zostało skompilowane i zainstalowane. Następnie wykonaj te same czynności w przypadku WebAssembly.
Kompilowanie mkbitmap
do WebAssembly
Emscripten to narzędzie do kompilowania programów w językach C i C++ do WebAssembly. W dokumentacji Building Projects (Tworzenie projektów) Emscripten podano:
Tworzenie dużych projektów za pomocą Emscripten jest bardzo proste. Emscripten udostępnia 2 proste skrypty, które konfigurują pliki makefile tak, aby używać
emcc
jako zamiennikagcc
. W większości przypadków reszta obecnego systemu kompilacji projektu pozostaje bez zmian.
Dokumentacja zawiera następnie (nieco skrócone):
Załóżmy, że zwykle kompilujesz za pomocą tych poleceń:
./configure
make
Aby skompilować za pomocą Emscripten, użyj tych poleceń:
emconfigure ./configure
emmake make
W zasadzie ./configure
staje się emconfigure ./configure
, a make
staje się 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 przebiegło pomyślnie, w katalogu powinny się teraz znajdować .wasm
pliki. Możesz je znaleźć, uruchamiając polecenie find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Dwa ostatnie wyglądają obiecująco, więc cd
do katalogu src/
. Dostępne są też 2 nowe pliki: mkbitmap
i potrace
. W tym artykule istotne jest tylko mkbitmap
. Brak rozszerzenia .js
może być nieco mylący, ale są to pliki JavaScript, co można szybko sprawdzić za pomocą 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 mv mkbitmap mkbitmap.js
(i odpowiednio mv potrace potrace.js
, jeśli chcesz).
Czas na pierwszy test, aby sprawdzić, czy wszystko działa. W tym celu wykonaj plik za pomocą 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ć mkbitmap
do WebAssembly. Teraz musisz sprawić, aby działał w przeglądarce.
mkbitmap
w przeglądarce za pomocą WebAssembly,
Skopiuj pliki mkbitmap.js
i mkbitmap.wasm
do nowego katalogu o nazwie mkbitmap
i utwórz plik index.html
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 lokalny serwer, który obsługuje katalog mkbitmap
, i otwórz go w przeglądarce. Powinna się wyświetlić prośba o wpisanie danych. Jest to zgodne z oczekiwaniami, ponieważ zgodnie ze stroną podręcznika narzędzia „[j]eśli nie podano argumentów nazwy pliku, mkbitmap działa jako filtr, odczytując dane ze standardowego wejścia”, które w przypadku Emscripten jest domyślnie prompt()
.
Zapobieganie automatycznemu wykonywaniu
Aby zapobiec natychmiastowemu wykonaniu funkcji mkbitmap
i sprawić, że będzie ona czekać na dane wejściowe użytkownika, musisz poznać obiekt Module
w Emscriptenie. Module
to globalny obiekt JavaScriptu z atrybutami, które kod wygenerowany przez Emscripten wywołuje w różnych momentach wykonywania.
Możesz podać implementację Module
, aby kontrolować wykonywanie kodu.
Gdy aplikacja Emscripten się uruchamia, sprawdza wartości w obiekcie Module
i je stosuje.
W przypadku mkbitmap
ustaw wartość Module.noInitialRun
na true
, aby zapobiec pierwszemu uruchomieniu, które spowodowało wyświetlenie promptu. Utwórz skrypt o nazwie script.js
, umieść go przed tagiem <script src="mkbitmap.js"></script>
w pliku index.html
i dodaj do niego ten kod:script.js
Po ponownym wczytaniu aplikacji komunikat powinien zniknąć.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Tworzenie modułowej kompilacji z dodatkowymi flagami kompilacji
Aby przekazywać dane do aplikacji, możesz używać obsługi systemu plików Emscripten w Module.FS
. W sekcji Including File System Support (W tym obsługa systemu plików) w dokumentacji jest napisane:
Emscripten automatycznie decyduje, czy uwzględnić obsługę systemu plików. Wiele programów nie potrzebuje plików, a obsługa systemu plików ma znaczną wielkość, więc Emscripten unika jej uwzględniania, jeśli nie widzi powodu. Oznacza to, że jeśli kod C/C++ nie uzyskuje dostępu do plików, obiekt
FS
i inne interfejsy API systemu plików nie zostaną uwzględnione w danych wyjściowych. Z drugiej strony, jeśli Twój kod w C/C++ korzysta z plików, obsługa systemu plików zostanie automatycznie uwzględniona.
Niestety mkbitmap
to jeden z przypadków, w których Emscripten nie uwzględnia automatycznie obsługi systemu plików, więc musisz mu to wyraźnie nakazać. Oznacza to, że musisz wykonać opisane wcześniej kroki emconfigure
i emmake
, ustawiając jeszcze kilka flag za pomocą argumentu CFLAGS
. Poniższe flagi mogą być przydatne również w innych projektach.
- Ustaw
-sFILESYSTEM=1
, aby uwzględnić obsługę systemu plików. - Ustaw
-sEXPORTED_RUNTIME_METHODS=FS,callMain
, aby wyeksportowaćModule.FS
iModule.callMain
. - Ustaw
-sMODULARIZE=1
i-sEXPORT_ES6
, aby wygenerować nowoczesny moduł ES6. - Ustaw wartość
-sINVOKE_RUN=0
, aby zapobiec początkowemu uruchomieniu, które spowodowało wyświetlenie prompta.
W tym konkretnym przypadku musisz też ustawić flagę --host
na wasm32
, aby poinformować skrypt configure
, że kompilujesz go 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ć emmake make
i skopiować nowo utworzonych plików do folderu mkbitmap
.
Zmodyfikuj index.html
, aby wczytywał tylko moduł ES script.js
, 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 teraz otworzysz aplikację w przeglądarce, w konsoli DevTools powinien pojawić się obiekt Module
, a prompt zniknie, ponieważ funkcja main()
obiektu mkbitmap
nie jest już wywoływana na początku.
Ręczne wykonywanie funkcji głównej
Następnym krokiem jest ręczne wywołanie funkcji mkbitmap
main()
przez uruchomienie Module.callMain()
. Funkcja callMain()
przyjmuje tablicę argumentów, które odpowiadają argumentom przekazywanym w wierszu poleceń. Jeśli w wierszu poleceń uruchamiasz polecenie mkbitmap -v
, w przeglądarce wywołujesz 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();
Przekierowywanie standardowego wyjścia
Domyślnym standardowym wyjściem (stdout
) jest konsola. Możesz jednak przekierować go na coś innego, np. na funkcję, która zapisuje 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();
Przenieś plik wejściowy do systemu plików w pamięci.
Aby przenieść plik wejściowy do systemu plików w pamięci, musisz użyć odpowiednika polecenia mkbitmap filename
w wierszu poleceń. Aby zrozumieć, jak do tego podchodzę, najpierw wyjaśnię, jak mkbitmap
oczekuje danych wejściowych i tworzy dane wyjściowe.
Obsługiwane formaty wejściowe mkbitmap
to PNM (PBM, PGM, PPM) i BMP. Formaty wyjściowe to PBM w przypadku bitmap i PGM w przypadku map odcieni szarości. Jeśli podano argument filename
, mkbitmap
domyślnie utworzy plik wyjściowy, którego nazwa zostanie utworzona na podstawie nazwy pliku wejściowego przez zmianę jego sufiksu na .pbm
. Na przykład dla nazwy pliku wejściowego example.bmp
nazwa pliku wyjściowego to example.pbm
.
Emscripten udostępnia wirtualny system plików, który symuluje lokalny system plików, dzięki czemu kod natywny korzystający z synchronicznych interfejsów API plików można skompilować i uruchomić z niewielkimi zmianami lub bez nich.
Aby mkbitmap
odczytywał plik wejściowy tak, jakby został przekazany jako argument wiersza poleceń filename
, musisz użyć obiektu FS
udostępnianego przez Emscripten.
Obiekt FS
jest obsługiwany przez system plików w pamięci (nazywany zwykle MEMFS) i ma funkcję writeFile()
, której używasz do zapisywania plików w wirtualnym systemie plików. Używasz znaku writeFile()
, jak pokazano w tym przykładowym kodzie.
Aby sprawdzić, czy operacja zapisu pliku się powiodła, uruchom funkcję readdir()
obiektu FS
z parametrem '/'
. Zobaczysz example.bmp
i liczbę domyślnych plików, które są zawsze tworzone automatycznie.
Zwróć uwagę, że poprzednie wywołanie funkcji Module.callMain(['-v'])
do drukowania numeru wersji zostało usunięte. Wynika to z faktu, że Module.callMain()
to funkcja, która zwykle powinna być uruchamiana 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();
Pierwsze rzeczywiste wykonanie
Gdy wszystko będzie gotowe, wykonaj mkbitmap
, uruchamiając Module.callMain(['example.bmp'])
. Zaloguj zawartość folderu MEMFS' '/'
. Obok pliku wejściowego example.bmp
powinien pojawić się nowo utworzony plik wyjściowy example.pbm
.
// 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();
Pobieranie pliku wyjściowego z systemu plików w pamięci
Funkcja readFile()
obiektu FS
umożliwia pobranie z systemu plików w pamięci obiektu example.pbm
utworzonego w ostatnim kroku. Funkcja zwraca wartość Uint8Array
, którą przekształcasz w obiekt File
i zapisujesz na dysku, ponieważ przeglądarki zwykle nie obsługują plików PBM do bezpośredniego wyświetlania w przeglądarce.
(Istnieją bardziej eleganckie sposoby zapisywania pliku, ale używanie dynamicznie tworzonego elementu <a download>
jest najbardziej rozpowszechnione). 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();
Dodawanie interaktywnego interfejsu
Do tej pory plik wejściowy jest zakodowany na stałe, a mkbitmap
działa z domyślnymi parametrami. Ostatnim krokiem jest umożliwienie użytkownikowi dynamicznego wyboru pliku wejściowego, dostosowania parametrów mkbitmap
, a następnie 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 przeanalizowania, więc za pomocą kodu JavaScript możesz nawet wyświetlić podgląd obrazu wyjściowego. Jedną z metod znajdziesz w kodzie źródłowym poniższej wersji demonstracyjnej.
Podsumowanie
Gratulacje! Udało Ci się skompilować mkbitmap
do WebAssembly i uruchomić go w przeglądarce. Było kilka ślepych zaułków i musiałem skompilować narzędzie więcej niż raz, zanim zaczęło działać, ale jak napisałem powyżej, to część procesu. Jeśli utkniesz, pamiętaj też o tagu webassembly
w StackOverflow. Miłego kompilowania!
Podziękowania
Ten artykuł został sprawdzony przez Sama Clegga i Rachel Andrew.