Co to jest WebAssembly i skąd się wzięło? Wyjaśniłem, jak powstało WebAssembly. W tym artykule pokażę, jak skompilować istniejący program C, mkbitmap
, do formatu WebAssembly. Jest ono bardziej skomplikowane niż przykład hello world, ponieważ obejmuje pracę z plikami, komunikację między WebAssembly a JavaScriptem oraz rysowanie na płótnie, ale nadal jest na tyle przystępne, że nie przytłoczy Cię.
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. Uprzedzam, że niekompilowanie się aplikacji lub biblioteki przy pierwszym uruchomieniu jest całkowicie normalne, dlatego niektóre z opisanych poniżej kroków nie zadziałały. Musiałem wrócić do poprzedniego stanu i spróbować jeszcze raz. 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 w języku C mkbitmap
odczytuje obraz i zastosuje do niego co najmniej jedną z tych operacji w podanej kolejności: odwrócenie, filtrowanie pasmowego, skalowanie i ustawienie progu. Każdą operację można włączać i wyłączać oddzielnie. Głównym zastosowaniem polecenia mkbitmap
jest konwertowanie obrazów kolorowych lub w szarościach na format odpowiedni do użycia w innych programach, zwłaszcza w programie do tworzenia ścieżek potrace
, który stanowi podstawę SVGcode. Jako narzędzie do wstępnej obróbki mkbitmap
jest szczególnie przydatne do konwertowania zeskanowanych rysunków wektorowych, takich jak kreskówki czy tekst odręczny, na dwupoziomowe obrazy o wysokiej rozdzielczości.
Funkcji mkbitmap
używa się, przekazując jej pewną liczbę opcji i jedną lub więcej nazw plików. Szczegółowe informacje znajdziesz na stronie man tego narzędzia.
$ mkbitmap [options] [filename...]
Pobierz kod
Pierwszym krokiem jest uzyskanie kodu źródłowego mkbitmap
. Znajdziesz go na stronie projektu. W chwili pisania tego tekstu najnowszą wersją jest potrace-1.16.tar.gz.
Kompilowanie i instalowanie lokalnie
Kolejnym krokiem jest skompilowanie i zainstalowanie narzędzia lokalnie, aby sprawdzić, jak się zachowuje. Plik INSTALL
zawiera te instrukcje:
cd
do katalogu zawierającego kod źródłowy pakietu i wpisz./configure
, aby skonfigurować pakiet dla swojego systemu.Uruchomienie listy
configure
może chwilę potrwać. Podczas działania wyświetla komunikaty z informacją, które funkcje są sprawdzane.Aby skompilować pakiet, wpisz
make
.Opcjonalnie wpisz
make check
, aby uruchomić autotesty dołączone do pakietu, zazwyczaj przy użyciu właśnie skompilowanych, nieinstalowanych plików binarnych.Wpisz
make install
, aby zainstalować programy oraz pliki danych i dokumentację. Podczas instalowania w prefiksie należącym do roota zalecamy skonfigurowanie i skompilowanie pakietu jako zwykły użytkownik, a tylko fazęmake install
należy wykonać z uprawnieniami roota.
Po wykonaniu tych czynności powinny pojawić się 2 pliki wykonywalne: potrace
i mkbitmap
. Ten drugi jest tematem tego artykułu. Aby sprawdzić, czy wszystko działa prawidłowo, uruchom polecenie mkbitmap --version
. Poniżej znajdziesz wyniki z mojego komputera w przypadku wszystkich 4 etapów, w którym 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 zobaczysz szczegóły wersji, oznacza to, że mkbitmap
została skompilowana i zainstalowana. Następnie wykonaj te same czynności w przypadku WebAssembly.
Kompilowanie mkbitmap
w standardzie WebAssembly
Emscripten to narzędzie do kompilowania programów C/C++ do WebAssembly. W dokumentacji Tworzenie projektów Emscripten można przeczytać:
Budowanie dużych projektów za pomocą Emscripten jest bardzo proste. Emscripten udostępnia 2 proste skrypty, które konfigurują pliki make, aby używać
emcc
jako zamiennika dlagcc
. W większości przypadków pozostała część obecnego systemu kompilacji projektu pozostaje niezmieniona.
Dokumentacja zawiera też (w uproszczeniu) następujące informacje:
Załóżmy, że zwykle kompilujesz za pomocą tych poleceń:
./configure
make
Aby skompilować za pomocą Emscripten, użyj zamiast tego tych poleceń:
emconfigure ./configure
emmake make
Oznacza to, że ./configure
staje się emconfigure ./configure
, a make
staje się emmake make
. Poniżej pokazujemy, jak to zrobić w przypadku 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 się teraz znajdować pliki .wasm
. Aby je znaleźć, uruchom 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 istotna jest tylko wartość mkbitmap
. Fakt, że nie mają rozszerzenia .js
, może być nieco mylący, ale są to w istocie pliki JavaScript, co można sprawdzić, wykonując szybkie wywołanie 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 funkcję mv mkbitmap mkbitmap.js
(i odpowiednio mv potrace potrace.js
).
Czas na pierwszy test, aby sprawdzić, czy wszystko działa prawidłowo. Uruchom plik za pomocą Node.js w wierszu poleceń, wykonując polecenie node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Kompilacja mkbitmap
została zakończona i plik został zapisany w formacie WebAssembly. Następnym krokiem jest uruchomienie aplikacji 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 szablon 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. Powinien pojawić się komunikat z prośbą o podanie danych. To się zgadza, ponieważ według strony man narzędzia [i]f no filename arguments are given, then mkbitmap acts as a filter, reading from standard input", co w przypadku Emscripten domyślnie oznacza prompt()
.
Zapobieganie 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 JavaScriptu z atrybutami, które kod wygenerowany przez Emscripten wywołuje w różnych punktach swojego wykonania.
Aby kontrolować wykonywanie kodu, możesz podać implementację Module
.
Gdy aplikacja Emscripten uruchamia się, sprawdza wartości obiektu 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
. Gdy ponownie wczytasz aplikację, ten komunikat powinien zniknąć.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Tworzenie modułowej wersji z dodatkowymi flagami kompilacji
Aby podać dane wejściowe do aplikacji, możesz skorzystać z obsługi systemu plików Emscripten w Module.FS
. W sekcji Obsługa systemu plików w dokumentacji można przeczytać:
Emscripten automatycznie decyduje, czy uwzględnić obsługę systemu plików. Wiele programów nie wymaga plików, a obsługa systemu plików nie jest nieznaczna pod względem rozmiaru, więc Emscripten nie uwzględnia jej, jeśli nie ma ku temu powodu. Oznacza to, że jeśli kod C/C++ nie ma dostępu do plików, obiekt
FS
i inne interfejsy API systemu plików nie będą uwzględniane w wyjściu. Jeśli natomiast kod w C/C++ używa plików, obsługa systemu plików zostanie dołączona automatycznie.
Niestety mkbitmap
to jeden z przypadków, w których Emscripten nie obsługuje automatycznie systemu plików, więc musisz wyraźnie to wskazać. Oznacza to, że musisz wykonać czynności opisane wcześniej w sekcji emconfigure
i emmake
, ale z kilkoma dodatkowymi flagami ustawionymi za pomocą argumentu CFLAGS
. Te 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 eksportować daneModule.FS
iModule.callMain
. - Aby wygenerować nowoczesny moduł ES6, ustaw opcje
-sMODULARIZE=1
i-sEXPORT_ES6
. - Ustaw wartość
-sINVOKE_RUN=0
, aby zapobiec początkowemu uruchomieniu, które spowodowało wyświetlenie promptu.
W tym konkretnym przypadku musisz też ustawić flagę --host
na wasm32
, aby poinformować skrypt configure
, że kompilujesz go dla 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.
Ręczne wykonywanie głównej funkcji
Następnym krokiem jest ręczne wywołanie funkcji main()
klasy mkbitmap
, wykonując instrukcję Module.callMain()
. Funkcja callMain()
przyjmuje tablicę argumentów, które są pojedynczo zgodne z tym, co przekazujesz na linii poleceń. Jeśli w wierszu poleceń użyjesz polecenia mkbitmap -v
, wywołasz w przeglądarce Module.callMain(['-v'])
. Spowoduje to zapisanie numeru wersji mkbitmap
w konsoli DevTools.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
Przekieruj standardowe dane wyjściowe
Standardowe wyjście (stdout
) to domyślnie 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();
Pobierz plik wejściowy do systemu plików pamięci
Aby w pamięci utworzyć system plików, musisz użyć odpowiednika polecenia 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 mkbitmap
to PNM (PBM, PGM, PPM) i BMP. Formaty wyjściowe to PBM dla map bitowych i PGM dla map szarych. Jeśli podano argument filename
, mkbitmap
domyślnie utworzy plik wyjściowy, którego nazwa zostanie uzyskana z nazwy pliku wejściowego przez zmianę jego sufiksu na .pbm
. Na przykład w przypadku nazwy pliku wejściowego example.bmp
nazwa pliku wyjściowego będzie brzmieć 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że być kompilowany i uruchamiany z niewielkimi lub żadnymi zmianami.
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 nazywany MEMFS) i ma funkcję writeFile()
, której używasz do zapisywania plików w systemie plików wirtualnych. Użyj writeFile()
w sposób pokazany w poniższym przykładowym kodzie.
Aby sprawdzić, czy operacja zapisu pliku się powiodła, uruchom funkcję readdir()
obiektu FS
z parametrem '/'
. Zobaczysz example.bmp
oraz kilka 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 tego, że funkcja Module.callMain()
jest zwykle wykonywana 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 faktyczne wykonanie
Gdy wszystko jest gotowe, uruchom polecenie mkbitmap
, uruchamiając polecenie Module.callMain(['example.bmp'])
. Zaloguj zawartość folderu '/'
w 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 pamięci
Funkcja readFile()
obiektu FS
umożliwia pobranie example.pbm
utworzonego na ostatnim kroku z systemu plików pamięci. Funkcja zwraca Uint8Array
, który jest konwertowany na obiekt File
i zapisywany na dysku, ponieważ przeglądarki zwykle nie obsługują plików PBM do bezpośredniego wyświetlania w przeglądarce.
(istnieją bardziej eleganckie sposoby zapisu pliku, ale użycie dynamicznie utworzonego <a download>
jest najbardziej powszechnie obsługiwane). Po zapisaniu pliku możesz go otworzyć w ulubionym programie do wyświetlania 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();
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 wybrania pliku wejściowego, dostosowanie parametrów mkbitmap
, a następnie uruchomienie 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ą niewielkiego kodu JavaScript możesz nawet wyświetlić podgląd obrazu wyjściowego. Poniżej znajdziesz kod źródłowy zaimplementowanego przykładu, który pokazuje jedną z możliwości wykonania tej czynności.
Podsumowanie
Gratulacje! Udało Ci się skompilować mkbitmap
na WebAssembly i uruchomić w przeglądarce. W pewnych miejscach utknęłaś i musiałaś skompilować narzędzie więcej niż raz, zanim zaczęło działać, ale jak już pisałam powyżej, to normalna sytuacja. Jeśli utkniesz, pamiętaj też o tagu webassembly
w witrynie StackOverflow. Powodzenia!
Podziękowania
Ten artykuł napisali Sam Clegg i Rachel Andrew.