Он привязывает 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
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 предоставляет вам это бесплатно, вместе с проверкой типов:

Это очень здорово, поскольку мы можем выявлять некоторые ошибки на ранних стадиях, вместо того чтобы разбираться с порой довольно громоздкими ошибками 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()
, а остальное — магия embind. Теперь я могу вызывать функцию 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 . Имейте в виду, что использование Embind может увеличить размер как вашего wasm-модуля, так и вашего связующего JavaScript-кода до 11 КБ при сжатии gzip — особенно заметно для небольших модулей. Если у вас очень маленькая область wasm, Embind может стоить дороже, чем он того стоит в рабочей среде! Тем не менее, вам определённо стоит попробовать.