/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // the WebAssembly screens: modules run by the WAMR interpreter (ref/wamr), with the "eu07" import module of // script_screens_doc.md #include "stdafx.h" #include "scripting/scriptscreenprogram.h" #include "scripting/scriptscreenkernels.h" #include "utilities/Logs.h" #include "utilities/dictionary.h" #include "utilities/utilities.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace script_screens { namespace { // the wasm stack of a screen, in bytes constexpr std::uint32_t stack_size{64 * 1024}; // the type of a value in the input of a screen, as eu07.value_type() reports it enum class value_type : std::int32_t { none = 0, floating = 1, integer = 2, boolean = 3, string = 4, vec2_list = 5 }; // the input of one refresh, indexed by key. when two values share a key the later one counts, as it does for the // python screens, which get the values in the same order class input_view { public: struct entry { value_type type{value_type::none}; std::size_t index{0}; }; explicit input_view(dictionary_source const &Source) : m_source{Source} { index(Source.floats, value_type::floating); index(Source.integers, value_type::integer); index(Source.bools, value_type::boolean); index(Source.strings, value_type::string); index(Source.vec2_lists, value_type::vec2_list); } entry find(std::string_view const Key) const { if (auto const lookup{m_entries.find(Key)}; lookup != m_entries.end()) { return lookup->second; } return {}; } dictionary_source const &source() const { return m_source; } private: template void index(Sequence_ const &Values, value_type const Type) { for (std::size_t idx = 0; idx < Values.size(); ++idx) { m_entries.insert_or_assign(std::string_view{Values[idx].first}, entry{Type, idx}); } } dictionary_source const &m_source; std::unordered_map m_entries; }; std::int32_t to_int32(double const Value) { if (std::isnan(Value)) { return 0; } return static_cast(std::clamp(Value, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max()))); } std::int32_t to_int32(std::size_t const Value) { return static_cast(std::min(Value, std::numeric_limits::max())); } class wasm_program final : public program { public: wasm_program(std::string const &Script, script_screen_kind const Kind) : kind{Kind}, script{Script}, folder{substr_path(Script)} {} ~wasm_program() override { if (exec_env != nullptr) { wasm_runtime_destroy_exec_env(exec_env); } if (instance != nullptr) { wasm_runtime_deinstantiate(instance); } } wasm_program(wasm_program const &) = delete; wasm_program &operator=(wasm_program const &) = delete; bool instantiate(wasm_module_t Module); bool render(render_call &Call) override; script_screen_kind const kind; std::string const script; std::string const folder; // the call in progress input_view const *input{nullptr}; render_call *call{nullptr}; bool commands_ignored{false}; // a screen outside the cab tried to send a command, and it was logged private: bool call_i32(wasm_function_inst_t Function, std::string_view Name, std::int32_t &Result); bool present(std::uint32_t Pixels, render_call &Call); wasm_module_inst_t instance{nullptr}; wasm_exec_env_t exec_env{nullptr}; wasm_function_inst_t m_render{nullptr}; wasm_function_inst_t m_width{nullptr}; wasm_function_inst_t m_height{nullptr}; }; wasm_program *program_of(wasm_exec_env_t Env) { return static_cast(wasm_runtime_get_custom_data(wasm_runtime_get_module_inst(Env))); } struct lookup_result { input_view const *view{nullptr}; input_view::entry entry; }; lookup_result lookup(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const *screen{program_of(Env)}; if (screen == nullptr || screen->input == nullptr || Key == nullptr) { return {}; } return {screen->input, screen->input->find(std::string_view{Key, Length})}; } // the natives of the "eu07" module, which a screen imports. see script_screens_doc.md std::int32_t native_value_type(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { return static_cast(lookup(Env, Key, Length).entry.type); } double native_get_float(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; using enum value_type; switch (entry.type) { case floating: return view->source().floats[entry.index].second; case integer: return view->source().integers[entry.index].second; case boolean: return view->source().bools[entry.index].second ? 1.0 : 0.0; default: return 0.0; } } std::int32_t native_get_int(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; using enum value_type; switch (entry.type) { case floating: return to_int32(view->source().floats[entry.index].second); case integer: return view->source().integers[entry.index].second; case boolean: return view->source().bools[entry.index].second ? 1 : 0; default: return 0; } } std::int32_t native_get_bool(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; using enum value_type; switch (entry.type) { case floating: return view->source().floats[entry.index].second != 0.0 ? 1 : 0; case integer: return view->source().integers[entry.index].second != 0 ? 1 : 0; case boolean: return view->source().bools[entry.index].second ? 1 : 0; default: return 0; } } // copies the string into the buffer, as UTF-8. returns the length of the whole string, which may be more than the // buffer took, or -1 when there is no such string std::int32_t native_get_string(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length, char *Buffer, std::uint32_t const Capacity) { auto const [view, entry]{lookup(Env, Key, Length)}; if (entry.type != value_type::string) { return -1; } auto const text{utf8_text(view->source().strings[entry.index].second)}; if (Buffer != nullptr) { std::memcpy(Buffer, text.data(), std::min(text.size(), Capacity)); } return to_int32(text.size()); } std::int32_t native_get_vec2_count(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; if (entry.type != value_type::vec2_list) { return 0; } return to_int32(view->source().vec2_lists[entry.index].second.size()); } // writes the point as two f32 at Output. returns 1 on success, 0 when there is no such point std::int32_t native_get_vec2(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length, std::int32_t const Index, std::uint32_t const Output) { auto const [view, entry]{lookup(Env, Key, Length)}; if (entry.type != value_type::vec2_list || Index < 0) { return 0; } auto const &points{view->source().vec2_lists[entry.index].second}; if (static_cast(Index) >= points.size()) { return 0; } auto *instance{wasm_runtime_get_module_inst(Env)}; std::array const point{points[Index].x, points[Index].y}; if (false == wasm_runtime_validate_app_addr(instance, Output, sizeof(point))) { // the runtime has raised an exception, which ends the call return 0; } std::memcpy(wasm_runtime_addr_app_to_native(instance, Output), point.data(), sizeof(point)); return 1; } void native_command(wasm_exec_env_t Env, char const *Name, std::uint32_t const Length, std::int32_t const Param1, std::int32_t const Param2) { auto *screen{program_of(Env)}; if (screen == nullptr || screen->call == nullptr || Name == nullptr) { return; } if (screen->kind != script_screen_kind::cab) { if (false == screen->commands_ignored) { screen->commands_ignored = true; WriteLog(std::format("WASM screen \"{}\": commands are only taken from screens in the cab", screen->script)); } return; } screen->call->commands.emplace_back(std::string{Name, Length}, Param1, Param2, screen->call->vehicle); } void native_log(wasm_exec_env_t Env, char const *Text, std::uint32_t const Length) { auto const *screen{program_of(Env)}; if (screen == nullptr || Text == nullptr) { return; } WriteLog(std::format("WASM screen \"{}\": {}", screen->script, std::string_view{Text, Length})); } // reads a file from the screen's folder into the buffer. returns the size of the whole file, which may be more // than the buffer took, or -1 when there is no such file or the path leads outside the folder std::int32_t native_file_read(wasm_exec_env_t Env, char const *Path, std::uint32_t const Length, char *Buffer, std::uint32_t const Capacity) { auto const *screen{program_of(Env)}; if (screen == nullptr || Path == nullptr) { return -1; } auto const contents{read_screen_file(screen->folder, std::string_view{Path, Length})}; if (false == contents.has_value()) { return -1; } if (Buffer != nullptr) { std::memcpy(Buffer, contents->data(), std::min(contents->size(), Capacity)); } return to_int32(contents->size()); } double native_time(wasm_exec_env_t /*Env*/) { return screen_time(); } // the bytes of an image in the module's memory; an empty span for one that lies outside (the runtime then raises an // exception, which ends the call) std::span module_image(wasm_exec_env_t Env, std::uint32_t const Offset, int const Width, int const Height) { auto *instance{wasm_runtime_get_module_inst(Env)}; auto const size{image_size(Width, Height)}; if (false == wasm_runtime_validate_app_addr(instance, Offset, size)) { return {}; } return {static_cast(wasm_runtime_addr_app_to_native(instance, Offset)), size}; } // led_util's render_post, natively: Target takes (2 Width + 2) x (2 Height + 2) pixels. returns 1, or 0 for an image // that doesn't fit std::int32_t native_led_post(wasm_exec_env_t Env, std::uint32_t const Source, std::int32_t const Width, std::int32_t const Height, std::uint32_t const Target, std::int32_t const Bloom) { if (false == valid_image_side(Width) || false == valid_image_side(Height)) { return 0; } auto const source{module_image(Env, Source, Width, Height)}; auto const target{module_image(Env, Target, Width * 2 + 2, Height * 2 + 2)}; if (source.empty() || target.empty()) { return 0; } return led_post(source, Width, Height, target, Bloom != 0) ? 1 : 0; } // Pillow's resize(..., LANCZOS), natively. returns 1, or 0 for an image that doesn't fit std::int32_t native_resize_lanczos(wasm_exec_env_t Env, std::uint32_t const Source, std::int32_t const Width, std::int32_t const Height, std::uint32_t const Target, std::int32_t const TargetWidth, std::int32_t const TargetHeight) { if (false == valid_image_side(Width) || false == valid_image_side(Height) || false == valid_image_side(TargetWidth) || false == valid_image_side(TargetHeight)) { return 0; } auto const source{module_image(Env, Source, Width, Height)}; auto const target{module_image(Env, Target, TargetWidth, TargetHeight)}; if (source.empty() || target.empty()) { return 0; } return resize_lanczos(source, Width, Height, target, TargetWidth, TargetHeight) ? 1 : 0; } template NativeSymbol native(char const *Name, Function_ *Function, char const *Signature) { return NativeSymbol{Name, reinterpret_cast(Function), Signature, nullptr}; } bool wasm_program::instantiate(wasm_module_t const Module) { std::array error{}; instance = wasm_runtime_instantiate(Module, stack_size, 0, error.data(), static_cast(error.size())); if (instance == nullptr) { ErrorLog(std::format("WASM screen \"{}\": instantiation failed: {}", script, error.data())); return false; } wasm_runtime_set_custom_data(instance, this); exec_env = wasm_runtime_create_exec_env(instance, stack_size); if (exec_env == nullptr) { ErrorLog(std::format("WASM screen \"{}\": failed to create the execution environment", script)); return false; } m_render = wasm_runtime_lookup_function(instance, "screen_render"); m_width = wasm_runtime_lookup_function(instance, "screen_width"); m_height = wasm_runtime_lookup_function(instance, "screen_height"); if (m_render == nullptr || m_width == nullptr || m_height == nullptr) { ErrorLog(std::format("WASM screen \"{}\": the module has to export screen_render, screen_width and screen_height", script)); return false; } if (auto *init{wasm_runtime_lookup_function(instance, "screen_init")}; init != nullptr) { std::int32_t result{0}; if (false == call_i32(init, "screen_init", result)) { return false; } if (result != 0) { ErrorLog(std::format("WASM screen \"{}\": screen_init returned {}", script, result)); return false; } } WriteLog(std::format("WASM screen \"{}\": ready", script)); return true; } // calls a function of the screen that takes nothing and returns an i32 bool wasm_program::call_i32(wasm_function_inst_t Function, std::string_view const Name, std::int32_t &Result) { wasm_runtime_set_instruction_count_limit(exec_env, static_cast(instruction_limit)); std::array cells{}; if (false == wasm_runtime_call_wasm(exec_env, Function, 0, cells.data())) { auto const *exception{wasm_runtime_get_exception(instance)}; ErrorLog(std::format("WASM screen \"{}\": {} failed: {}", script, Name, exception != nullptr ? exception : "unknown error")); return false; } Result = static_cast(cells[0]); return true; } bool wasm_program::render(render_call &Call) { input_view const view{Call.input}; input = &view; call = &Call; std::int32_t pixels{0}; auto const rendered{call_i32(m_render, "screen_render", pixels)}; input = nullptr; call = nullptr; if (false == rendered) { return false; } // 0: the screen didn't change, keep the image there is return pixels == 0 || present(static_cast(pixels), Call); } // copies the image the screen drew, Pixels being its address in the module's memory bool wasm_program::present(std::uint32_t const Pixels, render_call &Call) { std::int32_t width{0}; std::int32_t height{0}; if (false == call_i32(m_width, "screen_width", width) || false == call_i32(m_height, "screen_height", height)) { return false; } if (false == valid_image_side(width) || false == valid_image_side(height)) { ErrorLog(std::format("WASM screen \"{}\": invalid image size {}x{}", script, width, height)); return false; } auto const size{image_size(width, height)}; if (false == wasm_runtime_validate_app_addr(instance, Pixels, size)) { ErrorLog(std::format("WASM screen \"{}\": the image lies outside the module's memory", script)); return false; } auto const *pixels{static_cast(wasm_runtime_addr_app_to_native(instance, Pixels))}; Call.pixels.assign(pixels, pixels + size); Call.width = width; Call.height = height; return true; } class wasm_runtime_host final : public runtime { public: wasm_runtime_host() : m_natives{native("value_type", &native_value_type, "(*~)i"), native("get_float", &native_get_float, "(*~)F"), native("get_int", &native_get_int, "(*~)i"), native("get_bool", &native_get_bool, "(*~)i"), native("get_string", &native_get_string, "(*~*~)i"), native("get_vec2_count", &native_get_vec2_count, "(*~)i"), native("get_vec2", &native_get_vec2, "(*~ii)i"), native("command", &native_command, "(*~ii)"), native("log", &native_log, "(*~)"), native("file_read", &native_file_read, "(*~*~)i"), native("time", &native_time, "()F"), native("led_post", &native_led_post, "(iiiii)i"), native("resize_lanczos", &native_resize_lanczos, "(iiiiii)i")} { } ~wasm_runtime_host() override { for (auto const &[script, entry] : m_modules) { if (entry.module != nullptr) { wasm_runtime_unload(entry.module); } } if (m_started) { wasm_runtime_destroy(); } } wasm_runtime_host(wasm_runtime_host const &) = delete; wasm_runtime_host &operator=(wasm_runtime_host const &) = delete; bool start(); std::unique_ptr load(std::string const &Script, script_screen_kind Kind) override; void attach_thread() override { wasm_runtime_init_thread_env(); } void detach_thread() override { wasm_runtime_destroy_thread_env(); } private: struct module_entry { std::vector bytes; // the runtime reads the module from here as long as it is loaded wasm_module_t module{nullptr}; }; wasm_module_t load_module(std::string const &Script); // the runtime keeps the array and sorts it, so it lives as long as the runtime std::array m_natives; bool m_started{false}; std::unordered_map m_modules; // the worker's }; bool wasm_runtime_host::start() { RuntimeInitArgs arguments{}; arguments.mem_alloc_type = Alloc_With_System_Allocator; arguments.native_module_name = "eu07"; arguments.native_symbols = m_natives.data(); arguments.n_native_symbols = static_cast(m_natives.size()); arguments.running_mode = Mode_Interp; if (false == wasm_runtime_full_init(&arguments)) { ErrorLog("WASM screens: failed to initialize the runtime"); return false; } wasm_runtime_set_log_level(WASM_LOG_LEVEL_ERROR); m_started = true; return true; } wasm_module_t wasm_runtime_host::load_module(std::string const &Script) { if (auto const lookup{m_modules.find(Script)}; lookup != m_modules.end()) { return lookup->second.module; } // a failure is kept as well, so the module isn't tried again for every screen using it auto &entry{m_modules[Script]}; std::ifstream file{Script, std::ios::binary}; if (false == file.is_open()) { ErrorLog(std::format("WASM screen \"{}\": file not found", Script)); return nullptr; } entry.bytes.assign(std::istreambuf_iterator{file}, std::istreambuf_iterator{}); if (entry.bytes.empty() || entry.bytes.size() > std::numeric_limits::max()) { ErrorLog(std::format("WASM screen \"{}\": invalid module size", Script)); return nullptr; } std::array error{}; entry.module = wasm_runtime_load(entry.bytes.data(), static_cast(entry.bytes.size()), error.data(), static_cast(error.size())); if (entry.module == nullptr) { ErrorLog(std::format("WASM screen \"{}\": failed to load the module: {}", Script, error.data())); } return entry.module; } std::unique_ptr wasm_runtime_host::load(std::string const &Script, script_screen_kind const Kind) { auto *module{load_module(Script)}; if (module == nullptr) { return nullptr; } auto loaded{std::make_unique(Script, Kind)}; if (false == loaded->instantiate(module)) { return nullptr; } return loaded; } } // namespace std::unique_ptr make_wasm_runtime() { auto started{std::make_unique()}; if (false == started->start()) { return nullptr; } return started; } } // namespace script_screens