Emscripten i npm

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 implementacja malloc() specjalnie dla Emscripten. lub alternatywna opcja to dlmalloc, w pełni funkcjonalna implementacja malloc(). Tylko Ty Jeśli przydzielasz dużo małych obiektów, musisz przełączyć się na dlmalloc 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:

Narzędzia deweloperskie z komunikatem wydrukowanym w językach C++ i Emscripten.

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łą:

DevTools
z użyciem interfejsu ABI libvpx wydrukowanego z użyciem emscripten.

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.