Jak zintegrować WebAssembly z tą konfiguracją? W tym artykule omówimy to na przykładzie C/C++ i Emscripten.
WebAssembly (wasm) jest często jako podstawę wydajności lub sposób uruchamiania istniejącego języka C++ w bazie kodu w internecie. Chcieliśmy pokazać, jak aplikacja squoosh.app istnieje co najmniej trzecia perspektywa: wykorzystanie ogromnej i ekosystemy innych języków programowania. Na Emscripten, możesz użyć kodu w C/C++, Rust ma wbudowaną obsługę Wasm, a Go nad tym pracuje. Jestem a my pewnie pójdziesz w wielu innych językach.
W takich sytuacjach Wasm nie stanowi centralnego elementu aplikacji, tylko stanowi łamigłówkę mamy kolejny moduł. Twoja aplikacja ma już komponenty JavaScript, CSS, komponenty z obrazem, internetowy system kompilacji, a może nawet taką platformę jak React. Jak zintegrować WebAssembly z tą konfiguracją? W tym artykule zajmiemy się z C/C++ i Emscripten.
Docker
Uważam, że Docker jest przydatnym narzędziem podczas pracy z Emscripten. Kod C/C++ biblioteki są często pisane pod kątem współpracy z systemem operacyjnym, na którym są tworzone. Spójne środowisko jest bardzo przydatne. Docker zapewnia zwirtualizowany system Linux, który jest już skonfigurowany do pracy z Emscripten i ma wszystkie zainstalowane narzędzia i zależności. Jeśli czegoś brakuje, wystarczy, że możesz ją zainstalować, nie martwiąc się o to, jak wpłynie to na Twój komputer w innych projektach. Jeśli coś pójdzie nie tak, wyrzuć pojemnik i uruchom ponad. Jeśli raz zadziała, możesz mieć pewność, że będzie nadal działać, dają identyczne rezultaty.
Rejestr Dockera zawiera Emscripten zdjęcie: Trzeci, z których intensywnie korzystam.
Integracja z npm
W większości przypadków punktem wejścia do projektu internetowego jest npm
package.json
Zgodnie z konwencją większość projektów można tworzyć za pomocą npm install &&
npm run build
.
Ogólnie rzecz biorąc, artefakty kompilacji wyprodukowane przez Emscripten (.js
i .wasm
)
plik) powinna być traktowana jak kolejny moduł JavaScriptu
zasób. Plik JavaScript może być obsługiwany przez narzędzie do tworzenia pakietów, takie jak Webpack lub
i plik Wasm powinien być traktowany jak każdy inny większy zasób binarny, np.
obrazów.
Artefakty kompilacji Emscripten muszą więc zostać utworzone przed „normalnym” rozpoczyna się proces kompilacji:
{
"name": "my-worldchanging-project",
"scripts": {
"build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
"build:app": "<the old build command>",
"build": "npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
Nowe zadanie build:emscripten
może bezpośrednio wywołać funkcję Emscripten, ale
Jak już mówiłem wcześniej, zalecam użycie Dockera, żeby mieć pewność, że środowisko kompilacji
spójne.
docker run ... trzeci/emscripten ./build.sh
prosi Dockera o uruchomienie nowego
w kontenerze, używając obrazu trzeci/emscripten
i uruchomić polecenie ./build.sh
.
build.sh
to skrypt powłoki, który zamierzasz napisać w następnej kolejności. --rm
opowiada
Dockera usuwa kontener po jego uruchomieniu. W ten sposób unikniesz tworzenia
zbiór nieaktualnych obrazów maszyn. -v $(pwd):/src
oznacza, że
chcesz, aby Docker powielał treści z bieżącego katalogu ($(pwd)
) na /src
wewnątrz
kontener. Wszelkie zmiany wprowadzone w plikach w katalogu /src
wewnątrz folderu
kontener zostanie skopiowany do rzeczywistego projektu. Te powielane katalogi
są nazywane „punktami montowania”.
Przyjrzyjmy się build.sh
:
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
src/my-module.cpp
# Create output folder
mkdir -p dist
# Move artifacts
mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
Trzeba tu bardzo dokładnie przeanalizować dane!
set -e
szybko niszczy powłokę i trybu uzyskiwania zgody. Jeśli w skrypcie występują polecenia
zwróci błąd, cały skrypt zostanie natychmiast przerwany. Może to być
bardzo pomocne, ponieważ ostatnia wersja skryptu zawsze kończy się sukcesem
lub komunikat o błędzie, który spowodował niepowodzenie kompilacji.
Za pomocą instrukcji export
definiujesz wartości kilku środowisk
zmiennych. Umożliwiają przekazywanie do biblioteki C dodatkowych parametrów wiersza poleceń
kompilator (CFLAGS
), kompilator C++ (CXXFLAGS
) i łącznik łączący (LDFLAGS
).
Wszyscy otrzymują ustawienia optymalizatora za pośrednictwem sieci OPTIMIZE
, aby upewnić się, że
wszystko jest optymalizowane w ten sam sposób. Jest kilka wartości,
dla zmiennej OPTIMIZE
:
-O0
: nie optymalizuj. Żadne martwe kody nie są wyeliminowane, a kod Emscripten nie zmniejsza też generowanego przez siebie kodu JavaScript. Dobrze sprawdza się przy debugowaniu.-O3
: agresywna optymalizacja pod kątem skuteczności.-Os
: agresywna optymalizacja pod kątem skuteczności i rozmiaru jako dodatkowego kryterium.-Oz
: prowadź agresywną optymalizację pod kątem rozmiaru, tracąc skuteczność, jeśli to konieczne.
W internecie zwykle polecam usługę -Os
.
Polecenie emcc
udostępnia niezliczone opcje. Pamiętaj, że adres emcc to
Google powinien być „gotowym zamiennikiem dla kompilatorów takich jak GCC czy clang”. A więc wszystkie
być znane z GCC, są one najprawdopodobniej wdrażane przez EMCC,
cóż. Flaga -s
jest wyjątkowa, ponieważ pozwala nam konfigurować Emscripten
konkretnie. Wszystkie dostępne opcje znajdują się w
settings.js
ale ten plik może być dość przytłaczający. Oto lista flag Emscripten
które moim zdaniem są najważniejsze dla programistów stron internetowych:
--bind
włącza embind.-s STRICT=1
kończy obsługę wszystkich wycofanych opcji kompilacji. Dzięki temu masz pewność, aby kod został skompilowany w sposób zgodny z oczekiwaniami.-s ALLOW_MEMORY_GROWTH=1
umożliwia automatyczne zwiększanie pamięci, jeśli niezbędną. W momencie pisania Emscripten przydzieli 16 MB pamięci od początku. Gdy Twój kod przydziela fragmenty pamięci, ta opcja określa, czy te operacje spowodują, że cały moduł Wasm nie zadziała, jeśli pamięć lub jeśli kod typu glue może zwiększyć łączną pamięć do i uwzględniać przydzieloną ilość danych.-s MALLOC=...
wybiera implementacjęmalloc()
, której chce użyć.emmalloc
to To mała i szybka implementacjamalloc()
specjalnie dla Emscripten. lub alternatywna opcja todlmalloc
, w pełni funkcjonalna implementacjamalloc()
. Tylko Ty Jeśli przydzielasz dużo małych obiektów, musisz przełączyć się nadlmalloc
jeśli chcesz używać dzielenia wątków na wątki.-s EXPORT_ES6=1
przekształci kod JavaScript w moduł ES6 z atrybutem domyślny eksport, który działa z każdym modułem pakietów. Wymaga też-s MODULARIZE=1
do ustaw wartość.
Te flagi nie zawsze są niezbędne lub są pomocne tylko przy debugowaniu cele:
-s FILESYSTEM=0
to flaga odnosząca się do nazwy Emscripten i możliwości i symulować system plików, gdy kod na C/C++ używa operacji w tym systemie plików. Przeprowadza on pewne analizy skompilowanego kodu, aby zdecydować, czy w reklamie umieścić do emulacji systemu plików w kodzie typu glue. Czasami jednak to analiza może się nie udać i zapłacisz dość duże 70 kB dodatkowego kleju do emulacji systemu plików, którego możesz nie potrzebować. Jeśli używasz metody-s FILESYSTEM=0
, możesz wymusić, by Emscripten nie uwzględniał tego kodu.-g4
sprawi, że Emscripten będzie uwzględniać informacje debugowania w interfejsach.wasm
oraz mogą generować plik z mapami źródłowymi dla modułu Wasm. Więcej informacji znajdziesz na za pomocą narzędzia Emscripten debugowania .
I to wszystko. Aby przetestować tę konfigurację, przygotujmy my-module.cpp
:
#include <emscripten/bind.h>
using namespace emscripten;
int say_hello() {
printf("Hello from your wasm module\n");
return 0;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("sayHello", &say_hello);
}
Oraz index.html
:
<!doctype html>
<title>Emscripten + npm example</title>
Open the console to see the output from the wasm module.
<script type="module">
import wasmModule from "./my-module.js";
const instance = wasmModule({
onRuntimeInitialized() {
instance.sayHello();
}
});
</script>
(Oto informacje który zawiera wszystkie pliki).
Aby skompilować wszystko, uruchom polecenie
$ npm install
$ npm run build
$ npm run serve
Przejście do localhost:8080 powinno wyświetlić następujące dane wyjściowe Konsola Narzędzi deweloperskich:
Dodawanie kodu C/C++ jako zależności
Jeśli chcesz utworzyć bibliotekę C/C++ dla swojej aplikacji internetowej, jej kod musi być
do swojego projektu. Możesz ręcznie dodać kod do repozytorium projektu
Możesz też użyć npm do zarządzania tego rodzaju zależnościami. Powiedzmy, że
chcę użyć biblioteki libvpx w aplikacji internetowej. libvpx
to biblioteka C++ do kodowania obrazów za pomocą VP8 – kodeka używanego w plikach .webm
.
Parametru libvpx nie ma jednak w npm i nie ma parametru package.json
, więc nie mogę
zainstaluj ją bezpośrednio przy użyciu npm.
Aby rozwiązać ten problem,
napa. Napa pozwala zainstalować dowolny git
adres URL repozytorium jako zależność do folderu node_modules
.
Zainstaluj napa jako zależność:
$ npm install --save napa
i uruchom napa
jako skrypt instalacyjny:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
Gdy uruchomisz npm install
, napa sklonuje plik GitHub libvpx.
do repozytorium node_modules
o nazwie libvpx
.
Teraz możesz rozszerzyć skrypt kompilacji, aby utworzyć libvpx. libvpx używa funkcji configure
i make
. Na szczęście dzięki Emscripten możesz sprawdzić, czy configure
i
make
używa kompilatora Emscripten. Do tego celu służy otoka
polecenia emconfigure
i emmake
:
# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
Biblioteka C/C++ jest podzielona na 2 części: nagłówki (tradycyjnie w postaci .h
lub
.hpp
), które definiują struktury, klasy, stałe itp.
biblioteki ujawnione i rzeczywistą bibliotekę (czyli pliki .so
lub .a
). Do
używasz w kodzie stałej VPX_CODEC_ABI_VERSION
biblioteki,
, aby dołączyć pliki nagłówkowe biblioteki, używając instrukcji #include
:
#include "vpxenc.h"
#include <emscripten/bind.h>
int say_hello() {
printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
return 0;
}
Problem polega na tym, że kompilator nie wie, gdzie szukać elementu vpxenc.h
.
W tym celu służy flaga -I
. Informuje kompilator, które katalogi
sprawdź pliki nagłówka. Dodatkowo musisz też przekazać kompilatorowi
rzeczywisty plik biblioteki:
# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
-I ./node_modules/libvpx \
src/my-module.cpp \
build-vpx/libvpx.a
# ... below is unchanged ...
Jeśli teraz uruchomisz npm run build
, zobaczysz, że proces utworzy nowy .js
oraz nowy plik .wasm
, a strona demonstracyjna rzeczywiście zwróci stałą:
Zauważysz również, że proces kompilacji trwa bardzo długo. Powód
czas tworzenia może być różny. W przypadku libvpx zajmuje dużo czasu, ponieważ
przy każdym uruchomieniu kompiluje koder i dekoder dla VP8 i VP9
mimo że pliki źródłowe się nie zmieniły. Nawet mała
wprowadzenie zmiany w my-module.cpp
może zająć dużo czasu. Byłoby nam bardzo
warto zachować artefakty kompilacji libvpx po
po raz pierwszy.
Jednym ze sposobów osiągnięcia tego celu są zmienne środowiskowe.
# ... above is unchanged ...
eval $@
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...
(Oto Podstawowe informacje) zawierający wszystkie pliki).
Polecenie eval
umożliwia ustawianie zmiennych środowiskowych przez przekazywanie parametrów
do skryptu kompilacji. Polecenie test
pominie tworzenie biblioteki libvpx, jeśli
Parametr $SKIP_LIBVPX
jest ustawiony na dowolną wartość.
Teraz możesz skompilować moduł, ale pominąć odbudowywanie libvpx:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Dostosowywanie środowiska kompilacji
Czasami do utworzenia bibliotek potrzebne są dodatkowe narzędzia. Jeśli te zależności
których nie ma w środowisku kompilacji udostępnianym przez obraz Dockera, musisz
dodać je samodzielnie. Na przykład załóżmy, że chcesz też utworzyć
dokumentację biblioteki libvpx z wykorzystaniem doxygen. Doxygen to nie
dostępne w kontenerze Dockera, ale możesz je zainstalować za pomocą apt
.
W przeciwnym razie na urządzeniu build.sh
musisz pobrać i zainstalować aplikację.
za każdym razem, gdy chcesz utworzyć bibliotekę. Byłoby to konieczne
ale też uniemożliwiałoby pracę nad projektem w trybie offline.
W tym przypadku warto utworzyć własny obraz Dockera. Obrazy Dockera są tworzone przez
Dzięki temu Dockerfile
z opisem kroków kompilacji. Pliki Dockerfiles
i mają dużo
ale większość z nich
czas możesz uciec, używając FROM
, RUN
i ADD
. W tym przypadku:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
Dzięki FROM
możesz zadeklarować, którego obrazu Dockera chcesz użyć jako obrazu początkowego
. Jako podstawę wybrałam trzeci/emscripten
– obraz, którego używasz
cały czas rośnie. W RUN
instruujesz Dockera, aby uruchamiał polecenia powłoki wewnątrz
kontenera. Wszelkie zmiany wprowadzone przez te polecenia w kontenerze są teraz częścią
obraz Dockera. Aby upewnić się, że obraz Dockera został skompilowany
dostępne przed uruchomieniem kampanii build.sh
, musisz dostosować ustawienia package.json
bit:
{
// ...
"scripts": {
"build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
"build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
"build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
(Oto Podstawowe informacje) zawierający wszystkie pliki).
Spowoduje to utworzenie obrazu Dockera, ale tylko wtedy, gdy nie został jeszcze skompilowany. Potem
wszystko działa tak jak wcześniej. Teraz środowisko kompilacji ma interfejs doxygen
.
, co spowoduje, że dokumentacja libvpx zostanie skompilowana jako
cóż.
Podsumowanie
Nie jest zaskoczeniem, że kod w C/C++ i npm nie pasują do normy, ale możesz dzięki dodatkowym narzędziom i izolacji które zapewnia Docker. Ta konfiguracja nie działa w przypadku każdego projektu, ale dobry punkt wyjścia, który możesz dostosować do swoich potrzeb. Jeśli proszę o informację, jak ją poprawić.
Dodatek: korzystanie z warstw obrazów Dockera
Alternatywnym rozwiązaniem jest uwzględnienie większej liczby tych problemów za pomocą Dockera Inteligentne podejście Dockera do buforowania. Docker uruchamia pliki Dockerfiles krok po kroku przypisuje wynikowi każdego kroku własny obraz. Te obrazy pośrednie są często nazywane „warstwami”. Jeśli polecenie w pliku Dockerfile nie uległo zmianie, Docker nie będzie wykonywać tego kroku ponownie podczas ponownego kompilowania pliku Dockerfile. Zamiast tego korzysta z warstwy z ostatniego tworzenia obrazu.
Wcześniej trzeba było się starać, aby nie odbudowywanie pliku libvpx za każdym razem było możliwe.
tworząc aplikację. Zamiast tego możesz przenieść instrukcje budynku do libvpx.
z build.sh
do Dockerfile
, aby skorzystać z buforowania Dockera
mechanizm:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen git && \
mkdir -p /opt/libvpx/build && \
git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
emconfigure ../src/configure --target=generic-gnu && \
emmake make
(Oto Podstawowe informacje) zawierający wszystkie pliki).
Musisz ręcznie zainstalować git i sklonować libvpx, ponieważ nie masz
tworzyć powiązania podłączenia podczas uruchamiania docker build
. To efekt uboczny,
drzemki.