Статьи

Документация

Дополнительно

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//wit-bindgen//wit и stappler-build//wit-bindgen/.

Сборка C/C++ в WebAssembly с помощью SDK

Для генерации кода связи: wit-bindgen stappler-build//wit-bindgen//wit --out-dir .

В 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)
  • Стандартные биндинги для целевых модулей