Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
script_inspector_bridge.hpp
Go to the documentation of this file.
1// script_inspector_bridge.hpp — thread-marshaling seam between an off-thread
2// inspector and the single-threaded scripted-UI JS engine.
3#pragma once
4
6
7#include <chrono>
8#include <condition_variable>
9#include <functional>
10#include <memory>
11#include <mutex>
12#include <string>
13#include <thread>
14
15namespace pulp::view {
16
17class ScriptEngine;
18
19// Lets an inspector running on a *background* thread evaluate expressions
20// against a ScriptEngine that lives on the UI / engine-owner thread, query the
21// engine's honest debug capabilities, and cooperatively abort a runaway
22// evaluation — without ever touching the (not-thread-safe) engine off-thread.
23//
24// The engine thread is whichever thread calls attach()/pump()/detach(). It
25// drains queued evaluate requests in pump(), which a host runs once per frame
26// from ScriptedUiSession::poll(). A background evaluate() enqueues a request
27// and blocks (with a timeout) until pump() fulfills it; if evaluate() is itself
28// called on the engine thread it runs inline, with a watchdog using the
29// backend's cross-thread interrupt seam to enforce the same deadline. Only one
30// evaluation is in flight at a time — including owner-thread calls — and a
31// concurrent request returns `busy`.
32//
33// This is deliberately NOT a step debugger: mainline QuickJS (Pulp's bundled
34// engine) exposes no source-line breakpoint / stepping / local-scope API, so
35// capabilities() reports can_break/can_step/can_inspect_locals = false. It is
36// an honest runtime inspector: evaluate, capability reporting, and interrupt.
38 public:
39 struct EvalResult {
40 bool ok = false;
41 bool timed_out = false;
42 bool busy = false;
43 bool detached = false;
44 bool refused = false;
45 std::string json;
46 std::string error;
47 };
48
49 // Honest snapshot of what the attached engine can do for debugging. Fixed
50 // at attach() time from the engine's immutable identity, so it can be read
51 // from any thread without touching the engine.
52 struct Capabilities {
53 std::string engine;
54 bool can_evaluate = false;
55 bool can_interrupt = false;
56 bool can_break = false;
57 bool can_step = false;
58 bool can_inspect_locals = false;
65 std::string refused_capability;
66 };
67
68 static constexpr std::chrono::milliseconds kDefaultTimeout{2000};
69
72
75
76 // [engine thread] Record the realm's native grant. An explicit grant with
77 // an effectful capability makes evaluate() refuse and capabilities()
78 // report can_evaluate=false with the offending name; the legacy all()
79 // default grant is not a decision and leaves evaluation eligible.
80 void set_realm_capabilities(const CapabilitySet& granted) override;
81
82 // [engine thread] Attach the engine and capture its capability snapshot.
83 // Passing a new engine replaces the previous one; the calling thread is
84 // recorded as the engine thread for inline-eval detection.
85 void attach(ScriptEngine* engine) override;
86
87 // [engine thread] Detach the engine. Queued work fails with `detached`;
88 // running work is interrupted and detach waits for engine quiescence.
89 void detach() override;
90
91 // [engine thread] Install the host-owned realm reset owed after evaluated
92 // code has stopped. A completed evaluation records the debt; the host
93 // discharges it at its next frame boundary via
94 // run_pending_post_evaluation_reset(). The reset does NOT run inside the
95 // request, so an evaluation leaves the caller's view of the realm intact
96 // until the host returns to its run loop.
97 void set_post_evaluation_reset(std::function<std::string(EvaluationDeadline)> reset) override;
98
99 // [any thread] Whether a completed evaluation still owes a realm reset.
101
102 // [engine thread] Run the realm reset owed by a completed evaluation and
103 // clear the debt, or do nothing when none is owed. Returns the reset's
104 // failure text, empty on success. The host MUST call this before any frame
105 // pump that could execute what evaluated code left behind, and again at
106 // teardown so a caller that never pumps cannot drop a reset failure.
107 std::string run_pending_post_evaluation_reset() override;
108
109 // [any thread] Evaluate `code`, marshaled to the engine thread. Blocks up
110 // to `timeout`. On timeout the running evaluation is interrupted (if the
111 // engine supports it) so a runaway loop cannot wedge the bridge forever.
112 EvalResult evaluate(const std::string& code,
113 std::chrono::milliseconds timeout = kDefaultTimeout,
114 std::size_t max_result_bytes = 1024 * 1024);
115
116 // [any thread] Capability snapshot (empty engine name when detached).
118
119 // [any thread] Cancel queued work or cooperatively abort running work.
120 // Returns false when there is no cancellable request.
121 bool interrupt();
122
123 // [engine thread] Drain the pending evaluate request, if any. Returns true
124 // when a request was serviced. Host calls this once per frame.
125 bool pump() override;
126
127 // [any thread] Whether an evaluation is currently queued or executing.
128 bool is_busy() const;
129
130private:
131 enum class RequestState { queued, running, finished };
132
133 struct Request {
134 std::string code;
135 std::size_t max_result_bytes = 0;
136 std::condition_variable cv;
137 RequestState state = RequestState::queued;
138 bool timeout_requested = false;
139 bool detach_requested = false;
140 bool interrupt_requested = false;
141 bool interrupt_consumed = false;
142 bool evaluation_finished = false;
143 EvaluationDeadline deadline{};
144 ScriptEngine* engine = nullptr;
145 bool can_interrupt = false;
146 // Protected by mutex_. A cancelling thread closes the window and arms
147 // the engine interrupt while holding that mutex; the engine thread
148 // takes the same mutex before closing/draining at quiescence. That
149 // ordering prevents a cancel flag from being armed after the drain.
150 bool interrupt_window_open = false;
151 EvalResult result;
152 };
153
154 // Runs engine->evaluate on the engine thread, serializing the result or the
155 // thrown exception into an EvalResult. Takes the engine explicitly so the
156 // caller's snapshot is the one used. Caller guarantees engine-thread.
157 EvalResult serialize_eval(ScriptEngine* engine, const std::string& code,
158 std::size_t max_result_bytes) const;
159 // [engine thread] Execute, reset, normalize, and publish one already
160 // claimed request. Inline evaluation and pump share this state machine.
161 EvalResult run_claimed_request(const std::shared_ptr<Request>& request,
162 ScriptEngine* engine);
163
164 // mutex_ must be held. Publishes a terminal result and releases the
165 // single-flight slot. Request waiters use the same mutex, so completion and
166 // cancellation have one ordering point.
167 void finish_locked(const std::shared_ptr<Request>& request, EvalResult result);
168 // mutex_ must be held. The engine interrupt seam is explicitly nonblocking;
169 // retaining the lock through the call prevents detach() from returning and
170 // the caller-owned engine from being destroyed before the dereference.
171 static bool interrupt_if_active_locked(const std::shared_ptr<Request>& request);
172
174
175 // mutex_ must be held. Re-derives caps_.refused_capability / can_evaluate
176 // from the attached engine and the recorded realm grant.
177 void apply_realm_gate_locked();
178
179 mutable std::mutex mutex_;
180 std::condition_variable state_cv_;
181 ScriptEngine* engine_ = nullptr;
182 Capabilities caps_{};
183 // Grant of the realm the engine belongs to. Default-constructed (empty) so
184 // a bridge attached without a session evaluates as before.
185 CapabilitySet realm_capabilities_{};
186 std::thread::id engine_thread_{};
187 bool have_engine_thread_ = false;
188 bool in_flight_ = false;
189 // Set by a completed evaluation, cleared by the host's next reset drain.
190 // Several evaluations between two frames collapse into one debt: they all
191 // ran in the same realm, and one reconstruction from last-good source
192 // discards everything any of them left behind.
193 bool reset_pending_ = false;
194 bool reset_in_progress_ = false;
195 std::thread::id reset_thread_{};
196 std::shared_ptr<Request> pending_; // single-slot queue, drained by pump()
197 std::shared_ptr<Request> running_; // engine remains attached until this clears
198 std::function<std::string(EvaluationDeadline)> post_evaluation_reset_;
199};
200
202
207
211
212} // namespace pulp::view
A granted-capability set.
Definition reload_capabilities.hpp:74
Definition script_engine.hpp:22
Definition script_inspector_bridge.hpp:37
ScriptInspectorBridge(const ScriptInspectorBridge &)=delete
bool pump() override
Run at most one queued request. True when it changed realm state.
void set_post_evaluation_reset(std::function< std::string(EvaluationDeadline)> reset) override
Install the realm reset a completed evaluation owes.
ScriptInspectorBridge & operator=(const ScriptInspectorBridge &)=delete
friend struct ScriptInspectorBridgeTestAccess
Definition script_inspector_bridge.hpp:173
static constexpr std::chrono::milliseconds kDefaultTimeout
Definition script_inspector_bridge.hpp:68
void set_realm_capabilities(const CapabilitySet &granted) override
Tell the port which native capabilities the realm it is about to be attached to holds.
std::string run_pending_post_evaluation_reset() override
Run the owed reset if one is pending.
void detach() override
Release the engine; a blocked off-thread request wakes as detached.
EvalResult evaluate(const std::string &code, std::chrono::milliseconds timeout=kDefaultTimeout, std::size_t max_result_bytes=1024 *1024)
void attach(ScriptEngine *engine) override
Point the port at engine, or at none with nullptr.
Capabilities capabilities() const
The engine-thread half of a runtime inspector, as ScriptedUiSession drives it.
Definition script_inspector_port.hpp:22
std::chrono::steady_clock::time_point EvaluationDeadline
Definition script_inspector_port.hpp:24
Definition scripted_ui.hpp:125
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
void install_script_inspector_bridge() noexcept
Make every ScriptedUiSession constructed afterwards carry a ScriptInspectorBridge as its inspector po...
ScriptInspectorBridge * script_inspector(ScriptedUiSession &session) noexcept
The session's ScriptInspectorBridge, or nullptr when the session was built without one (install_scrip...
Definition script_inspector_bridge.hpp:52
bool can_break
source-line breakpoints (never on mainline QuickJS)
Definition script_inspector_bridge.hpp:56
bool can_inspect_locals
paused local-scope inspection
Definition script_inspector_bridge.hpp:58
bool can_interrupt
a runaway eval can be aborted
Definition script_inspector_bridge.hpp:55
std::string refused_capability
Wire name of the first effectful capability the realm's EXPLICIT grant holds, empty when evaluation i...
Definition script_inspector_bridge.hpp:65
std::string engine
"QuickJS" / "JavaScriptCore" / "V8" / ""
Definition script_inspector_bridge.hpp:53
bool can_evaluate
bounded Runtime.evaluate is available
Definition script_inspector_bridge.hpp:54
bool can_step
step in/over/out
Definition script_inspector_bridge.hpp:57
Definition script_inspector_bridge.hpp:39
bool timed_out
the request exceeded its deadline
Definition script_inspector_bridge.hpp:41
bool detached
no engine is attached
Definition script_inspector_bridge.hpp:43
bool refused
the realm's explicit grant holds an effectful capability
Definition script_inspector_bridge.hpp:44
bool busy
another evaluation was already in flight
Definition script_inspector_bridge.hpp:42
bool ok
evaluation completed without throwing
Definition script_inspector_bridge.hpp:40
std::string error
message when !ok (exception text / reason)
Definition script_inspector_bridge.hpp:46
std::string json
result serialized as JSON when ok
Definition script_inspector_bridge.hpp:45