Embind sakralny

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:

Błędy w Narzędziach deweloperskich po wywołaniu funkcji z niewłaściwą liczbą argumentów
lub argumenty zawierają błędne
typ

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 sprawić, ż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ć.