| #if defined(WITH_RIVE_SCRIPTING_WASM) && defined(__EMSCRIPTEN__) | |
| #include "rive/wasm/browser_scripting_vm.hpp" | |
| #include <emscripten/em_js.h> | |
| #include <emscripten/emscripten.h> | |
| #include <emscripten/eventloop.h> | |
| #include <emscripten/stack.h> | |
| #include <cassert> | |
| #include <cstdlib> | |
| using namespace rive; | |
| // clang-format off | |
| EM_JS_DEPS(riveWebScripting, | |
| "$UTF8ToString,$stringToUTF8,$stringToNewUTF8," | |
| "$stackSave,$stackAlloc,$stackRestore,$wasmTable,malloc,free"); | |
| EM_JS(double, riveWebTimeOrigin, (), { return performance.timeOrigin; }); | |
| // The runner is web/rive_script_runner.mjs, installed on each thread; only | |
| // librive sees its heap and exports, so they are handed over here. | |
| EM_JS(int, riveWebAttachRunner, (double pageOrigin), { | |
| const runner = Module['riveScripting']; | |
| if (!runner) | |
| { | |
| return 0; | |
| } | |
| const withUtf8 = (text, fn) => { | |
| const ptr = stringToNewUTF8(text); | |
| fn(ptr); | |
| _free(ptr); | |
| }; | |
| runner.attach({ | |
| exports: Module, | |
| hostCall: (index) => { | |
| const fn = HEAPU32[(_rive_web_calls() >> 2) + index]; | |
| return fn === 0 ? undefined : wasmTable.get(fn); | |
| }, | |
| heapU8: () => HEAPU8, | |
| heapF64: () => HEAPF64, | |
| stackSave: stackSave, | |
| stackAlloc: stackAlloc, | |
| stackRestore: stackRestore, | |
| malloc: _malloc, | |
| free: _free, | |
| utf8: (ptr) => UTF8ToString(ptr), | |
| writeUtf8: (text, ptr, capacity) => stringToUTF8(text, ptr, capacity), | |
| memory: () => wasmMemory, | |
| // Builds that cannot play processors leave this export out. | |
| processorEvent: (processor, kind, text) => | |
| withUtf8(text, (ptr) => Module['_rive_web_processor_event'](processor, kind, ptr)), | |
| laneLog: (lane, text) => withUtf8(text, (ptr) => _rive_web_lane_log(lane, ptr)), | |
| laneFail: (lane, text) => withUtf8(text, (ptr) => _rive_web_lane_fail(lane, ptr)), | |
| laneStop: (lane) => { | |
| const text = _rive_web_lane_fuel(lane); | |
| return text === 0 ? null : UTF8ToString(text); | |
| }, | |
| pageOrigin: pageOrigin, | |
| }); | |
| return 1; | |
| }); | |
| #ifdef __EMSCRIPTEN_PTHREADS__ | |
| // The page hands the lane modules it compiles to its idle threads, as a | |
| // running one never takes a message and would only pin them. Its parallel | |
| // team are threads it started, whose workers it reaches by thread. | |
| EM_JS(void, riveWebAttachThreads, (), { | |
| Module['riveScripting'].attachThreads( | |
| () => PThread.unusedWorkers, | |
| (thread) => PThread.pthreads[thread]); | |
| }); | |
| #endif | |
| EM_JS(void, riveWebRegister, (uint32_t vm, const uint8_t* bytes, uint32_t size), { | |
| Module['riveScripting'].register(vm, HEAPU8.subarray(bytes, bytes + size)); | |
| }); | |
| EM_JS(void, riveWebRelease, (uint32_t vm), { | |
| Module['riveScripting']?.release(vm); | |
| }); | |
| // Null when the VM has an instance, else a malloc'd error. | |
| EM_JS(char*, riveWebStart, (uint32_t vm), { | |
| const error = Module['riveScripting'].start(vm); | |
| return error === null ? 0 : stringToNewUTF8(error); | |
| }); | |
| EM_JS(int, riveWebPreparing, (uint32_t vm), { | |
| return Module['riveScripting'].preparing(vm); | |
| }); | |
| EM_JS(int, riveWebHasExport, (uint32_t vm, const char* name), { | |
| return Module['riveScripting'].hasExport(vm, name); | |
| }); | |
| EM_JS(int, riveWebCall, | |
| (uint32_t vm, const char* name, uint32_t argc, const double* argv, | |
| double* result, char* trap, uint32_t trapCapacity), { | |
| return Module['riveScripting'].call( | |
| vm, name, argc, argv, result, trap, trapCapacity); | |
| }); | |
| EM_JS(void, riveWebRaise, (uint32_t vm, const char* message), { | |
| Module['riveScripting'].raise(vm, message); | |
| }); | |
| // A uint32_t arrives in JS signed. | |
| EM_JS(int, riveWebReadMemory, | |
| (uint32_t vm, uint32_t appAddr, uint8_t* dst, uint32_t size), { | |
| return Module['riveScripting'].read(vm, appAddr >>> 0, dst, size >>> 0); | |
| }); | |
| EM_JS(int, riveWebWriteMemory, | |
| (uint32_t vm, uint32_t appAddr, const uint8_t* src, uint32_t size), { | |
| return Module['riveScripting'].write(vm, appAddr >>> 0, src, size >>> 0); | |
| }); | |
| EM_JS(int, riveWebStringLength, (uint32_t vm, uint32_t appAddr), { | |
| return Module['riveScripting'].stringLength(vm, appAddr >>> 0); | |
| }); | |
| EM_JS(uint32_t, riveWebMemoryPages, (uint32_t vm), { | |
| return Module['riveScripting'].pages(vm); | |
| }); | |
| // clang-format on | |
| // Static init runs only on the page's thread, so pool threads see its origin. | |
| static const double s_pageOrigin = riveWebTimeOrigin(); | |
| extern "C" int riveWebAttach() { return riveWebAttachRunner(s_pageOrigin); } | |
| #ifdef __EMSCRIPTEN_PTHREADS__ | |
| // A thread of the page's parallel team runs no C++ of its own. It stays alive | |
| // in its worker's event loop, where the runner's team loop runs once the page | |
| // sends it the team's words, and where it can still take each new module's | |
| // memory. | |
| extern "C" EMSCRIPTEN_KEEPALIVE uint32_t rive_web_parallel_team_thread() | |
| { | |
| pthread_t thread; | |
| auto enter = [](void*) -> void* { | |
| emscripten_runtime_keepalive_push(); | |
| return nullptr; | |
| }; | |
| if (pthread_create(&thread, nullptr, enter, nullptr) != 0) | |
| { | |
| return 0; | |
| } | |
| pthread_detach(thread); | |
| return (uint32_t)(uintptr_t)thread; | |
| } | |
| #endif | |
| extern "C" EMSCRIPTEN_KEEPALIVE void rive_web_vm_module_export( | |
| uint32_t vm, | |
| const char* name) | |
| { | |
| ((BrowserScriptingVM*)(uintptr_t)vm)->noteModuleExport(name); | |
| } | |
| // Pages print what this returns as warnings. | |
| extern "C" EMSCRIPTEN_KEEPALIVE const char* rive_web_vm_frame_boundary( | |
| uint32_t vm) | |
| { | |
| return ((BrowserScriptingVM*)(uintptr_t)vm)->frameBoundaryWarnings(); | |
| } | |
| // The runner's outcome when JS unwound librive frames. | |
| static constexpr int kHostFault = 3; | |
| // Script and host calls nest on librive's small stack, which release builds | |
| // never check, so a call needs this much left to cross and come back. | |
| static constexpr size_t kCallStackReserve = 32 * 1024; | |
| static uint32_t handleOf(const BrowserScriptingVM* vm) | |
| { | |
| return (uint32_t)(uintptr_t)vm; | |
| } | |
| std::unique_ptr<BrowserScriptingVM> BrowserScriptingVM::make( | |
| Span<const uint8_t> module, | |
| Factory* factory) | |
| { | |
| std::unique_ptr<BrowserScriptingVM> vm(new BrowserScriptingVM()); | |
| vm->m_factory = factory; | |
| #ifdef __EMSCRIPTEN_PTHREADS__ | |
| vm->m_owner = pthread_self(); | |
| #endif | |
| if (!riveWebAttach()) | |
| { | |
| fprintf( | |
| stderr, | |
| "wasm script module skipped: the page has not installed the " | |
| "rive script runner\n"); | |
| return nullptr; | |
| } | |
| #ifdef __EMSCRIPTEN_PTHREADS__ | |
| riveWebAttachThreads(); | |
| #endif | |
| riveWebRegister(handleOf(vm.get()), module.data(), (uint32_t)module.size()); | |
| vm->attachLanes(module); | |
| return vm; | |
| } | |
| BrowserScriptingVM::~BrowserScriptingVM() | |
| { | |
| assertOwnerThread(); | |
| riveWebRelease(handleOf(this)); | |
| } | |
| void BrowserScriptingVM::assertOwnerThread() const | |
| { | |
| #ifdef __EMSCRIPTEN_PTHREADS__ | |
| assert(pthread_equal(m_owner, pthread_self())); | |
| #endif | |
| } | |
| BrowserScriptingVM::Start BrowserScriptingVM::start() | |
| { | |
| if (m_started) | |
| { | |
| return m_L != 0 ? Start::started : Start::failed; | |
| } | |
| if (char* error = riveWebStart(handleOf(this))) | |
| { | |
| m_lastError = std::string("module instantiate failed: ") + error; | |
| free(error); | |
| return riveWebPreparing(handleOf(this)) ? Start::preparing : Start::failed; | |
| } | |
| m_started = true; | |
| if (hostBudget()) | |
| { | |
| fprintf( | |
| stderr, | |
| "wasm script module expects a watchdog to bound its calls, " | |
| "which the browser does not have: a runaway script will hang " | |
| "the page\n"); | |
| } | |
| return bootModule() ? Start::started : Start::failed; | |
| } | |
| bool BrowserScriptingVM::valid() const | |
| { | |
| return m_started && m_L != 0 && !m_hostFault && !poisoned(); | |
| } | |
| void* BrowserScriptingVM::outOfBounds() | |
| { | |
| if (m_callDepth != 0) | |
| { | |
| raiseModuleError("out of bounds memory access"); | |
| } | |
| return nullptr; | |
| } | |
| void* BrowserScriptingVM::resolveModulePtr(uint32_t appAddr, uint32_t size) | |
| { | |
| if (!fitsModule(appAddr, size)) | |
| { | |
| return outOfBounds(); | |
| } | |
| std::vector<uint8_t> bytes(size); | |
| if (!riveWebReadMemory(handleOf(this), appAddr, bytes.data(), size)) | |
| { | |
| return outOfBounds(); | |
| } | |
| m_staged.push_back({appAddr, bytes, std::move(bytes)}); | |
| return m_staged.back().bytes.data(); | |
| } | |
| void* BrowserScriptingVM::resolveModuleWritePtr(uint32_t appAddr, uint32_t size) | |
| { | |
| if (!fitsModule(appAddr, size)) | |
| { | |
| return outOfBounds(); | |
| } | |
| // An empty original never matches, so the whole range is written back. | |
| m_staged.push_back({appAddr, std::vector<uint8_t>(size), {}}); | |
| return m_staged.back().bytes.data(); | |
| } | |
| // A script picks the size, so check it fits before allocating it here. | |
| bool BrowserScriptingVM::fitsModule(uint32_t appAddr, uint32_t size) | |
| { | |
| uint64_t limit = (uint64_t)riveWebMemoryPages(handleOf(this)) * 65536; | |
| return (uint64_t)appAddr + size <= limit; | |
| } | |
| bool BrowserScriptingVM::copyToModule( | |
| uint32_t appAddr, | |
| const void* src, | |
| uint32_t size) | |
| { | |
| if (riveWebWriteMemory(handleOf(this), appAddr, (const uint8_t*)src, size)) | |
| { | |
| return true; | |
| } | |
| outOfBounds(); | |
| return false; | |
| } | |
| const char* BrowserScriptingVM::resolveModuleString(uint32_t appAddr) | |
| { | |
| int length = riveWebStringLength(handleOf(this), appAddr); | |
| return length < 0 | |
| ? (const char*)outOfBounds() | |
| : (const char*)resolveModulePtr(appAddr, (uint32_t)length + 1); | |
| } | |
| WasmScriptingVM::CallOutcome BrowserScriptingVM::callF64( | |
| const char* name, | |
| const double* args, | |
| uint32_t argc, | |
| double* result) | |
| { | |
| assertOwnerThread(); | |
| if (m_hostFault || poisoned()) | |
| { | |
| return CallOutcome::trapped; | |
| } | |
| // Writes through staged pointers must land before the module runs. | |
| for (StagedRange& range : m_staged) | |
| { | |
| if (range.bytes == range.original) | |
| { | |
| continue; | |
| } | |
| riveWebWriteMemory( | |
| handleOf(this), | |
| range.appAddr, | |
| range.bytes.data(), | |
| (uint32_t)range.bytes.size()); | |
| } | |
| m_staged.clear(); | |
| if (emscripten_stack_get_free() < kCallStackReserve) | |
| { | |
| recordTrap(name, "script calls nest too deep for the host stack"); | |
| return CallOutcome::trapped; | |
| } | |
| char trap[512]; | |
| trap[0] = 0; | |
| int outcome; | |
| { | |
| ScriptCallScope callScope(this); | |
| outcome = riveWebCall( | |
| handleOf(this), | |
| name, | |
| argc, | |
| args, | |
| result, | |
| trap, | |
| sizeof(trap)); | |
| } | |
| if (outcome == kHostFault) | |
| { | |
| m_hostFault = true; | |
| poison(); | |
| m_lastError = std::string("librive faulted in a host call, so its " | |
| "script module stops: ") + | |
| trap; | |
| recordTrap(name, m_lastError.c_str()); | |
| return CallOutcome::trapped; | |
| } | |
| if (outcome == (int)CallOutcome::trapped) | |
| { | |
| recordTrap(name, trap); | |
| } | |
| return (CallOutcome)outcome; | |
| } | |
| WasmScriptingVM::CallOutcome BrowserScriptingVM::callModuleChecked( | |
| const char* name, | |
| uint32_t argc, | |
| uint32_t* argv, | |
| uint32_t* result) | |
| { | |
| double args[8]; | |
| assert(argc <= 8); | |
| for (uint32_t i = 0; i < argc; i++) | |
| { | |
| args[i] = argv[i]; | |
| } | |
| double value = 0; | |
| CallOutcome outcome = callF64(name, args, argc, &value); | |
| if (outcome == CallOutcome::ok) | |
| { | |
| *result = (uint32_t)(int64_t)value; | |
| } | |
| return outcome; | |
| } | |
| bool BrowserScriptingVM::hasExport(const char* name) | |
| { | |
| if (!m_started) | |
| { | |
| return false; | |
| } | |
| auto it = m_exports.find(name); | |
| if (it == m_exports.end()) | |
| { | |
| it = m_exports.emplace(name, riveWebHasExport(handleOf(this), name) != 0) | |
| .first; | |
| } | |
| return it->second; | |
| } | |
| void BrowserScriptingVM::raiseModuleError(const char* message) | |
| { | |
| m_moduleErrorDetail = message; | |
| riveWebRaise(handleOf(this), message); | |
| } | |
| bool BrowserScriptingVM::memoryLimits(uint32_t* pages, uint32_t* maxPages) const | |
| { | |
| *pages = riveWebMemoryPages(handleOf(this)); | |
| // The declared ceiling is not readable from the page. | |
| *maxPages = 0; | |
| return *pages != 0; | |
| } | |
| #endif |