blob: 307b0b86d855334dc42d58c4a80b02d8a861352f [file] [edit]
#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