Ustawienie biblioteki C w Wasm

Czasami chcesz użyć biblioteki, która jest dostępna tylko jako kod w języku C lub C++. Tradycyjnie na tym etapie się poddajesz. Już nie, ponieważ teraz mamy Emscripten i WebAssembly (czy Wasm)!

Łańcuch narzędzi

Postanowiłem dowiedzieć się, jak skompilować istniejący kod C Wasm. W pobliżu backendu Wasm LLVM pojawiło się szum, więc Zacząłem się tym zagłębiać. Choć możesz użyć prostych programów do kompilowania, w ten sposób, kiedy chcesz użyć standardowej biblioteki C czy nawet skompilować wiele plików, prawdopodobnie wystąpią problemy. Tak właśnie trafiłem na studia, lekcja, której się nauczyłam:

Chociaż aplikacja Emscripten używała kompilatora C-to-asm.js, dojrzała jest docelowy Wasm i w procesie zmiany urządzenia, do oficjalnego backendu LLVM. Emscripten zapewnia też Implementacja standardowej biblioteki C zgodna z Wasm. Użyj Emscripten. it wymagają sporej pracy. emuluje system plików, umożliwia zarządzanie pamięcią, opakowuje OpenGL w interfejs WebGL – jest wiele rzeczy, których tworzenie nie jest potrzebne we własnym zakresie.

Choć może się wydawać, że ma Pan/Pani problem z naporami na zmęczenie, ja też – kompilator Emscripten usuwa wszystko, co nie jest potrzebne. W moich w eksperymentach, wynikowe moduły Wasm są odpowiednio dopasowane pod kątem a zespoły Emscripten i WebAssembly pracują nad tym, w przyszłości.

Aby pobrać aplikację Emscripten, postępuj zgodnie z instrukcjami strony internetowej lub Homebrew. Jeśli jesteś fanem/fanką: lub skompresowane polecenia takie jak ja i nie chcesz instalować rzeczy w systemie, wymaga obsługi WebAssembly, Obraz Dockera, którego możesz użyć zamiast:

    $ docker pull trzeci/emscripten
    $ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>

Kompilowanie prostych działań

Przyjrzyjmy się prawie kanonicznemu przykładowi zapisu funkcji w języku C, która oblicza n-tą liczbę fibonacci:

    #include <emscripten.h>

    EMSCRIPTEN_KEEPALIVE
    int fib(int n) {
      if(n <= 0){
        return 0;
      }
      int i, t, a = 0, b = 1;
      for (i = 1; i < n; i++) {
        t = a + b;
        a = b;
        b = t;
      }
      return b;
    }

Jeśli znasz język C, sama funkcja nie powinna być zbyt zaskakująca. Nawet jeśli nie znam C, ale znam JavaScript, ale mam nadzieję, że będziesz w stanie zrozumieć co się tutaj dzieje.

emscripten.h to plik nagłówka udostępniany przez firmę Emscripten. Potrzebujemy go tylko ma dostęp do makra EMSCRIPTEN_KEEPALIVE, ale daje znacznie więcej możliwości. To makro informuje kompilator, aby nie usuwał funkcji, nawet jeśli się pojawia nieużywane. Jeśli pominiemy to makro, kompilator zoptymalizuje funkcję przecież nikt z niego nie korzysta.

Zapiszmy wszystko w pliku o nazwie fib.c. Aby przekształcić go w plik .wasm, należy użyć polecenia kompilatora Emscripten emcc:

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c

Przeanalizujmy to polecenie. emcc to kompilator aplikacji Emscripten. fib.c to nasz C . Idzie Ci doskonale. -s WASM=1 prosi Emscripten o przekazanie nam pliku Wasm zamiast asm.js. -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' nakazuje kompilatorowi opuścić Funkcja cwrap() dostępna w pliku JavaScript – więcej o tej funkcji później. -O3 nakazuje kompilatorowi agresywną optymalizację. Możesz wybrać niższą w celu skrócenia czasu kompilacji, ale w większym stopniu wpływa na to, jest większy, bo kompilator może nie usuwać nieużywanego kodu.

Po uruchomieniu polecenia powinien wyświetlić się plik JavaScript o nazwie a.out.js i plik WebAssembly o nazwie a.out.wasm. plik Wasm (lub „module”) zawiera skompilowany kod C. Powinien on być dość mały. Plik JavaScript zajmuje się wczytywaniem i inicjowaniem modułu Wasm oraz i zapewnić lepszy interfejs API. W razie potrzeby zajmie się również konfiguracją stos, stertę i inne funkcje, których zwykle oczekuje się od systemu operacyjnego podczas pisania kodu C. W związku z tym plik JavaScript jest nieco większy i ważący 19 kB (ok. 5 kB w pliku gzip).

Prowadzenie prostych działań

