Эмбинд Эмскриптена

Он привязывает JS к вашему Wasm!

В моей последней статье о Wasm я говорил о том, как скомпилировать библиотеку C в Wasm, чтобы можно было использовать ее в Интернете. Одна вещь, которая бросилась в глаза мне (и многим читателям), — это грубый и немного неуклюжий способ вручную объявлять, какие функции вашего модуля Wasm вы используете. Чтобы освежить ваш разум, я говорю о фрагменте кода:

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

Здесь мы объявляем имена функций, которые мы отметили EMSCRIPTEN_KEEPALIVE , каковы их типы возвращаемых значений и типы их аргументов. После этого мы можем использовать методы объекта api для вызова этих функций. Однако такое использование wasm не поддерживает строки и требует ручного перемещения фрагментов памяти, что делает использование многих библиотечных API очень утомительным. Нет ли лучшего способа? Почему да, иначе о чем была бы эта статья?

Искажение имен C++

Хотя опыт разработчика был бы достаточной причиной для создания инструмента, который помогает с этими привязками, на самом деле есть более веская причина: когда вы компилируете код C или C++, каждый файл компилируется отдельно. Затем компоновщик объединяет все эти так называемые объектные файлы и превращает их в файл Wasm. В языке C имена функций по-прежнему доступны в объектном файле для использования компоновщиком. Все, что вам нужно для вызова функции C, — это имя, которое мы предоставляем в виде строки для cwrap() .

C++, с другой стороны, поддерживает перегрузку функций, то есть вы можете реализовать одну и ту же функцию несколько раз, если сигнатуры различаются (например, параметры с разными типами). На уровне компилятора красивое имя, такое как add будет преобразовано во что-то, что кодирует подпись в имени функции для компоновщика. В результате мы больше не сможем искать нашу функцию по ее имени.

Введите вставку

embind является частью набора инструментов Emscripten и предоставляет вам набор макросов C++, которые позволяют аннотировать код C++. Вы можете объявить, какие функции, перечисления, классы или типы значений вы планируете использовать из JavaScript. Начнем с простых функций:

#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);
}

По сравнению с моей предыдущей статьей, мы больше не включаем emscripten.h , так как нам больше не нужно аннотировать наши функции с помощью EMSCRIPTEN_KEEPALIVE . Вместо этого у нас есть раздел EMSCRIPTEN_BINDINGS , в котором мы перечисляем имена, под которыми мы хотим предоставлять наши функции для JavaScript.

Для компиляции этого файла мы можем использовать ту же настройку (или, если хотите, тот же образ Docker), что и в предыдущей статье . Чтобы использовать embind, мы добавляем флаг --bind :

$ emcc --bind -O3 add.cpp

Теперь все, что осталось, — это создать HTML-файл, который загружает наш только что созданный модуль Wasm:

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    console.log(Module.add(1, 2.3));
    console.log(Module.exclaim("hello world"));
};
</script>

Как видите, мы больше не используем cwrap() . Это работает прямо из коробки. Но что еще более важно, нам не нужно беспокоиться о ручном копировании фрагментов памяти, чтобы строки работали! Embind дает вам это бесплатно вместе с проверкой типов:

Ошибки DevTools при вызове функции с неправильным количеством аргументов или аргументов неправильного типа.

Это очень здорово, поскольку мы можем обнаружить некоторые ошибки раньше, вместо того, чтобы иметь дело с иногда довольно громоздкими ошибками Wasm.

Объекты

Многие конструкторы и функции JavaScript используют объекты параметров. Это хороший шаблон в JavaScript, но его крайне утомительно реализовывать вручную в Wasm. Embind может помочь и здесь!

Например, я придумал невероятно полезную функцию C++, которая обрабатывает мои строки, и мне срочно хочется использовать ее в Интернете. Вот как я это сделал:

#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);
}

Я определяю структуру для параметров моей processMessage() . В блоке EMSCRIPTEN_BINDINGS я могу использовать value_object , чтобы JavaScript рассматривал это значение C++ как объект. Я также мог бы использовать value_array если бы предпочитал использовать это значение C++ в качестве массива. Я также привязываю processMessage() , а все остальное — это привязка магии . Теперь я могу вызвать processMessage() из JavaScript без какого-либо шаблонного кода:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

Классы

Для полноты картины я также должен показать вам, как embind позволяет предоставлять целые классы, что обеспечивает большую синергию с классами ES6. Вероятно, вы уже можете начать видеть закономерность:

#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);
}

Со стороны JavaScript это почти похоже на собственный класс:

<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>

А что насчет С?

embind был написан для C++ и может использоваться только в файлах C++, но это не означает, что вы не можете ссылаться на файлы C! Чтобы смешать C и C++, вам нужно всего лишь разделить входные файлы на две группы: одну для C и одну для файлов C++, а также дополнить флаги CLI для emcc следующим образом:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

Заключение

Embind значительно упрощает работу разработчика при работе с Wasm и C/C++. В этой статье не рассматриваются все варианты встраиваемых предложений. Если вам интересно, рекомендую продолжить изучение документации embind . Имейте в виду, что использование embind может увеличить как ваш модуль Wasm, так и ваш связующий код JavaScript на 11 КБ при сжатии gzip — особенно на небольших модулях. Если у вас очень маленькая поверхность, встраивание может стоить дороже, чем оно того стоит в производственной среде! Тем не менее, вам обязательно стоит попробовать.