WebAssembly
Сборка связей (bindings)
Модуль поддержки WebAssembly: stappler_wasm
. Для работы используется интерпретатор WAMR версии 1.3.3 (ABI версии 2.x отличается). Переход на старшую версию возможен позже.
Для сборки интерфейса используется wit-bindgen и описание на языке WIT.
Для того, чтобы система распознавала предоставляемые со стороны SDK интерфейсы, они помечаются как /* @import */
, интерфейс со стороны WebAssembly определяется как /* @export */
. Это необходимо из-за текущий особенностей работы wit-bindgen, не допускающих иерархию модулей.
/* @import */ interface data {
}
/* @export */ interface app {
run: func() -> bool;
}
Система автоматически генерирует описание world
в зависимости от подключенных модулей. Пример генерации:
// Autogenerated by makefile (wasm/apply.mk)
package stappler:wasm;
world stappler {
import wasm;
import filesystem;
import data;
import core;
export app;
export initialize: func();
export finalize: func();
}
Код на WebAssembly может быть написан на любом поддерживаемом языке (с поддержкой wit-bindgen или в ручном режиме связи), однако, для работы с SDK требуется минимальный модуль, собираемый с помощью SDK. Для сборки этого модуля необходимо указать это в корневом Makefile:
...
LOCAL_WASM_MODULE := app.wasm
...
Этот модуль должен быть затем слинкован с основным модулем логики приложения. При использовании системы сборки SDK, это делается автоматически. Также, будет собран готовый набор файлов для wit-bindgen. Код для сборки приложения WebAssembly на C/C++ создаётся автоматически.
Другие средства сборки могут получить доступ к ним по адресу stappler-build/
и stappler-build/
.
Сборка C/C++ в WebAssembly с помощью SDK
Для генерации кода связи: wit-bindgen
.
В SDK для сборки испольуется WASI SDK. Для основных целей используется wasm32-wasi-threads
, для Windows: wasm32-wasi
. На текущий момент, в Windows не поддерживается многопоточная работа WebAssembly.
Используемый набор расширенных функций WebAssembly: -mreference-types -mbulk-memory -msign-ext -mmultivalue -mtail-call
. С позиции компонентной модели WebAssembly, собираемый модуль имеет тип reactor
.
Параметры сборки в Makefile:
LOCAL_WASM_MODULE
: название собираемого модуля WebAssembly
LOCAL_WASM_DIRS
: пути для поиска исходных файлов WebAssembly
LOCAL_WASM_OBJS
: файля для сборки в WebAssembly
LOCAL_WASM_INCLUDES_DIRS
: пути для рекурсивного поиска заголовков WebAssembly
LOCAL_WASM_INCLUDES_OBJS
: пути для поиска заголовков WebAssembly
LOCAL_WASM_CFLAGS
: дополнительные флаги сборки C WebAssembly
LOCAL_WASM_CXXFLAGS
: дополнительные флаги сборки C++ WebAssembly
Сборка другими средствами
Необходимо
- Подключить к системе сборки в качестве зависимости систему сборки SDK (например, вызов
make -C <путь к проекту> host-release
. - Собрать биндинги из
stappler-build/host/wit-bindgen/release/wit
- В конце сборки слинковать финальный модуль с модулем
stappler-build/host/wasm/clang/release/
..wasm
На стороне приложения
Пример простого запуска функции WebAssembly:
// Загрузка модуля, имя должно соотвествовать описанию в WIT
auto mod = Rc<Module>::create("stappler:wasm/app", FilePath(...));
if (!mod) {
return -1;
}
// Создание рабочего экземпляра модуля
auto inst = Rc<ModuleInstance>::create(mod);
if (!inst) {
return -1;
}
// Создание исполнительного окружения (стека вызовов)
auto env = Rc<ExecEnv>::create(inst);
if (!env) {
return -1;
}
// Поиск функции в модуле (можно использовать полностью квалифицированное имя в WIT, или имя функции)
// В последнем случае, имя будет дополнено автоматически
auto fn = inst->lookup("run");
// Запуск функции, возвращающей один аргумент
auto res = fn->call1(env);
// Доступ к результату как к 32-битному целому
if (res.of.i32) {
printf("GREAT! PASS ALL CHKs\n");
}
Пример определения внешнего модуля для WebAssembly в коде приложения:
// Используется WIT-нотация для имён и нотация WAMR для сигнатуры функций
static NativeSymbol stapper_data_symbols[] = {
NativeSymbol{"read", (void *)&StapplerDataRead, "(*~*~)i", NULL},
NativeSymbol{"read-file", (void *)&StapplerDataReadFile, "(*~*~)i", NULL},
NativeSymbol{"[constructor]value", (void *)&stappler_wasm_data_constructor_value, "()i", NULL},
NativeSymbol{"[method]value.copy", (void *)&StapplerDataCopy, "(i)i", NULL},
NativeSymbol{"[method]value.write-to-file", (void *)&StapplerDataWriteToFile, "(i*~)i", NULL},
NativeSymbol{"[method]value.write-to-memory", (void *)&StapplerDataWriteToMemory, "(ii*)i", NULL},
NativeSymbol{"[method]value.to-string", (void *)&StapplerDataToString, "(ii*)", NULL},
};
// Название модуля из WIT
static NativeModule s_dataModule("stappler:wasm/data", stapper_data_symbols, sizeof(stapper_data_symbols) / sizeof(NativeSymbol));
В сигнатуре функций каждый символ внутри скобок обозначет аргумент. Значение после скобки - результат.
Значение символов:
- 'i': i32
- 'I': i64
- 'f': f32
- 'F': f64
- 'r': externref
- '*': адрес буфера в памяти WASM (конвертируется в указатель на стороне приложения автоматически)
- '~': размер буфера в байтах для предыдущего аргумента (i32)
- '$': строковый буфер на стороне WASM
Особенности работы
При передаче на сторону приложения, ABI WIT разворачивает составные типы в отдельные аргументы. Потому, передача по указателю для больших структур предпочтительнее. Далее, пример, демонстрирующий проблему:
/* interface webserver {
record host-component-data {
on-child-init: u32,
on-storage-init: u32,
on-heartbeat: u32,
userdata: u32,
}
resource host-component {
constructor(host: borrow<host>, info: borrow<host-component-info>, data: host-component-data);
}
} */
// Автогенерированный интерфейс
stappler_wasm_webserver_own_host_component_t stappler_wasm_webserver_constructor_host_component(stappler_wasm_webserver_borrow_host_t host, stappler_wasm_webserver_borrow_host_component_info_t info, stappler_wasm_webserver_host_component_data_t *data) {
int32_t ret = __wasm_import_stappler_wasm_webserver_constructor_host_component((host).__handle, (info).__handle,
(int32_t) ((*data).on_child_init), (int32_t) ((*data).on_storage_init), (int32_t) ((*data).on_heartbeat), (int32_t) ((*data).userdata));
return (stappler_wasm_webserver_own_host_component_t) { ret };
}
// На стороне приложения
static uint32_t stappler_wasm_webserver_constructor_host_component(wasm_exec_env_t exec_env, uint32_t hostHandle, uint32_t infoHandle,
uint32_t onChildInit, uint32_t onStorageInit, uint32_t onHeartbeat, uint32_t userdata) {
...
}
Возврат строк и буферов в WIT выполняется через аргументы по указателю, а не через тип возврата:
/* interface data {
resource value {
to-string: func(format: u32) -> string;
}
} */
static uint32_t StapplerDataToString(wasm_exec_env_t exec_env, uint32_t handle, uint32_t fmt, ListOutput *out) {
auto env = ExecEnv::get(exec_env);
auto mod = env->getInstance();
auto val = mod->getObject<ValueContainer>(handle);
if (!val) {
log::error("wasm::Runtime", "[method]value.to-string: invalid handle");
return false;
}
auto d = data::toString(*val->value, data::EncodeFormat::Format(fmt));
out->setData(mod, d.data(), d.size());
return true;
}
static NativeSymbol stapper_data_symbols[] = {
NativeSymbol{"[method]value.to-string", (void *)&StapplerDataToString, "(ii*)", NULL},
};
Для передачи объектов придожения в WebAssembly используется набор функций ModuleInstance
. Объект хранится модулем до тех пор, пока не будет удалён явно. К объекту можно присоединить деструктор, который будет вызван при удалении.
На текущий момент WIT не поддерживает externref, потому на стороне WebAssembly объект представлен уникальным 32-битным идентификатором.
class ModuleInstance final : public Ref {
public:
// ...
template <typename T>
uint32_t addHandle(T *, Function<void()> &&dtor = nullptr);
template <typename T>
uint32_t getHandle(T *) const;
template <typename T = void>
T *getObject(uint32_t) const;
uint32_t getHandle(void *) const;
void removeHandle(uint32_t);
void removeObject(void *);
// ...
};
Функции, определённые на стороне WebAssembly, представлены 32-битным идентификатором их позиции в таблице функций. Для установки обратного вызова в WebAssembly со стороны приложения необходимо получить этот идентификатор как 32-битное значение, и вызывать функцию с помощью ExecEnv::callIndirect
. Результат вызова будет записан в буфер, предназначенный для аргументов. 64-битные значения занимают 2 32-битных слота. WIT на текущий момент не имеет способа описания функциональных типов, потому функция описывается как u32.
/* interface data {
enum foreach-result {
continue,
stop,
drop
}
resource value {
foreach-array-by-idx: func(idx: u32, callback: u32, userdata: u32) -> bool;
}
} */
static uint32_t stappler_wasm_data_process_foreach_array(wasm_exec_env_t exec_env, ModuleInstance *inst, ValueContainer *val,
Value::ArrayType &arr, uint32_t callback, uint32_t userdata) {
ValueContainer iterContainer;
iterContainer.source = val->source;
auto iterHandle = inst->addHandle(&iterContainer);
uint32_t idx = 0;
uint32_t args[3];
auto it = arr.begin();
while (it != arr.end()) {
args[0] = userdata;
args[1] = idx;
args[2] = iterHandle;
iterContainer.value = & (*it);
if (!wasm_runtime_call_indirect(exec_env, callback, 3, args)) {
log::error("wasm::Runtime", __FUNCTION__, ": fail to call_indirect");
inst->removeObject(&iterContainer);
return 0;
}
ForeachResult res = ForeachResult(args[0]);
switch (res) {
case ForeachResult::Continue:
++ it;
break;
case ForeachResult::Stop:
it = arr.end();
break;
case ForeachResult::Drop:
if (iterContainer.source->readOnlySource) {
log::error("wasm::Runtime", __FUNCTION__, ": fail to drop in read-only object");
it = arr.end();
} else {
it = arr.erase(it);
}
break;
}
++ idx;
}
inst->removeObject(&iterContainer);
return 1;
}
Текущий статус
Поддержка WebAssembly экспериментальна, но готова к прикладному использованию как MVP. Идёт разработка системы автоматической генерации WIT на основе исходного кода SDK и разработка стандартных биндингов к модулям.
Нерешённые проблемы:
- Описание функциональных типов
- Отслеживание ссылок на объекты на стороне WebAssembly (возможно, внедрение GC и WAMR 2.x)
- Стандартные биндинги для целевых модулей