Najłatwiejszym sposobem wczytania i uruchomienia modułu jest użycie wygenerowanego JavaScriptu . Po jego załadowaniu zobaczysz Module globalnie do Twojej dyspozycji. Używaj cwrap aby utworzyć natywną funkcję JavaScriptu, która zajmie się parametrami konwersji w coś przyjaznego dla języka C i wywołując funkcję opakowaną. cwrap wykonuje: nazwa funkcji, zwracany typ i typy argumentów jako argumenty w tej kolejności:

    <script src="a.out.js"></script>
    <script>
      Module.onRuntimeInitialized = _ => {
        const fib = Module.cwrap('fib', 'number', ['number']);
        console.log(fib(12));
      };
    </script>

Jeśli uruchom ten kod, powinien być widoczny komunikat „144”, co jest dwunastą liczbą Fibonacciego.

Święty Graal: kompilacja biblioteki C

Do tej pory kod C, który napisaliśmy, był napisany z myślą o Wasm. Rdzeń dla WebAssembly jest jednak wykorzystanie istniejącego ekosystemu C i umożliwiają programistom korzystanie z nich w internecie. Te biblioteki często bazują na standardowej bibliotece C, systemie operacyjnym, systemie plików i innych rzeczy. Większość tych funkcji zapewnia Emscripten, chociaż ograniczeniach.

Wróćmy do pierwotnego celu: skompilowania kodera dla WebP do Wasm. kodu źródłowego WebP jest napisany w języku C i jest dostępny GitHub oraz niektórych rozbudowanych dokumentacji interfejsu API. To całkiem dobry początek.

    $ git clone https://github.com/webmproject/libwebp

Na początek spróbujmy udostępnić element WebPGetEncoderVersion() z: encode.h do JavaScriptu, zapisując plik C o nazwie webp.c:

    #include "emscripten.h"
    #include "src/webp/encode.h"

    EMSCRIPTEN_KEEPALIVE
    int version() {
      return WebPGetEncoderVersion();
    }

To dobry, prosty program do przetestowania, czy można uzyskać kod źródłowy libwebp. do skompilowania, ponieważ nie wymagamy żadnych parametrów ani złożonych struktur danych wywołaj tę funkcję.

Aby skompilować ten program, musimy poinformować go, gdzie znajdzie plików nagłówkowych libwebp przy użyciu flagi -I oraz przekazują je wszystkie pliki C potrzebnych libwebp. Będę szczery: po prostu dałem wszystkie C które udało mi się znaleźć i posłużył się kompilatorem do usuwania wszystkiego, niepotrzebne. Wygląda na to, że działała rewelacyjnie.

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
        -I libwebp \
        webp.c \
        libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

Teraz do wczytania nowego modułu potrzebujemy tylko kodu HTML i JavaScript:

<script src="/a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async (_) => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>

Numer poprawionej wersji znajdzie się na output:

Zrzut ekranu konsoli Narzędzi deweloperskich z prawidłową wersją
numer.

Pobierz obraz z JavaScriptu do Wasm

Najlepiej jest uzyskać numer wersji kodera, taki obraz byłby bardziej imponujący, prawda? Zróbmy to teraz.

Pierwsze pytanie, na które musimy odpowiedzieć, brzmi: jak przenieść zdjęcie do Wasmland? Patrząc na encoding API of libwebp, oczekuje tablica bajtów w formacie RGB, RGBA, BGR lub BGRA. Na szczęście interfejs Canvas API ma getImageData() który daje nam Uint8ClampedArray z danymi zdjęć w formacie RGBA:

async function loadImage(src) {
  // Load image
  const imgBlob = await fetch(src).then((resp) => resp.blob());
  const img = await createImageBitmap(imgBlob);
  // Make canvas same size as image
  const canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  // Draw image onto canvas
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
  return ctx.getImageData(0, 0, img.width, img.height);
}

Teraz „tylko” sprowadza się do skopiowania danych z strony JavaScript do Wasm. ląd. Aby to zrobić, musisz udostępnić 2 dodatkowe funkcje. Ten, który przydziela dla zdjęcia na lądzie Wasm i innej, która ją ponownie uwalnia:

    EMSCRIPTEN_KEEPALIVE
    uint8_t* create_buffer(int width, int height) {
      return malloc(width * height * 4 * sizeof(uint8_t));
    }

    EMSCRIPTEN_KEEPALIVE
    void destroy_buffer(uint8_t* p) {
      free(p);
    }

Funkcja create_buffer przydziela bufor do obrazu RGBA, czyli 4 bajty na piksel. Wskaźnik zwrócony przez funkcję malloc() to adres pierwszej komórki pamięci tego bufora. Gdy wskaźnik jest zwracany do strony JavaScript, jest traktowany jako tylko liczbę. Po zaprezentowaniu funkcji w języku JavaScript za pomocą algorytmu cwrap możemy użyjemy tego numeru do znalezienia początku bufora i skopiujemy dane zdjęcia.

const api = {
  version: Module.cwrap('version', 'number', []),
  create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
  destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);

Wielki finał: kodowanie zdjęcia

Zdjęcie jest już dostępne w regionie Wasm. Czas wywołać koder WebP, wykonaj swoje zadanie! Patrząc na Dokumentacja WebP, WebPEncodeRGBA wydaje się idealnie pasować. Funkcja wykorzystuje wskaźnik do obrazu wejściowego i jego wymiarów oraz opcji jakości od 0 do 100. Wybiera także bufor wyjściowy, który trzeba zwolnić za pomocą WebPFree(), gdy już z obrazem WebP.

