Powiąże JS z Twoim wasm.
W ostatnim artykule omówiłem jak skompilować bibliotekę C do wykorzystania w internecie. Jedna rzecz która szczególnie mi się spodobała (i wielu czytelnikom), to prymitywny i nieco niezręczny sposób, musisz ręcznie zadeklarować, których funkcji modułu Wasm używasz. Oto fragment kodu, o którym mówię:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
Tutaj deklarujemy nazwy funkcji, które zaznaczyliśmy
EMSCRIPTEN_KEEPALIVE
, jakie są typy zwrotów i jakie są ich typy
. Później możemy użyć metod obiektu api
, aby wywołać
tych funkcji. Jednak użycie Wasm w ten sposób nie obsługuje ciągów tekstowych,
wymaga ręcznego przenoszenia fragmentów pamięci, co sprawia, że wiele bibliotek
Interfejsy API są bardzo pracochłonne. Czy nie ma lepszego sposobu? Dlaczego tak jest, w przeciwnym razie
o czym miałby być ten artykuł?
Zarządzanie nazwami w C++
Chociaż doświadczenie dewelopera jest dla nas wystarczającym powodem, by stworzyć narzędzie,
z tymi powiązaniami, jest ważniejszy powód: gdy skompilujesz C
lub kodu w C++, każdy plik jest skompilowany osobno. Następnie tag łączący dba
Łączenie tych tak zwanych plików obiektów i przekształcanie ich w Wasm.
. Jeśli pisze się C, nazwy funkcji są nadal dostępne w pliku obiektowym
który ma być używany przez tag łączący. Aby wywołać funkcję C, wystarczy jej nazwa,
które przekazujemy jako ciąg znaków cwrap()
.
C++ obsługuje przeciążanie funkcji, co oznacza, że można
tę samą funkcję wiele razy, pod warunkiem że podpis jest inny (np.
parametrów o innym typie). Na poziomie kompilatora ładna nazwa, np. add
zostanie zniekształcona w coś, co koduje podpis w funkcji
dla tagu łączącego. W efekcie nie moglibyśmy wyszukać funkcji
.
Wpisz embind
embind jest częścią łańcucha narzędzi Emscripten i udostępnia zestaw makr C++. które pozwalają dodawać adnotacje do kodu w C++. Możesz zadeklarować, które funkcje, wyliczenia, klas lub typów wartości, których zamierzasz użyć w języku JavaScript. Zaczynamy z prostymi w obsłudze funkcjami:
#include <emscripten/bind.h>
using namespace emscripten;
double add(double a, double b) {
return a + b;
}
std::string exclaim(std::string message) {
return message + "!";
}
EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
function("exclaim", &exclaim);
}
W porównaniu z poprzednim artykułem nie uwzględniamy już domeny emscripten.h
.
nie musimy już dodawać adnotacji do naszych funkcji za pomocą EMSCRIPTEN_KEEPALIVE
.
Zamiast tego mamy sekcję EMSCRIPTEN_BINDINGS
, w której podajemy imiona i nazwiska
który chcemy udostępnić JavaScriptowi.
Aby skompilować ten plik, możemy użyć tej samej konfiguracji (lub, jeśli chcesz, użyć tej samej konfiguracji)
Dockera), tak jak w poprzednim
. Aby użyć narzędzia Embind,
dodajemy flagę --bind
:
$ emcc --bind -O3 add.cpp
Teraz wystarczy przygotować plik HTML, który wczytuje utworzony moduł Wasm:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
Jak widzisz, nie korzystamy już z konta cwrap()
. To od razu sprawdza się
z pudełka. Co ważniejsze, nie musimy zaprzątać sobie głowy ręcznym kopiowaniem
aby pamięć działała na strunach. Embind to bezpłatne rozwiązanie,
ze sprawdzaniem typu:
To świetnie, bo możemy szybko wychwytywać błędy, zamiast zajmować się nimi ale czasem zdarzają się dość nieporęczne błędy Wasm.
Obiekty
Wiele konstruktorów i funkcji JavaScriptu korzysta z obiektów options. To miło w JavaScripcie, ale niezwykle żmudne było ręczne realizowanie ich w Wasm. Embind też tutaj może pomóc.
Na przykład wymyśliłem tę niezwykle przydatną funkcję w C++, która przetwarza i chcę pilnie użyć ich w internecie. Oto jak to zrobić:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
struct ProcessMessageOpts {
bool reverse;
bool exclaim;
int repeat;
};
std::string processMessage(std::string message, ProcessMessageOpts opts) {
std::string copy = std::string(message);
if(opts.reverse) {
std::reverse(copy.begin(), copy.end());
}
if(opts.exclaim) {
copy += "!";
}
std::string acc = std::string("");
for(int i = 0; i < opts.repeat; i++) {
acc += copy;
}
return acc;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<ProcessMessageOpts>("ProcessMessageOpts")
.field("reverse", &ProcessMessageOpts::reverse)
.field("exclaim", &ProcessMessageOpts::exclaim)
.field("repeat", &ProcessMessageOpts::repeat);
function("processMessage", &processMessage);
}
Definiuję strukturę opcji mojej funkcji processMessage()
. W
EMSCRIPTEN_BINDINGS
, mogę użyć value_object
, by JavaScript mógł zobaczyć
tę wartość w C++ jako obiekt. Mogę też użyć atrybutu value_array
, jeśli wolę
użyjemy tej wartości w C++ jako tablicy. Wiążę też funkcję processMessage()
oraz
reszta to magia. Mogę teraz wywołać funkcję processMessage()
z
JavaScript bez stałego kodu:
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
Zajęcia
Dla kompletności pokażę też, jak embind umożliwia ekspozycję przez cały czas trwania zajęć, co wnosi dużą synergię z klasami ES6. Prawdopodobnie możesz zobaczysz już wzór:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
class Counter {
public:
int counter;
Counter(int init) :
counter(init) {
}
void increase() {
counter++;
}
int squareCounter() {
return counter * counter;
}
};
EMSCRIPTEN_BINDINGS(my_module) {
class_<Counter>("Counter")
.constructor<int>()
.function("increase", &Counter::increase)
.function("squareCounter", &Counter::squareCounter)
.property("counter", &Counter::counter);
}
Jeśli chodzi o JavaScript, wygląda to prawie jak klasa natywna:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const c = new Module.Counter(22);
console.log(c.counter); // prints 22
c.increase();
console.log(c.counter); // prints 23
console.log(c.squareCounter()); // prints 529
};
</script>
A co z C?
Plik embind został napisany dla języka C++ i może być używany tylko w plikach C++, ale
Oznacza to, że nie można tworzyć linków do plików C. Aby połączyć język C i C++, wystarczy
podziel pliki wejściowe na 2 grupy: jedną dla plików C, drugą dla plików C++
uzupełnij flagi interfejsu wiersza poleceń emcc
w ten sposób:
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
Podsumowanie
Embind znacznie usprawnia pracę, używając Wasm i C/C++. W tym artykule nie omówiono wszystkich opcji dotyczących ofert. Jeśli chcesz dowiedzieć się więcej, zajrzyj do Embind's dokumentacji. Pamiętaj, że stosowanie embind może spowodować, że moduł Wasm i moduł W przypadku kodu gzip kod JavaScript glue może być większy nawet o 11 KB, szczególnie w przypadku małych modułów. Jeśli masz tylko bardzo małą powierzchnię waszej powierzchni, narzędzie może kosztować więcej niż warto sprawdzić ją w środowisku produkcyjnym. Niemniej jednak zdecydowanie spróbować.