WebAssembly w TypeScript
Ten artykuł wyjaśnia, czym jest WebAssembly w TypeScript.
Wyjaśnimy praktyczne i łatwe do zrozumienia metody integracji TypeScript z WebAssembly.
YouTube Video
WebAssembly w TypeScript
WebAssembly (Wasm) to środowisko uruchomieniowe w formacie binarnym, które działa w przeglądarce z prędkością zbliżoną do natywnej. Wywołując Wasm z poziomu TypeScript, możesz wydajnie korzystać z procesów wymagających dużej mocy obliczeniowej oraz istniejących natywnych bibliotek napisanych w C/C++ lub Rust.
Podstawowy przebieg wykonywania
Tutaj wyjaśnimy podstawowy przebieg wykonywania Wasm. TypeScript (lub przeglądarka) pobiera plik .wasm, instancjonuje go i wywołuje eksportowane funkcje.
-
- Utwórz plik binarny .wasm za pomocą AssemblyScript, Rust, C++ lub przygotuj gotowy.
-
- Pobierz plik .wasm w TypeScript (lub w przeglądarce) i zainicjuj go synchronicznie lub asynchronicznie.
-
- Wywołaj eksportowane funkcje i w razie potrzeby współdziel pamięć za pomocą
WebAssembly.Memory.
- Wywołaj eksportowane funkcje i w razie potrzeby współdziel pamięć za pomocą
WebAssembly.instantiateStreaming
Następnie pokażemy podstawowy przykład ładowania pliku Wasm i wywoływania eksportowanej funkcji. Przeglądarka musi obsługiwać instantiateStreaming.
Poniższy kod jest przykładem pobierania pliku simple.wasm z serwera i wywoływania funkcji add.
1// TypeScript: load-and-call.ts
2// Fetch and instantiate a wasm module and call its exported `add` function.
3async function run() {
4 const response = await fetch('http://localhost:3000/simple.wasm');
5 // Use instantiateStreaming when available for efficiency.
6 const { instance } = await WebAssembly.instantiateStreaming(response, {});
7 // @ts-ignore
8 const result = instance.exports.add(2, 3);
9 console.log('2 + 3 =', result);
10}
11run().catch(console.error);- Funkcje wewnątrz Wasm są przechowywane w
instance.exports. - Ponieważ TypeScript nie otrzymuje informacji o typach, należy użyć
@ts-ignorealbo stworzyć własne definicje typów.
Przebieg pracy z użyciem AssemblyScript
AssemblyScript umożliwia pisanie Wasm w składni zbliżonej do TypeScript, co czyni go przystępnym wyborem dla deweloperów TypeScript. Tutaj przygotowujemy prostą funkcję, kompilujemy ją do plików .wasm i .d.ts, a następnie wywołujemy z TypeScriptu.
1// assembly/index.ts (AssemblyScript)
2// npm install --save-dev assemblyscript
3
4// Export a simple function to add two integers.
5export function add(a: i32, b: i32): i32 {
6 return a + b;
7}- Używając narzędzia
asc(kompilatora AssemblyScript), możesz wygenerować plik.wasm, a opcjonalnie także plik definicji typów.d.ts. Aby wypróbować lokalnie, zainstalujassemblyscriptprzez npm i zbuduj projekt.
1# build commands
2# npm install --save-dev assemblyscript
3npx asc assembly/index.ts -o build/simple.wasm -t build/simple.wat --bindings esm --exportTable --sourceMap
4
5# optionally generate d.ts with --exportRuntime or use as-bind / loader toolsOto przykład pobierania i wywoływania z poziomu TypeScript.
1// ts client that loads AssemblyScript-generated wasm
2async function runAssemblyScript() {
3 const res = await fetch('http://localhost:3000/build/simple.wasm');
4 const { instance } = await WebAssembly.instantiateStreaming(res, {});
5 // @ts-ignore
6 console.log('AssemblyScript add:', instance.exports.add(10, 7));
7}
8runAssemblyScript().catch(console.error);- AssemblyScript wymaga ostrożnego zarządzania modelami pamięci i łańcuchami znaków, lecz jest bardzo prosty w obsłudze przy podstawowych obliczeniach numerycznych.
Rust + wasm-bindgen (Potężne i powszechnie stosowane rozwiązanie)
Ta sekcja wyjaśnia przebieg pracy związany z pisaniem kodu Wasm w języku Rust oraz łączeniem go z JavaScriptem lub TypeScriptem przy użyciu wasm-bindgen. Tutaj wykorzystamy prostą funkcję Fibonacciego jako przykład pokazujący, jak zaimportować wygenerowany moduł jako moduł ES.
Eksportuj funkcje po stronie Rust przy użyciu wasm-bindgen.
1// src/lib.rs (Rust)
2// install wasm-pack from https://drager.github.io/wasm-pack/installer/
3use wasm_bindgen::prelude::*;
4
5// Export a function to JavaScript using wasm-bindgen.
6#[wasm_bindgen]
7pub fn fib(n: u32) -> u32 {
8 if n <= 1 { return n; }
9 let mut a = 0;
10 let mut b = 1;
11 for _ in 2..=n {
12 let tmp = a + b;
13 a = b;
14 b = tmp;
15 }
16 b
17}- Kiedy budujesz za pomocą
wasm-packlub CLIwasm-bindgen, generowane są definicje typów dla TypeScript i wrappery JS, dzięki czemu możesz bezpośrednio importować je jako ESM.
1# build with wasm-pack
2# install wasm-pack from https://drager.github.io/wasm-pack/installer/
3wasm-pack build --target nodejs --out-dir pkgPo stronie TypeScript zaimportuj i użyj modułu ES z katalogu pkg.
1// Node.js: import WASM module built with --target web
2// import init, { fib } from '../pkg/my_wasm_module.js';
3// Node.js: import WASM module built with --target nodejs
4import wasm from '../pkg/my_wasm_module.js';
5
6async function run() {
7 //await init(); // --target web
8 console.log('fib(10)=', wasm.fib(10));
9}
10
11run().catch(console.error);wasm-packgeneruje wrappery JavaScript oraz definicje typów.d.ts, co ułatwia korzystanie z nich w TypeScript. Należy pamiętać, że gdy określiszwebjako opcję--targetw poleceniuwasm-pack, wymagana jest asynchroniczna inicjalizacja.
Praktyczny przykład współdzielenia pamięci: przekazywanie i przetwarzanie tablic (niskopoziomowo)
Podczas wymiany dużych ilości danych z Wasm, istotne jest współdzielenie ArrayBuffer dla wydajnej wymiany danych. Tutaj pokazujemy przykład z użyciem AssemblyScript, ale ta sama zasada dotyczy wasm-bindgen w Rust.
Po stronie AssemblyScript przygotuj eksportowaną funkcję do zapisu do pamięci. Na przykład funkcja podnosząca każdy element tablicy do kwadratu wyglądałaby następująco.
1// assembly/array_ops.ts (AssemblyScript)
2// Square values in place in the wasm linear memory starting at `ptr` for `len` elements.
3export function square_in_place(ptr: usize, len: i32): void {
4 // Treat memory as a pointer to 32-bit integers.
5 for (let i = 0; i < len; i++) {
6 let offset = ptr + (i << 2); // i * 4 bytes
7 let value = load<i32>(offset);
8 store<i32>(offset, value * value);
9 }
10}Aby określić ustawienia pamięci używane przez AssemblyScript, przygotuj poniższy plik asconfig.json.
1{
2 "options": {
3 "memoryBase": 0,
4 "importMemory": false,
5 "initialMemory": 1,
6 "maximumMemory": 10
7 }
8}1 npx asc assembly/array_ops.ts -o build/array_ops.wasm -t build/array_ops.wat --bindings esm --exportTable --sourceMap- Aby wywołać tę funkcję, należy skopiować
ArrayBufferdo przestrzeni pamięci Wasm i przekazać wskaźnik.
Poniżej znajduje się przykład użycia WebAssembly.Memory w TypeScript w celu skopiowania danych i wywołania funkcji.
1// TypeScript: use memory to pass array to wasm
2async function runArrayOps() {
3 const res = await fetch('http://localhost:3000/build/array_ops.wasm');
4 const { instance } = await WebAssembly.instantiateStreaming(res, {});
5 // @ts-ignore
6 const memory: WebAssembly.Memory = instance.exports.memory;
7 // Create a view into wasm memory.
8 const i32View = new Int32Array(memory.buffer);
9
10 // Example data
11 const input = new Int32Array([1, 2, 3, 4]);
12 // Choose an offset (in i32 elements) to copy data to (simple example: at index 0).
13 const offset = 0;
14 i32View.set(input, offset);
15
16 // Call wasm function: ptr in bytes, len in elements
17 // @ts-ignore
18 instance.exports.square_in_place(offset * 4, input.length);
19
20 // Read back result
21 const result = i32View.slice(offset, offset + input.length);
22 console.log('squared:', result);
23}
24runArrayOps().catch(console.error);memory.bufferto współdzielona liniowa pamięć; minimalizacja kopiowania maksymalizuje szybkość przetwarzania. Zwracaj uwagę, że wskaźniki odnoszą się do pozycji w bajtach, podczas gdyTypedArraysą zarządzane według liczby elementów — nie pomyl tych różnic.
Obsługa z zachowaniem typów: przygotuj definicje typów TypeScript
Eksporty Wasm to obiekty JavaScript, zatem dostarczenie definicji typów po stronie TypeScript ułatwia rozwój. Oto prosty przykład pliku z definicją typów.
Poniżej znajduje się minimalna definicja typów, którą możesz ręcznie stworzyć jako simple.d.ts.
1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;- Umieszczenie tego w
typeRootsw Twoimtsconfig.jsonlub użyciedeclare moduleumożliwi sprawdzanie typów.wasm-packwygodnie generuje pliki.d.tsautomatycznie, dlatego warto z nich korzystać.
Wzorce inicjalizacji w czasie wykonywania: synchroniczne vs asynchroniczne
Ponieważ moduły Wasm wymagają operacji wejścia/wyjścia (pobierania) oraz kompilacji, inicjalizacja asynchroniczna jest powszechna. Istnieje jednak także wzorzec, gdzie WebAssembly.Module jest wcześniej buforowany i instancjonowany synchronicznie.
Poniżej znajduje się podstawowa struktura kodu dla inicjalizacji WebAssembly w trybie asynchronicznym. W rzeczywistych projektach zalecany jest ten wzorzec.
1// async init pattern
2async function initWasm(url: string) {
3 const res = await fetch(url);
4 const { instance, module } = await WebAssembly.instantiateStreaming(res, {});
5 return instance;
6}- Inicjalizacja asynchroniczna pozwala elastycznie obsługiwać błędy i ładowanie lazy loading, dlatego jest najwygodniejsza w praktycznym rozwoju aplikacji. Dodatkowo kod generowany przez
wasm-packzawiera APIinit()do inicjalizacji, dlatego przyzwyczajenie się do tego przepływu ułatwi Twoją pracę.
Praktyczne uwagi dotyczące wydajności
Oto kilka punktów, które warto mieć na uwadze dla znaczących popraw wydajności. Korzystaj z tych wskazówek optymalizacyjnych przy łączeniu TypeScript i WebAssembly.
- Przy bardzo częstym wywoływaniu funkcji, narzut związany z wywołaniami między JavaScript a Wasm może stać się wąskim gardłem. Zalecamy grupowanie danych i przetwarzanie ich jednorazowo tak często, jak to możliwe.
- Alokacja i kopiowanie pamięci zwiększają obciążenie procesora. Wykorzystuj współdzielone bufory i wskaźniki, aby zminimalizować te operacje.
- Zachowaj ostrożność podczas operowania liczbami zmiennoprzecinkowymi. W TypeScript stają się typem
number, ale możesz zachować precyzję, dopasowując typy po stronie Wasm.
Podsumowanie
Łącząc TypeScript i WebAssembly, możesz uzyskać wydajność zbliżoną do natywnej w przeglądarce. To szczególnie skuteczne przy zadaniach wymagających dużej mocy obliczeniowej lub gdy chcesz wykorzystać istniejące natywne zasoby. To połączenie jest bardzo potężną opcją, gdy chcesz zwiększyć wydajność swojej aplikacji webowej.
Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.