Wynikiem operacji kodowania jest bufor wyjściowy i jego długość. Ponieważ funkcje w C nie mogą mieć tablic jako typów zwracanych (chyba że przydzielimy pamięć ), skorzystałem ze statycznej tablicy globalnej. Nie uczą C (a tak naprawdę opiera się na tym, że wskaźniki Wasm mają 32-bitową szerokość), ale pozwalamy to chyba dobry skrót.

    int result[2];
    EMSCRIPTEN_KEEPALIVE
    void encode(uint8_t* img_in, int width, int height, float quality) {
      uint8_t* img_out;
      size_t size;

      size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);

      result[0] = (int)img_out;
      result[1] = size;
    }

    EMSCRIPTEN_KEEPALIVE
    void free_result(uint8_t* result) {
      WebPFree(result);
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_pointer() {
      return result[0];
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_size() {
      return result[1];
    }

Teraz, gdy wszystko jest gotowe, możemy wywołać funkcję kodowania, pobrać wskaźnik i rozmiar obrazu, umieść go w buforze JavaScript. i uwolnić wszystkie bufory na Wasmlandii przydzielone w ramach tego procesu.

    api.encode(p, image.width, image.height, 100);
    const resultPointer = api.get_result_pointer();
    const resultSize = api.get_result_size();
    const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
    const result = new Uint8Array(resultView);
    api.free_result(resultPointer);

W zależności od rozmiaru obrazu może wystąpić błąd związany z Wasm. nie może powiększyć pamięci w stopniu wystarczającym, aby pomieścić zarówno dane wejściowe, jak i obraz wyjściowy:

Zrzut ekranu konsoli Narzędzi deweloperskich z widocznym błędem.

Na szczęście rozwiązaniem tego problemu jest komunikat o błędzie. Musimy tylko dodaj -s ALLOW_MEMORY_GROWTH=1 do polecenia kompilacji.

I to wszystko! Skompilowaliśmy koder WebP i przetranskodowaliśmy obraz JPEG na WebP Aby udowodnić, że wszystko zadziałało, możemy zmienić bufor wyników w blob i użyć go w elemencie <img>:

const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);

Oto bogactwo nowego obrazu WebP

Panel sieci w Narzędziach deweloperskich i wygenerowany obraz.

Podsumowanie

Aby otworzyć bibliotekę C w przeglądarce, nie trzeba chodzić po parku. poznasz ogólny proces i sposób jego działania, staje się on a efekty mogą być oszałamiające.

WebAssembly otwiera w internecie wiele nowych możliwości przetwarzania, crunching i gry. Trzeba pamiętać, że Wasm nie jest dobrym rozwiązaniem, można zastosować do wszystkiego. Gdy jednak dojdzie do wąskiego gardła, Wasm może To niezwykle przydatne narzędzie.

Dodatkowa treść: uciążliwe prowadzenie czegoś prostego

Jeśli chcesz uniknąć generowania pliku JavaScript, możesz spróbować do. Wróćmy do przykładu z efektem Fibonacciego. Aby go załadować i uruchomić, możemy wykonaj te czynności:

<!DOCTYPE html>
<script>
  (async function () {
    const imports = {
      env: {
        memory: new WebAssembly.Memory({ initial: 1 }),
        STACKTOP: 0,
      },
    };
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch('/a.out.wasm'),
      imports,
    );
    console.log(instance.exports._fib(12));
  })();
</script>

Moduły WebAssembly utworzone przez Emscripten nie mają wystarczającej pamięci chyba że udostępnisz mu wspomnienie. Sposób udostępnienia modułu Wasm w dowolnego jest użycie obiektu imports – drugiego parametru w parametrze instantiateStreaming. Moduł Wasm ma dostęp do wszystkiego, co znajduje się w środku Obiekt importu, ale nic poza nim. Zgodnie z konwencją moduły kompilowane przez Emscripting wymagają od wczytywania JavaScriptu środowisko:

  • Pierwszy z nich to env.memory. Moduł Wasm nie ma dostępu do elementów z zewnątrz na świecie, więc musi mieć trochę pamięci, nad którą pracuje. Wejście WebAssembly.Memory Jest to (opcjonalnie) kawałek pamięci liniowej. Rozmiar są podane w „jednostkach stron WebAssembly”, co oznacza powyższy kod przydziela 1 stronę pamięci, przy czym każda strona ma rozmiar 64 KiB: Bez podawania atrybutu maximum wzrost pamięci jest teoretycznie nieograniczony (w Chrome (limit wynosi 2 GB). Większość modułów WebAssembly nie powinna zawierać parametru maksimum.
  • env.STACKTOP określa, gdzie powinien zacząć rosnąć stos. Stos jest niezbędna do wykonywania wywołań funkcji i przydzielania pamięci dla zmiennych lokalnych. W naszym niewielkim stopniu programu Fibonacci, możemy wykorzystać całą pamięć jako stos, STACKTOP = 0