spiegel-keyman/core/src/wasm.cpp
Eberhard Beilharz 83dca0f779
feat(core): address code review comments
Test-bot: skip
2026-01-19 16:28:52 +01:00

510 lines
19 KiB
C++

/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>
#include <vector>
#include <sstream>
#ifdef __cplusplus
#define EXTERN extern "C" EMSCRIPTEN_KEEPALIVE
#else
#define EXTERN EMSCRIPTEN_KEEPALIVE
#endif
#include <keyman_core.h>
#include "processor.hpp"
#include "state.hpp"
namespace em = emscripten;
// This little bit of magic gives us implicit bindings for any `std::vector`,
// so long as we bind `emscripten::value_object<T>`, and comes from:
// https://github.com/emscripten-core/emscripten/issues/11070#issuecomment-717675128
namespace emscripten {
namespace internal {
// Enable for std::vector<T>
template <typename T>
struct TypeID<
T,
typename std::enable_if_t<std::is_same<
typename Canonicalized<T>::type,
std::vector<typename Canonicalized<T>::type::value_type, typename Canonicalized<T>::type::allocator_type>>::value>> {
static constexpr TYPEID get() {
return TypeID<val>::get();
}
};
template <typename T, typename Allocator> struct BindingType<std::vector<T, Allocator>> {
using ValBinding = BindingType<val>;
using WireType = ValBinding::WireType;
#if (__EMSCRIPTEN_major__ == 3 && __EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ >= 60) || (__EMSCRIPTEN_major__ > 3)
// emscripten-core/emscripten#21692
static WireType toWireType(const std::vector<T, Allocator> &vec, rvp::default_tag) {
return ValBinding::toWireType(val::array(vec), rvp::default_tag{});
}
#else
static WireType toWireType(const std::vector<T, Allocator> &vec) {
return ValBinding::toWireType(val::array(vec));
}
#endif
static std::vector<T, Allocator> fromWireType(WireType value) {
return vecFromJSArray<T>(ValBinding::fromWireType(value));
}
};
} // namespace internal
} // namespace emscripten
constexpr km_core_attr const engine_attrs = {
256,
KM_CORE_LIB_CURRENT,
KM_CORE_LIB_AGE,
KM_CORE_LIB_REVISION,
KM_CORE_TECH_KMX,
"SIL International"
};
km_core_attr const& tmp_wasm_attributes() {
return engine_attrs;
}
struct km_core_option_item_wasm {
std::u16string key;
std::u16string value;
uint8_t scope;
};
class km_core_keyboard_attrs_wasm {
public:
std::u16string version_string;
std::u16string id;
std::vector<km_core_option_item_wasm> default_options;
};
class km_core_actions_wasm {
private:
std::string getCodePoints(const std::u32string& str) const {
std::stringstream result;
for (char32_t codePoint : str) {
result << "U+" << std::hex << codePoint << " ";
}
return result.str();
}
public:
unsigned int code_points_to_delete;
std::u32string output;
std::vector<km_core_option_item_wasm> persist_options;
bool do_alert;
bool emit_keystroke;
int new_caps_lock_state;
std::u32string deleted_context;
#ifndef NDEBUG
std::string toString() const {
std::string result = "Actions(delete=" + std::to_string(this->code_points_to_delete) +
", output=\"" + std::string(this->output.begin(), this->output.end()) + "\" (" + getCodePoints(this->output) + ")" +
", do_alert=" + (this->do_alert ? "true" : "false") +
", emit_keystroke=" + (this->emit_keystroke ? "true" : "false") +
", new_caps_lock_state=" + std::to_string(this->new_caps_lock_state) +
", deleted_context=\"" + std::string(this->deleted_context.begin(), this->deleted_context.end()) + "\"" +
", persist_options=[";
for (size_t i = 0; i < this->persist_options.size(); ++i) {
result += "{" + std::string(this->persist_options[i].key.begin(), this->persist_options[i].key.end()) +
": " + std::string(this->persist_options[i].value.begin(), this->persist_options[i].value.end()) +
", scope=" + std::to_string(this->persist_options[i].scope) + "}";
if (i + 1 < this->persist_options.size()) {
result += ", ";
}
}
result += "])";
return result;
}
#endif
};
class km_core_context_item_wasm: public km_core_context_item {
// Binding a union is not directly supported by emscripten, so we
// provide getters and setters.
// See https://github.com/emscripten-core/emscripten/issues/5381
public:
km_core_context_item_wasm() {}
km_core_context_item_wasm(km_core_context_item const& item) : km_core_context_item(item) {}
unsigned int getType() const {
return this->type;
}
unsigned int getCharacter() const {
return this->character;
}
void setCharacter(unsigned int character) {
this->character = character;
this->type = KM_CORE_CT_CHAR;
}
unsigned int getMarker() const {
return this->marker;
}
void setMarker(unsigned int marker) {
this->marker = marker;
this->type = KM_CORE_CT_MARKER;
}
std::string toString() const {
switch (this->type) {
case KM_CORE_CT_CHAR:
return "CHAR(" + std::to_string(this->character) + ")";
case KM_CORE_CT_MARKER:
return "MARKER(" + std::to_string(this->marker) + ")";
case KM_CORE_CT_END:
return "END";
default:
return "UNKNOWN";
}
}
};
struct km_core_context_items {
private:
std::vector<km_core_context_item_wasm> items = std::vector<km_core_context_item_wasm>();
public:
void push_back(const km_core_context_item_wasm& item) {
items.push_back(km_core_context_item_wasm(item));
}
void resize(const size_t size) {
items.resize(size);
}
size_t size() const {
return items.size();
}
const km_core_context_item_wasm& get(size_t index) const {
if (index >= items.size()) {
throw std::out_of_range("index out of range");
}
return items[index];
}
void set(size_t index, const km_core_context_item_wasm& item) {
if (index >= items.size()) {
throw std::out_of_range("index out of range");
}
items[index] = item;
}
const km_core_context_item_wasm* data() const {
return items.data();
}
std::string toString() const {
std::string result = "(" + std::to_string(items.size()) + ") [";
for (size_t i = 0; i < items.size(); ++i) {
result += items[i].toString();
if (i + 1 < items.size()) {
result += ", ";
}
}
result += "]";
return result;
}
};
template <typename T>
class CoreReturn {
public:
CoreReturn(int status = 0, const T* obj = nullptr) : status(status), object(obj) {
}
void setStatus(int status) {
this->status = status;
}
int getStatus() const {
return status;
}
void setObject(const T* obj) {
object = obj;
}
const T* getObject() const {
return object;
}
private:
int status;
const T* object;
};
km_core_context_item_wasm* km_core_context_item_create_end() {
return new km_core_context_item_wasm({KM_CORE_CT_END, {0,}, {0,}});
}
km_core_status
keyboard_load_from_blob_internal(const km_core_path_name kb_name, const std::vector<uint8_t>& buf, km_core_keyboard** keyboard);
const CoreReturn<km_core_keyboard>*
km_core_keyboard_load_from_blob_wasm(
std::string kb_name,
const emscripten::val& blob_val
) {
std::vector<uint8_t> blob = emscripten::convertJSArrayToNumberVector<uint8_t>(blob_val);
km_core_keyboard* keyboard_ptr = nullptr;
km_core_status retVal = ::keyboard_load_from_blob_internal(kb_name.c_str(), blob, &keyboard_ptr);
return new CoreReturn<km_core_keyboard>(retVal, keyboard_ptr);
}
const CoreReturn<km_core_keyboard_attrs_wasm>*
km_core_keyboard_get_attrs_wasm(const km_core_keyboard* keyboard) {
const km_core_keyboard_attrs* attrs = nullptr;
km_core_status retVal = km_core_keyboard_get_attrs(keyboard, &attrs);
km_core_keyboard_attrs_wasm* attrs_wasm = nullptr;
if (attrs) {
attrs_wasm = new km_core_keyboard_attrs_wasm();
attrs_wasm->version_string = std::u16string(attrs->version_string);
attrs_wasm->id = std::u16string(attrs->id);
for (const km_core_option_item* item = attrs->default_options; item->key; ++item) {
km_core_option_item_wasm item_wasm;
item_wasm.key = std::u16string(item->key);
item_wasm.value = std::u16string(item->value);
item_wasm.scope = item->scope;
attrs_wasm->default_options.push_back(item_wasm);
}
}
return new CoreReturn<km_core_keyboard_attrs_wasm>(retVal, attrs_wasm);
}
const CoreReturn<km_core_state>*
km_core_state_create_wasm(const km_core_keyboard* keyboard,
const std::vector<km_core_option_item_wasm>& env) {
km_core_option_item* env_c = new km_core_option_item[env.size() + 1];
for (size_t i = 0; i < env.size(); ++i) {
env_c[i].key = env[i].key.c_str();
env_c[i].value = env[i].value.c_str();
env_c[i].scope = env[i].scope;
}
env_c[env.size()] = {nullptr, nullptr, 0};
km_core_state* state = nullptr;
km_core_status retVal = km_core_state_create(keyboard, env_c, &state);
delete[] env_c;
return new CoreReturn<km_core_state>(retVal, state);
}
const CoreReturn<km_core_state>*
km_core_state_clone_wasm(const km_core_state* state) {
km_core_state* new_state = nullptr;
km_core_status retVal = km_core_state_clone(state, &new_state);
return new CoreReturn<km_core_state>(retVal, new_state);
}
int
km_core_state_context_set_if_needed_wasm(
km_core_state const* state,
std::u16string application_context
) {
return km_core_state_context_set_if_needed(const_cast<km_core_state*>(state), application_context.c_str());
}
std::u16string
km_core_state_context_debug_wasm(const km_core_state* state, int context_type) {
std::u16string context;
km_core_cu* context_c = km_core_state_context_debug(state, static_cast<km_core_debug_context_type>(context_type));
if (context_c) {
context = std::u16string(context_c);
km_core_cu_dispose(context_c);
}
return context;
}
km_core_actions_wasm*
km_core_state_get_actions_wasm(km_core_state const *state) {
km_core_actions const* actions = km_core_state_get_actions(state);
km_core_actions_wasm* actions_wasm = new km_core_actions_wasm();
actions_wasm->code_points_to_delete = actions->code_points_to_delete;
actions_wasm->output = std::u32string(actions->output);
actions_wasm->do_alert = actions->do_alert;
actions_wasm->emit_keystroke = actions->emit_keystroke;
actions_wasm->new_caps_lock_state = actions->new_caps_lock_state;
actions_wasm->deleted_context = std::u32string(actions->deleted_context);
for (const km_core_option_item* item = actions->persist_options; item->key; ++item) {
km_core_option_item_wasm item_wasm;
item_wasm.key = std::u16string(item->key);
item_wasm.value = std::u16string(item->value);
item_wasm.scope = item->scope;
actions_wasm->persist_options.push_back(item_wasm);
}
return actions_wasm;
}
int
km_core_state_options_update_wasm(
km_core_state const *state,
const std::vector<km_core_option_item_wasm>& new_options
) {
km_core_option_item* options_c = new km_core_option_item[new_options.size() + 1];
for (size_t i = 0; i < new_options.size(); ++i) {
options_c[i].key = new_options[i].key.c_str();
options_c[i].value = new_options[i].value.c_str();
options_c[i].scope = new_options[i].scope;
}
options_c[new_options.size()] = KM_CORE_OPTIONS_END;
km_core_status status = km_core_state_options_update(const_cast<km_core_state*>(state), options_c);
delete[] options_c;
return status;
}
const CoreReturn<km_core_context_items>*
km_core_context_get_wasm(
km_core_context const* context
) {
km_core_context_item* items = nullptr;
km_core_status status = km_core_context_get(context, &items);
km_core_context_items* items_wasm = new km_core_context_items();
if (status == KM_CORE_STATUS_OK) {
for (const km_core_context_item* item = items; item && item->type != KM_CORE_CT_END; item++) {
items_wasm->push_back(km_core_context_item_wasm(*item));
}
items_wasm->push_back(km_core_context_item_wasm({KM_CORE_CT_END, {0,}, {0,}}));
km_core_context_items_dispose(items);
}
return new CoreReturn<km_core_context_items>(status, items_wasm);
}
km_core_status
km_core_context_set_wasm(
km_core_context const* context,
km_core_context_items const *context_items
) {
if (!context_items) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
auto items = std::vector<km_core_context_item>(context_items->size());
for (size_t i = 0; i < context_items->size(); ++i) {
items[i] = context_items->get(i);
}
return km_core_context_set((km_core_context*)context, items.data());
}
km_core_status
km_core_process_event_wasm(
km_core_state const *state_,
km_core_virtual_key vk,
uint16_t modifier_state,
bool is_key_down,
uint16_t event_flags
) {
return km_core_process_event(state_, vk, modifier_state, is_key_down ? 1 : 0, event_flags);
}
EMSCRIPTEN_BINDINGS(core_interface) {
em::value_object<km_core_attr>("km_core_attr")
.field("max_context", &km_core_attr::max_context)
.field("current", &km_core_attr::current)
.field("revision", &km_core_attr::revision)
.field("age", &km_core_attr::age)
.field("technology", &km_core_attr::technology)
//.field("vendor", &km_core_attr::vendor, em::allow_raw_pointers())
;
em::value_object<km_core_option_item_wasm>("km_core_option_item")
.field("key", &km_core_option_item_wasm::key)
.field("value", &km_core_option_item_wasm::value)
.field("scope", &km_core_option_item_wasm::scope);
em::function("tmp_wasm_attributes", &tmp_wasm_attributes);
// Unfortunately embind has an open issue with enums and typescript where it
// only generates a type for the enum, but not the values in a usable way.
// Therefore it's not much use to expose the enum here.
// See https://github.com/emscripten-core/emscripten/issues/18585
// em::enum_<km_core_status_codes>("km_core_status_codes")
// .value("OK", KM_CORE_STATUS_OK)
// .value("NO_MEM", KM_CORE_STATUS_NO_MEM)
// .value("IO_ERROR", KM_CORE_STATUS_IO_ERROR)
// .value("INVALID_ARGUMENT", KM_CORE_STATUS_INVALID_ARGUMENT)
// .value("KEY_ERROR", KM_CORE_STATUS_KEY_ERROR)
// .value("INSUFFICENT_BUFFER", KM_CORE_STATUS_INSUFFICENT_BUFFER)
// .value("INVALID_UTF", KM_CORE_STATUS_INVALID_UTF)
// .value("INVALID_KEYBOARD", KM_CORE_STATUS_INVALID_KEYBOARD)
// .value("NOT_IMPLEMENTED", KM_CORE_STATUS_NOT_IMPLEMENTED)
// .value("OS_ERROR", KM_CORE_STATUS_OS_ERROR);
em::class_<km_core_keyboard>("km_core_keyboard");
em::class_<CoreReturn<km_core_keyboard>>("CoreKeyboardReturn")
.property("status", &CoreReturn<km_core_keyboard>::getStatus)
.property("object", &CoreReturn<km_core_keyboard>::getObject, em::allow_raw_pointers());
em::class_<CoreReturn<km_core_keyboard_attrs_wasm>>("CoreKeyboardAttrsReturn")
.property("status", &CoreReturn<km_core_keyboard_attrs_wasm>::getStatus)
.property("object", &CoreReturn<km_core_keyboard_attrs_wasm>::getObject, em::allow_raw_pointers());
em::class_<CoreReturn<km_core_context_items>>("CoreContextReturn")
.property("status", &CoreReturn<km_core_context_items>::getStatus)
.property("object", &CoreReturn<km_core_context_items>::getObject, em::allow_raw_pointers());
em::class_<km_core_keyboard_attrs_wasm>("km_core_keyboard_attrs")
.property("version_string", &km_core_keyboard_attrs_wasm::version_string)
.property("id", &km_core_keyboard_attrs_wasm::id)
.property("default_options", &km_core_keyboard_attrs_wasm::default_options);
em::class_<km_core_actions_wasm>("km_core_actions")
#ifndef NDEBUG
.function("toString", &km_core_actions_wasm::toString)
#endif
.property("code_points_to_delete", &km_core_actions_wasm::code_points_to_delete)
.property("output", &km_core_actions_wasm::output)
.property("persist_options", &km_core_actions_wasm::persist_options)
.property("do_alert", &km_core_actions_wasm::do_alert)
.property("emit_keystroke", &km_core_actions_wasm::emit_keystroke)
.property("new_caps_lock_state", &km_core_actions_wasm::new_caps_lock_state)
.property("deleted_context", &km_core_actions_wasm::deleted_context);
em::class_<km_core_state>("km_core_state");
em::class_<CoreReturn<km_core_state>>("CoreStateReturn")
.property("status", &CoreReturn<km_core_state>::getStatus)
.property("object", &CoreReturn<km_core_state>::getObject, em::allow_raw_pointers());
em::class_<km_core_context>("km_core_context");
// Since we use it in CoreReturn it has to be bound as class.
em::class_<km_core_context_items>("km_core_context_items")
.constructor()
.function("push_back(item)", &km_core_context_items::push_back)
.function("resize(size)", &km_core_context_items::resize)
.function("size", &km_core_context_items::size)
.function("get(index)", &km_core_context_items::get)
.function("set(index, item)", &km_core_context_items::set)
.function("data", &km_core_context_items::data, em::allow_raw_pointers())
.function("toString", &km_core_context_items::toString);
em::class_<km_core_context_item_wasm>("km_core_context_item")
.constructor<>()
.property("type", &km_core_context_item_wasm::getType)
.property("character", &km_core_context_item_wasm::getCharacter, &km_core_context_item_wasm::setCharacter)
.property("marker", &km_core_context_item_wasm::getMarker, &km_core_context_item_wasm::setMarker)
.function("toString", &km_core_context_item_wasm::toString);
em::function("create_end_context", &km_core_context_item_create_end, em::allow_raw_pointers());
em::function("keyboard_load_from_blob(kb_name, blob)", &km_core_keyboard_load_from_blob_wasm, em::allow_raw_pointers());
em::function("keyboard_dispose(keyboard)", &km_core_keyboard_dispose, em::allow_raw_pointers());
em::function("keyboard_get_attrs(keyboard)", &km_core_keyboard_get_attrs_wasm, em::allow_raw_pointers());
em::function("state_create(keyboard, env)", &km_core_state_create_wasm, em::allow_raw_pointers());
em::function("state_clone(state)", &km_core_state_clone_wasm, em::allow_raw_pointers());
em::function("state_dispose(state)", &km_core_state_dispose, em::allow_raw_pointers());
em::function("process_event(state, vk, modifier_state, is_key_down, event_flags)", &km_core_process_event_wasm, em::allow_raw_pointers());
em::function("state_context(state)", &km_core_state_context, em::allow_raw_pointers());
em::function("state_context_set_if_needed(state, application_context)", &km_core_state_context_set_if_needed_wasm, em::allow_raw_pointers());
em::function("state_context_clear(state)", &km_core_state_context_clear, em::allow_raw_pointers());
em::function("state_context_debug(state, context_type)", &km_core_state_context_debug_wasm, em::allow_raw_pointers());
em::function("state_get_actions(state)", &km_core_state_get_actions_wasm, em::allow_raw_pointers());
em::function("state_options_update(state, new_options)", &km_core_state_options_update_wasm, em::allow_raw_pointers());
em::function("context_get(context)", &km_core_context_get_wasm, em::allow_raw_pointers());
em::function("context_set(context, context_items)", &km_core_context_set_wasm, em::allow_raw_pointers());
}
#endif