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:
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:
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
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ścieWebAssembly.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 atrybutumaximum
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