blob: 0a2ce73d7de5a621f4e01441b07798466d0ed009 [file]
#ifndef _RIVE_WASM_SCRIPTING_VM_HPP_
#define _RIVE_WASM_SCRIPTING_VM_HPP_
#ifdef WITH_RIVE_SCRIPTING_WASM
#include "rive/async/threading.hpp"
#include "rive/refcnt.hpp"
#include "rive/scripted/script_backend.hpp"
#include "rive/span.hpp"
#include "rive/viewmodel/write_attribution.hpp"
#include "rive/wasm/wasm_lane_layout_gen.hpp"
#include "rive/wasm/wasm_lane_policy.hpp"
#include "rive/wasm/wasm_signal_wait.hpp"
#include <chrono>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <vector>
namespace rive
{
class WasmJobs;
class WasmParallel;
class WasmWorkers;
class WasmProcessors;
class WasmLane;
class WasmAudioDecode;
struct HostAudioStream;
class AudioSource;
struct VMPollList;
class SharedRegion;
/// Executes a .riv's self contained wasm script module (VM + bindings +
/// compiled scripts) under WAMR. One instance per file instance. The module
/// needs no instantiation time wiring beyond emscripten's setjmp/longjmp glue,
/// provided here as native symbols per the luau_wasm EH spike. Implements the
/// ScriptBackend seam over the module's host_obj_* exports, whose bodies
/// mirror the Luau backend.
class Factory;
class File;
class RenderPaint;
class RenderPath;
class TransformComponent;
class ViewModel;
class WorkTask;
class WasmScriptingVM;
struct PathEffectPaintWire;
namespace scriptnet
{
struct HttpResponse;
}
/// A debugger's view of a module baked with rasc's line probes. Everything
/// arrives on the module's thread: the probes from inside the module, the
/// call brackets around each host call into it, and a stopped probe may
/// block until the debugger resumes.
class WasmDebugHooks
{
public:
virtual ~WasmDebugHooks() = default;
virtual void onEnter(
WasmScriptingVM& vm,
uint32_t function,
uint32_t line) = 0;
/// True when execution stopped here, so the module re-arms its budget.
virtual bool onLine(WasmScriptingVM& vm, uint32_t line) = 0;
virtual void onLeave(WasmScriptingVM& vm) = 0;
virtual void onCallBegin(WasmScriptingVM& vm) = 0;
/// trap names the exception of a call that died, null for a normal
/// return; frames the probes left open are the debugger's to drop.
virtual void onCallEnd(WasmScriptingVM& vm, const char* trap) = 0;
/// The VM is going away.
virtual void onDetach(WasmScriptingVM& vm) = 0;
};
class WasmScriptingVM : public ScriptBackend
{
public:
/// factory makes the real render objects the module's handles resolve to;
/// it must outlive the VM. print is installed before instantiation, the
/// only way to see output from module start: rasc runs every script's top
/// level there, riveRegister included. A module that fails to start sets
/// outError, unless the boot hook gave it an error sink, which hears the
/// start failure line instead.
#ifndef __EMSCRIPTEN__
static std::unique_ptr<WasmScriptingVM> make(
Span<const uint8_t> module,
Factory* factory,
std::string& outError,
std::function<void(const char*, size_t)> print = {});
#endif
~WasmScriptingVM();
/// One live host object per module handle: 24-bit slot, 7-bit generation,
/// type tag checked on resolve. Stale or foreign handles resolve null.
struct HandleTable
{
enum class Tag : uint8_t
{
empty,
path,
measure,
paint,
renderer,
shader,
object,
viewModelInstance,
instanceValue,
image,
font,
buffer,
meshInstances,
canvas,
gpuCanvas,
gpuPass,
gpuBuffer,
gpuTexture,
gpuSampler,
gpuTextureView,
gpuShaderModule,
gpuBindGroupLayout,
gpuBindGroup,
gpuPipeline,
dataContext,
artboard,
animation,
node,
audioSource,
audioSound,
audioStream,
drawable,
text,
transitionChild,
// context:decodeFile's files and their bindable artboards.
riveFile,
bindableArtboard,
// rive_lanes_v1's jobs, workers and audio processors.
lane,
count,
};
struct Slot
{
Tag tag = Tag::empty;
uint8_t generation = 0;
void* object = nullptr;
};
std::vector<Slot> slots;
std::vector<uint32_t> freeSlots;
/// Slots whose generation wrapped, oldest first. Each sits out until
/// retiredLimit more wrap, so its old handles stay stale that long.
std::deque<uint32_t> retiredSlots;
size_t retiredLimit = (size_t)1 << 16;
/// The most slots a handle's 24 slot bits address.
size_t slotLimit = 0xffffff;
uint32_t mint(Tag tag, void* object);
void* resolve(uint32_t handle, Tag tag) const;
/// Bumps the generation so outstanding copies of the handle go stale.
void release(uint32_t handle, Tag tag);
size_t live() const
{
return slots.size() - freeSlots.size() - retiredSlots.size();
}
};
HandleTable& handles() { return m_handles; }
/// Borrowed node handles, released when the path effect update that
/// minted them returns.
std::vector<uint32_t>& scopedNodes() { return m_scopedNodes; }
Factory* factory() const { return m_factory; }
/// Brackets one call into the module. Calls nest, so the exit reclaims
/// only the render passes and canvas frames this call left open. The
/// outermost budgeted scope arms the timeout watchdog.
class ScriptCallScope
{
public:
explicit ScriptCallScope(WasmScriptingVM* vm, bool budgeted = true);
~ScriptCallScope();
private:
WasmScriptingVM* m_vm;
#ifdef WITH_RIVE_TOOLS
WriteAttributionScope m_writeSource{WriteSource::Kind::wasm, m_vm};
#endif
uint64_t m_passToken = 0;
uint64_t m_frameToken = 0;
bool m_budgeted = false;
bool m_deadlineArmed = false;
uint32_t m_outerRegionId = 0;
SharedRegion* m_outerRegion = nullptr;
};
/// The deadline of this VM's current budgeted call, polled by the
/// watchdog thread; 0 when disarmed.
struct BudgetSlot;
/// Brackets a wait the host makes for a main lane call. The watchdog
/// holds off meanwhile, and time past usable() moves the call's
/// deadlines on, unless the wait began with none usable.
class WaitScope
{
public:
explicit WaitScope(WasmScriptingVM* vm);
~WaitScope();
/// The seconds of the call's budget the wait may spend, infinity
/// when the call has none.
double usable() const { return m_usable; }
private:
WasmScriptingVM* m_vm;
double m_usable;
std::chrono::steady_clock::time_point m_start;
int64_t m_paused = 0;
};
/// Tokens start at 1, so 0 takes every open frame.
uint64_t nextCanvasFrameToken() const { return m_nextCanvasFrameToken; }
void registerOpenCanvasFrame(uint32_t canvas)
{
m_openCanvasFrames.push_back({m_nextCanvasFrameToken++, canvas});
}
void unregisterOpenCanvasFrame(uint32_t canvas);
/// Removes and returns the frames begun at or after `token`.
std::vector<uint32_t> takeOpenCanvasFramesFrom(uint64_t token);
/// The file's view models, backing the module's Data constructors; the
/// file outlives the VM.
void viewModels(std::vector<ViewModel*>* value) { m_viewModels = value; }
std::vector<ViewModel*>* viewModels() const { return m_viewModels; }
/// The owning file, for id-bound asset property resolution through its
/// registry; the file outlives the VM.
void file(File* value) { m_file = value; }
File* file() const { return m_file; }
/// Loads and runs a module registered in the embedded registry; returns
/// false with the error in lastError(). outResultRef, when set, receives
/// a ref to the module's result (the generator for protocol scripts).
bool requireModule(const std::string& name, int* outResultRef = nullptr);
/// Registers loose Luau bytecode under `name` for require, the dynamic
/// twin of the linker's embedded registry; lets bytecode-only files run
/// on a stock vm module.
bool registerBytecode(const std::string& name, Span<const uint8_t> bytecode);
/// Delivers a watched value change to the module's listener registry.
void notifyDataValueChanged(uint32_t token);
/// Hands one drawable to the module's draw visitor. False when the
/// module has none or it trapped.
bool notifyDrawVisit(uint32_t drawable);
/// context:decodeImage plumbing. The module issues the token; the decode
/// runs as a WorkTask on the global WorkPool, so completion lands through
/// the same per-advance rive_pollAsyncWork poll as the Luau backend and
/// resolves the module's promise via its host_image_decoded /
/// host_image_decode_failed exports. Returns false when this build has
/// no decoders.
bool startImageDecode(
const uint8_t* bytes,
uint32_t byteCount,
uint32_t token);
/// Flags the in-flight decode cancelled so the poll skips its callbacks;
/// fired by the module promise's onCancel hook.
void cancelImageDecode(uint32_t token);
/// WorkTask completion callbacks, delivered from rive_pollAsyncWork.
void resolveImageDecode(
uint32_t token,
uint32_t width,
uint32_t height,
const uint8_t* pixels,
uint32_t byteCount);
void rejectImageDecode(uint32_t token, const char* message);
/// fetch() plumbing. The module issues the token and passes a NetWire
/// request; scriptnet runs the policy, this VM's limits and the host's
/// transport, and the outcome reaches the module on a later
/// rive_pollAsyncWork, on the thread that started it, through its
/// host_fetch_resolved / host_fetch_failed exports. Returns false on
/// hosts built without fetch() support, where nothing would settle, and
/// for a token already in flight.
bool startFetch(Span<const uint8_t> request, uint32_t token);
/// The script cancelled: aborted at the transport, never delivered.
void cancelFetch(uint32_t token);
/// scriptnet listener callbacks: the response, or the NetErrorCode and
/// its "<code>: <message>" rejection.
void resolveFetch(uint32_t token, scriptnet::HttpResponse&& response);
void rejectFetch(uint32_t token, uint32_t code, const std::string& message);
/// Delivers the decode and fetch outcomes that landed while a module
/// call was running; a no-op while one still is.
void deliverHeldOutcomes();
/// deliverHeldOutcomes() for every VM on this thread holding some.
static void deliverAllHeldOutcomes();
/// True while a decode or fetch this VM started has yet to reach the
/// module, held outcomes included.
bool hasPendingAsyncWork() const;
/// The module's jobs section, null when it carries none.
WasmJobs* jobs() const { return m_jobs.get(); }
/// The parts a parallel call runs at once: 1 in safe mode, without
/// threads, or while a debugger is attached.
uint32_t parallelSize() const;
/// Runs the parallel call whose control block lies at control, raising
/// the first failure of any part as this call's trap. Answers the threads
/// that ran parts.
uint32_t runParallel(uint32_t control);
/// The module's workers section, null when it carries none.
WasmWorkers* workers() const { return m_workers.get(); }
/// The module's audio section, null when it carries none.
WasmProcessors* processors() const { return m_processors.get(); }
/// The module carries a section of any kind of lane.
bool hasLanes() const
{
return m_jobs != nullptr || m_workers != nullptr || m_processors != nullptr;
}
/// Hands each lane's console lines, and a worker's or processor's
/// failure, to its VM, on the thread those VMs run on.
static void deliverAllLaneOutput();
/// A module call is live on this thread, where a lane run inline could
/// share its held region.
static bool inModuleCallOnThread();
/// Every lane's, once per poll, and settles the main lane's waits.
void deliverLaneOutput();
/// One lane's, where its output must land ahead of what the caller
/// prints next.
void deliverLaneOutput(WasmLane* lane);
/// Why a kind's lane module failed to load, empty when it loaded or the
/// module carries none.
const std::string& laneLoadError(LaneKind kind) const
{
return m_laneLoadErrors[(int)kind];
}
/// Registers the VM for lane output on this thread, the only one its
/// lanes may be started from.
void watchLanes();
/// The kind lanes_start last refused with kStartFailed for its load
/// error, which lanes_failed answers for that code.
LaneKind refusedStart = LaneKind::job;
/// The main lane's awaits, settled at a poll through host_lane_wake with
/// the token they answer.
uint32_t watchLane(uint32_t lane);
/// Settled with the stream's handle once its first records are queued.
uint32_t watchStream(uint32_t stream);
uint32_t waitOnMain(uint32_t signal, double after, double timeoutSeconds);
/// Drops the await token answers, whose promise was cancelled.
void cancelMainWait(uint32_t token);
/// Decodes source on a job, settled at a poll through host_lane_wake with
/// the blob's id, a Create code, or -1 when it did not decode.
uint32_t startAudioDecode(
rcp<AudioSource> source,
uint32_t sampleRate,
uint32_t channels);
/// Owns the shared objects this VM creates, released with it.
uint64_t sharedOwner();
/// The audio streams each poll tops up, released ones a reader still holds
/// included.
std::vector<HostAudioStream*>& audioStreams() { return m_audioStreams; }
/// The holds a ring or snapshot read carried, kept until the std has
/// decoded the value or, when decoding threw, until the frame ends.
void keepReadHolds(Span<const uint32_t> ids);
void releaseReadHolds();
// ScriptBackend.
bool valid() const override;
bool utf16Strings() const { return m_utf16Strings; }
bool utf16HostStrings() const { return m_utf16HostStrings; }
bool hostBudget() const { return m_hostBudget; }
// An init that read data the host had not bound yet fails so it reruns,
// as the Luau context's missingRequestedData does.
void noteMissingRequestedData() { m_missingRequestedData = true; }
void releaseRef(int ref) override;
int instantiate(
int generatorRef,
ScriptedObject* object,
int* outContextRef,
ScriptedContext** outContextPtr) override;
InitResult callUserInit(ScriptedObject* object, int selfRef, int contextRef)
override;
bool callAdvance(ScriptedObject* object, int selfRef, float elapsedSeconds)
override;
void callUpdate(ScriptedObject* object, int selfRef) override;
void callTrigger(ScriptedObject* object, int selfRef, const char* name)
override;
bool callNumberMethod(
ScriptedObject* object,
int selfRef,
const char* name,
const float* args,
size_t argCount,
float* outResult) override;
bool callBooleanMethod(ScriptedObject* object, int selfRef, const char* name)
override;
/// The node argument exposes the Luau lane's PaintData snapshot of the
/// ShapePaint (paint / asPaint()); its live transform surface stays host
/// side.
bool callPathEffectUpdate(
ScriptedObject* object,
int selfRef,
const RawPath& sourcePath,
const ShapePaint* shapePaint,
RawPath* outPath) override;
/// The same call with the paint already snapshotted, for hosts whose
/// paint is not a runtime ShapePaint; shape may be null.
bool callPathEffectUpdate(
ScriptedObject* object,
int selfRef,
const RawPath& sourcePath,
const PathEffectPaintWire& paint,
TransformComponent* shape,
RawPath* outPath);
bool callDataConvert(
ScriptedObject* object,
int selfRef,
const char* method,
DataValue* input,
ScriptDataResult* outResult) override;
/// Every invocation kind crosses; the host-entangled pointers (focus
/// group, reported Event, view model source, semantic group) are dropped
/// because no script surface reads them.
void callListenerPerform(
ScriptedObject* object,
int selfRef,
const ListenerInvocation& invocation) override;
void callDraw(ScriptedObject* object, int selfRef, Renderer* renderer)
override;
bool callGamepadEvent(
ScriptedObject* object,
int selfRef,
const char* method,
const ListenerInvocation& invocation) override;
bool callPointerEvent(
ScriptedObject* object,
int selfRef,
const char* method,
int pointerId,
Vec2D localPosition,
ListenerType hitType,
float timeStamp,
HitResult* outResult) override;
bool callScrollEvent(
ScriptedObject* object,
int selfRef,
int pointerId,
Vec2D localPosition,
const ScrollEvent& event,
float timeStamp,
HitResult* outResult) override;
bool callKeyboardEvent(
ScriptedObject* object,
int selfRef,
Key key,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat) override;
bool callTextEvent(
ScriptedObject* object,
int selfRef,
const std::string& text) override;
/// The transition protocol is not ported to the module lane yet; there is
/// no TransitionChild wire type, so these keep the host defaults.
bool transitionManagesTo(int selfRef) override;
void callTransitionChanged(
ScriptedObject* object,
int selfRef,
const TransitionChildRef& from,
const TransitionChildRef& to,
int direction) override;
void callTransitionDraw(
ScriptedObject* object,
int selfRef,
Renderer* renderer,
const TransitionChildRef& from,
const TransitionChildRef& to) override;
void callLayoutResize(ScriptedObject* object, int selfRef, Vec2D size)
override;
bool callLayoutMeasure(ScriptedObject* object, int selfRef, Vec2D* outSize)
override;
void setInputBoolean(int selfRef, const char* name, bool value) override;
void setInputNumber(int selfRef, const char* name, float value) override;
void setInputUnsigned(int selfRef, const char* name, uint32_t value) override;
void setInputString(int selfRef, const char* name, const char* value)
override;
/// self[name] becomes a module Artboard userdata over a host-owned
/// instance handle, mirroring the Luau lane's ScriptedArtboard.
void setInputArtboard(
int selfRef,
const char* name,
ScriptedObject* object,
Artboard* artboard) override;
void setInputViewModel(
int selfRef,
const char* name,
ViewModelInstanceValue* value) override;
/// Result sinks the module's rive_path_v1.effect_result and
/// rive_data_v1.convert_result imports write through; non-null only
/// while the corresponding seam call runs.
RawPath* pathEffectOut() const { return m_pathEffectOut; }
ScriptDataResult* convertResultOut() const { return m_convertResultOut; }
/// Modules baked into the embedded registry, from host_register_embedded.
int embeddedModuleCount() const { return m_embeddedModules; }
#ifdef WITH_RIVE_TOOLS
/// Edit-time shader side-band, the wasm twin of ScriptingContext's RSTB
/// registry: requestWasmVM registers freshly compiled WGSL here and the
/// module's shader natives consult it before file assets.
void registerShaderRstb(std::string name, std::vector<uint8_t> bytes);
const std::vector<uint8_t>* findShaderRstb(const std::string& name) const;
/// The wasm twin of ScriptingContext::isPlaying: hosts pause and resume
/// it with playback, and audio refuses to play while it is off.
void isPlaying(bool value) { m_isPlaying = value; }
bool isPlaying() const { return m_isPlaying; }
#endif
const std::string& lastError() const { return m_lastError; }
/// The representation the module booted on; it runs there for life.
enum class ExecutionTier : uint8_t
{
interp = 0,
aotO0 = 1,
aotO3 = 2,
};
ExecutionTier executionTier() const { return m_tier; }
/// Whether the module runs code that leaves its bounds to guard pages.
#ifndef __EMSCRIPTEN__
bool hardwareBounds() const;
#else
bool hardwareBounds() const { return false; }
#endif
/// How a host's log names a tier, with why it is the interpreter.
static std::string tierName(
ExecutionTier tier,
const std::string& interpReason);
/// Adds a line per lane module it loaded, as "jobs module tier: aot -O3".
void laneTierLines(std::vector<std::string>& lines) const;
#ifndef __EMSCRIPTEN__
/// Loads bytes once for their holders, from compiled code where an artifact
/// exists. The lease keeps the WAMR module loaded, null with the error.
static std::shared_ptr<void> loadSharedModule(
Span<const uint8_t> bytes,
ExecutionTier& tier,
std::string& interpReason,
std::string& error,
bool checkedBounds = false);
/// For an instance that found no address space to reserve for its
/// hardware bounds memory: the module on code that checks its bounds.
static std::shared_ptr<void> loadFallbackModule(
Span<const uint8_t> bytes,
const char* what,
ExecutionTier& tier,
std::string& interpReason);
/// The wasm behind a module whose code skips bounds checks, kept once per
/// loaded module for loadFallbackModule; empty for any other.
static Span<const uint8_t> hardwareBoundsWasm(
const std::shared_ptr<void>& lease);
/// How many modules nothing leases stay loaded for a later load of the
/// same bytes; a load past it unloads the least recently leased.
static constexpr size_t kIdleModules = 8;
#endif
/// Why the module runs on the interpreter, for a host's log; empty when
/// it runs compiled code.
const std::string& interpReason() const { return m_interpReason; }
/// The per-frame collection point: frame collector modules scavenge
/// here (call once during load after init so init promotion lands in
/// load time). All rasc modules also get the leak watch: once the heap
/// grows well past its post-first-advance baseline the call returns a
/// warning for the host's log channel, re-armed per 8MB of further
/// growth and carrying a time-to-trap estimate (RIVE_WASM_LEAK_WARN=0
/// silences it, e.g. for benches).
///
/// Call once per host frame with no wasm frames live; returns null when
/// there is nothing to report.
const char* frameBoundary();
/// What frameBoundary returns once a frame collector first collects, a
/// notice rather than a warning.
static constexpr char
kFrameGcNotice[] = "script frame gc: scavenging per frame";
/// frameBoundary without its notices, for hosts that print it as
/// warnings.
const char* frameBoundaryWarnings();
/// Adds a boundary report to joined, a line each.
static void joinFrameReport(std::string& joined, std::string_view report);
const char* handleLeakWarning();
const char* heapGrowthWarning();
/// Current size of the module's linear memory in 64KB wasm pages.
uint32_t memoryPages() const;
/// Backend seams: host code reaches module memory and module functions
/// only through these, so a browser backend can substitute staged
/// copies and JS dispatch for WAMR's direct access.
/// Resolves size bytes of module memory to a host pointer, null when
/// out of range. The pointer and writes through it stay coherent until
/// the next callModule, when a copying backend flushes staged ranges;
/// WAMR returns the linear memory address directly.
virtual void* resolveModulePtr(uint32_t appAddr, uint32_t size);
/// resolveModulePtr for a range the host overwrites whole, such as a
/// fresh allocation, so a copying backend need not read it first.
virtual void* resolveModuleWritePtr(uint32_t appAddr, uint32_t size)
{
return resolveModulePtr(appAddr, size);
}
/// Writes size bytes into module memory, false when out of range. A
/// copying backend need not stage a range the host never reads.
virtual bool copyToModule(uint32_t appAddr, const void* src, uint32_t size);
/// The zero terminated string at appAddr, under the same contract.
virtual const char* resolveModuleString(uint32_t appAddr);
/// Calls a module export with i32 args; returns the first result, or 0
/// when the export is missing or the call traps.
uint32_t callModule(const char* name, uint32_t argc, uint32_t* argv);
/// The honest variant: distinguishes a missing export from a trap from
/// a real zero result, for callers where the difference is a failure.
enum class CallOutcome
{
ok,
missing,
trapped,
};
virtual CallOutcome callModuleChecked(
const char* name,
uint32_t argc,
uint32_t* argv,
uint32_t* result);
virtual bool hasExport(const char* name);
/// The module memory's size and declared ceiling in 64KB pages; false
/// when the module has no memory. A ceiling of 0 means unknown.
virtual bool memoryLimits(uint32_t* pages, uint32_t* maxPages) const;
/// Ends module execution like a trap once the current native returns.
virtual void raiseModuleError(const char* message);
/// raiseModuleError for a spent execution budget, which lastTrap marks.
void raiseBudgetExceeded();
/// raiseModuleError for a message the script threw, which lastTrap marks.
void raiseThrown(const char* message);
/// What ended the last call that trapped: the message the script threw,
/// else the runtime's own trap text. A test takes only a thrown trap for
/// the error it expected.
struct Trap
{
std::string message;
bool budget = false;
bool thrown = false;
};
const Trap& lastTrap() const { return m_lastTrap; }
/// Traps still land in lastTrap but print nothing, for callers that
/// expect them.
void setQuietTraps(bool quiet) { m_quietTraps = quiet; }
/// The trap text without the runtime's "Exception: " prefix: the full
/// raiseModuleError text when `exception` is its truncation (the
/// runtime's exception buffer is small), otherwise `exception`.
const char* fullTrapMessage(const char* exception) const;
/// Function imports the module declares that no host native resolves.
/// They trap only when first called, so surface them at load instead.
const std::vector<std::string>& unresolvedImports() const
{
return m_unresolvedImports;
}
/// Script execution budget, matching the Luau backend: 0 disables the
/// metering, which is the host's trust decision for validated content
/// (armed metering costs about 5 percent on loop heavy scripts). Starts
/// at the limits' main call budget.
void setTimeoutMs(int ms);
int timeoutMs() const { return m_timeoutMs; }
/// What a lane's run may take while it holds the page's thread, as a call
/// may: the call budget where it bounds calls, else 0.
uint32_t pageThreadBudgetMs() const;
/// The host's limits when the VM was made.
const ScriptingLimits& limits() const { return m_limits; }
/// Backs a Tests suite's blob(name); empty when the blob is missing,
/// which every blob is outside test runs.
using TestBlobLookup = std::function<Span<const uint8_t>(const std::string&)>;
void setTestBlobs(TestBlobLookup lookup) { m_testBlobs = std::move(lookup); }
Span<const uint8_t> testBlob(const std::string& name) const
{
return m_testBlobs ? m_testBlobs(name) : Span<const uint8_t>();
}
/// Receives script print output; defaults to stdout.
void onPrint(std::function<void(const char*, size_t)> handler)
{
m_print = std::move(handler);
}
/// Receives each script error line, such as a trapped call, a failed
/// lane or a module that failed to start. Without it they go to stderr,
/// and a lane's failure to the print sink.
void onError(std::function<void(const std::string&)> handler)
{
m_error = std::move(handler);
}
void reportError(const std::string& line);
static constexpr char
kModuleStartFailed[] = "wasm script module failed to start: ";
/// A debugger over this module's line probes, or null. The hooks must
/// outlive the VM or clear themselves first.
void setDebugHooks(WasmDebugHooks* hooks) { m_debugHooks = hooks; }
WasmDebugHooks* debugHooks() const { return m_debugHooks; }
/// Runs for every VM made on the calling thread, after its print sink
/// is set and before its module starts, with the module as baked: the
/// place to install debug hooks and a print sink that see module
/// start, where rasc runs every script's top level.
using BootHook = std::function<
void(WasmScriptingVM& vm, Span<const uint8_t> module)>;
static void setBootHook(BootHook hook);
/// Modules started after the call must carry a bound this host can
/// enforce, their own fuel or natively the watchdog, as an editor needs.
static void requireBounds(bool on);
/// The module can never run again.
bool poisoned() const { return m_poisoned; }
protected:
/// Loads the lane modules the main module carries, and their policy.
void attachLanes(Span<const uint8_t> module);
/// Never runs the module again, nor any lane it started.
void poison();
// The browser backend subclasses over the seams and boots without the
// WAMR init, so it shares construction, error, factory, and lua state.
// Out of line so subclass TUs need no complete member types.
WasmScriptingVM();
std::string m_lastError;
// Full text of the last raiseModuleError; see fullTrapMessage().
std::string m_moduleErrorDetail;
Factory* m_factory = nullptr;
uint32_t m_L = 0;
uint32_t m_callDepth = 0;
// Method names the module keeps past the frame, one copy per instance.
std::unordered_map<std::string, uint32_t> m_guestNames;
// console.time starts by label, in steady nanoseconds.
std::unordered_map<std::string, int64_t> m_consoleTimers;
bool m_advancedOnce = false;
bool m_missingRequestedData = false;
/// Module exports __riveUtf16Strings: its string arguments arrive as
/// UTF-16. Modules baked before that, and the Luau one, pass UTF-8.
bool m_utf16Strings = false;
/// Exports modules built before them lack, probed once at boot.
bool m_rawAllocExport = false;
bool m_inputSlotExport = false;
bool m_pathEffectV2 = false;
bool m_pointerEventV2 = false;
bool m_layoutResizeV2 = false;
bool m_transitionManagesToExport = false;
std::function<void(const char*, size_t)> m_print;
std::function<void(const std::string&)> m_error;
/// Reads a flag export. Backends call it off the module before start,
/// which runs every script's top level and already passes strings.
void noteModuleExport(const char* name);
/// What follows instantiation on every backend: the optional export
/// probes, then the module side boot. False with m_lastError set.
bool bootModule();
/// Fills lastTrap from a backend's trap text and logs it unless quiet.
void recordTrap(const char* where, const char* exception);
/// Calls an export with args converted to its parameter types, for
/// exports that take floats. name must be a string literal: a backend
/// may cache the export by the pointer.
virtual CallOutcome callF64(
const char* name,
const double* args,
uint32_t argc,
double* result);
uint32_t guestString(const char* text, const char* allocator = "malloc");
void guestFree(uint32_t ptr);
// A call scoped handle over a transition child, 0 for an empty one.
uint32_t mintTransitionChild(const TransitionChildRef& ref);
void releaseTransitionChild(uint32_t handle);
void releaseScopedNodes(size_t from);
// A method name in guest memory: made once per instance when the module
// can keep it past the frame, else copied for this call alone.
class GuestName
{
public:
GuestName(WasmScriptingVM* vm, const char* name);
~GuestName();
uint32_t ptr = 0;
private:
WasmScriptingVM* m_vm;
bool m_owned = false;
};
// The input argument of a set or trigger call: the slot, or the name
// itself for modules baked before the slot ABI, which owned then holds
// for the caller to free. False when the instance lacks the input.
bool inputArg(int selfRef, const char* name, uint32_t& arg, uint32_t& owned);
private:
#ifndef __EMSCRIPTEN__
bool init(Span<const uint8_t> module);
#endif
// The module's slot for an input name, resolved once per instance; -1
// when the instance declares no such input.
int32_t inputSlot(int selfRef, const char* name);
bool legacyInputs();
std::unordered_map<int, std::unordered_map<std::string, int32_t>>
m_inputSlots;
#ifndef __EMSCRIPTEN__
struct WamrState;
std::unique_ptr<WamrState> m_state;
#endif
ExecutionTier m_tier = ExecutionTier::interp;
std::string m_interpReason;
WasmDebugHooks* m_debugHooks = nullptr;
std::vector<std::string> m_unresolvedImports;
ScriptingLimits m_limits = ScriptingLimits::get();
int m_timeoutMs = (int)m_limits.mainCallBudgetMs;
TestBlobLookup m_testBlobs;
HandleTable m_handles;
std::vector<uint32_t> m_scopedNodes;
/// Canvas frames begun and not ended, as handles with a monotonic token
/// so a script call's exit reclaims only the frames it began.
struct OpenCanvasFrame
{
uint64_t token;
uint32_t canvas;
};
std::vector<OpenCanvasFrame> m_openCanvasFrames;
uint64_t m_nextCanvasFrameToken = 1;
/// Object handles minted for the init scoped context, released once init
/// completes or the context ref is released.
std::unordered_map<int, uint32_t> m_contextObjects;
/// In-flight decodes by module token, for cancellation; completed or
/// cancelled entries erase themselves.
#ifdef __EMSCRIPTEN__
/// The browser decodes, so each token keeps its browser request id.
std::unordered_map<uint32_t, uint32_t> m_pendingDecodes;
#else
std::unordered_map<uint32_t, rcp<WorkTask>> m_pendingDecodes;
uint64_t m_decodeOwnerId = 0;
#endif
// Delivery is synchronous, so the pixels borrow the decoder's buffer.
struct DecodeResult
{
bool ok = false;
uint32_t token = 0;
uint32_t width = 0;
uint32_t height = 0;
Span<const uint8_t> pixels;
std::string error;
};
void deliverDecodeResult(const DecodeResult& result);
// Logs the pending trap and clears it so later calls can run.
void reportTrap(const char* where);
/// Calls a pointer or scroll export, whose 0 means unhandled and anything
/// else is 1 + the HitResult the handler reported.
bool callHitExport(
const char* name,
const double* args,
uint32_t argc,
HitResult* outResult);
/// In-flight fetches: module token to scriptnet request id, for
/// cancellation. The owner id scopes this VM's rate limits and lets
/// teardown cancel everything it started.
std::unordered_map<uint32_t, uint32_t> m_pendingFetches;
uint64_t m_netOwnerId = 0;
void deliverFetchFailure(
uint32_t token,
uint32_t code,
const std::string& message);
/// A nested advance polls async work while the module is mid call;
/// settling its promises there would run script under that call.
bool inModuleCall() const;
void holdOutcome(std::function<void()> deliver);
std::vector<std::function<void()>> m_heldOutcomes;
struct MainWait
{
SignalWait wait;
// The watched lane's handle, 0 for a signal wait.
uint32_t lane;
rcp<WasmAudioDecode> decode = nullptr;
// The handle of a stream that is not ready yet, released on a cancel.
uint32_t stream = 0;
};
/// Stops what a wait no one will settle still runs or holds.
void abandonWait(const MainWait& wait);
std::vector<MainWait> m_mainWaits;
std::vector<HostAudioStream*> m_audioStreams;
bool streaming() const;
uint32_t m_nextMainWait = 1;
void settleMainWaits();
void wakeMain(uint32_t token, double value);
std::unique_ptr<BudgetSlot> m_budgetSlot;
std::unique_ptr<WasmJobs> m_jobs;
#ifndef __EMSCRIPTEN__
std::unique_ptr<WasmParallel> m_parallel;
#endif
std::unique_ptr<WasmWorkers> m_workers;
std::unique_ptr<WasmProcessors> m_processors;
std::string m_laneLoadErrors[3];
// The polls that reach its lanes and its held outcomes, null until it
// has some.
#if RIVE_ONE_THREAD()
VMPollList* m_laneList = nullptr;
VMPollList* m_holdingList = nullptr;
#else
std::shared_ptr<VMPollList> m_laneList;
std::shared_ptr<VMPollList> m_holdingList;
#endif
uint64_t m_sharedOwner = 0;
std::vector<uint32_t> m_readHolds;
/// Set while a budgeted call holds the slot's deadline, which calls
/// nested in it share.
bool m_budgetArmed = false;
/// Gives the call running on env (a wasm_exec_env_t) one timeout, and
/// takes it back, poisoning the module when the watchdog fired.
// Fuel stops metered code first with a trap the module survives; the
// watchdog, which poisons it, only catches code fuel cannot see.
int watchdogBudgetMs() const { return m_timeoutMs * (m_fuelBudget ? 2 : 1); }
void armWatchdog(void* env);
void disarmWatchdog();
/// The budget bounds the module's calls and its top level.
bool boundsCalls() const;
/// Set while a bounded call holds the deadline this VM's waits read,
/// which calls nested in it share.
bool m_deadlineArmed = false;
std::chrono::steady_clock::time_point m_outerDeadline;
/// Gives a bounded call's waits their deadline; false when a call it
/// nests in already did, or the module has no budget or no lanes.
bool armDeadline();
void disarmDeadline();
#ifdef __EMSCRIPTEN__
/// What a web start set aside until it ends, as a call's scope does: the
/// VM booting when it began and the region the thread held.
WasmScriptingVM* m_outerBooting = nullptr;
uint32_t m_outerRegionId = 0;
SharedRegion* m_outerRegion = nullptr;
#endif
/// This VM's own boundary work, without the files it decoded.
const char* collectFrame();
/// The VM whose script decoded this VM's file, and the VMs of files
/// this one's scripts decoded, which collect at its boundaries.
WasmScriptingVM* m_decodedBy = nullptr;
std::vector<WasmScriptingVM*> m_decodedVMs;
/// What frameBoundary returns once those VMs report too, one per line.
std::string m_frameReport;
/// Module exports __riveUtf16HostStrings: strings the host hands it are
/// written as UTF-16 too.
bool m_utf16HostStrings = false;
/// Module exports __riveHostBudget: the watchdog bounds its calls. The
/// rest meter themselves or are trusted to run unbounded.
bool m_hostBudget = false;
/// Module exports __riveFuelCheck: it traps itself once a call overruns.
bool m_fuelBudget = false;
bool m_fuelShift = false;
/// Module exports __riveFetchBlobBody: a fetched body crosses as a blob.
bool m_fetchBlobBody = false;
/// Set once the watchdog interrupts a call or the collector traps; the
/// module never runs again.
bool m_poisoned = false;
bool m_budgetRaised = false;
bool m_thrownRaised = false;
bool m_quietTraps = false;
Trap m_lastTrap;
bool m_frameMinor = false;
bool m_frameMinorAnnounced = false;
/// Module exports __riveFrameRelease: its reap queues the handles it
/// finds dead and the host releases them right after each collection.
bool m_frameRelease = false;
/// Module exports __riveHeapUsed; the leak watch reads bump bytes
/// instead of page counts.
bool m_heapUsedProbe = false;
/// Collected runtimes warn only on a second consecutive growth window.
bool m_leakArmedCollected = false;
bool m_leakWatch = false;
std::string m_leakWarning;
uint32_t m_leakBaselinePages = 0;
uint32_t m_leakFirstBaselinePages = 0;
uint32_t m_leakFrames = 0;
uint32_t m_leakTotalFrames = 0;
uint32_t m_leakWarningCount = 0;
bool m_leakTrapContextPrinted = false;
bool m_collectedRuntime = false;
bool m_handleWatch = false;
uint32_t m_handleBaselineLive = 0;
uint32_t m_handleFrames = 0;
uint32_t frameMajors();
uint8_t m_handleCollects = 0;
uint32_t m_handleCollectMajors = 0;
bool m_frameMajorsProbe = false;
RawPath* m_pathEffectOut = nullptr;
ScriptDataResult* m_convertResultOut = nullptr;
std::vector<ViewModel*>* m_viewModels = nullptr;
File* m_file = nullptr;
int m_embeddedModules = 0;
#ifdef WITH_RIVE_TOOLS
std::unordered_map<std::string, std::vector<uint8_t>> m_shaderRstbs;
bool m_isPlaying = false;
#endif
friend struct WasmScriptingVMNatives;
};
} // namespace rive
#endif
#endif