Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
js_engine.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4
5#include <string>
6#include <string_view>
7#include <functional>
8#include <optional>
9#include <memory>
10#include <vector>
11#include <cstddef>
12#include <cstdint>
13#include <stdexcept>
14#include <unordered_set>
15#include <choc/containers/choc_Value.h>
16#include <choc/text/choc_JSON.h>
17
18namespace pulp::view {
19
20// Callback for resolving module imports (returns source code or nullopt)
21using ModuleResolver = std::function<std::optional<std::string>(std::string_view path)>;
22using ModuleCompletionHandler = std::function<void(const std::string& error, const choc::value::Value& result)>;
23
24// Callback for logging from JS (console.log, console.warn, etc.)
25using LogCallback = std::function<void(std::string_view level, std::string_view message)>;
26
27// Native function callable from JS
28using NativeFunction = std::function<choc::value::Value(const choc::value::Value* args, size_t num_args)>;
30
31// Native-backed global objects with snapshot properties
32// and native method callbacks. This is intentionally smaller than a full opaque
33// proxy / HostObject API, but it gives the bridge a truthful native
34// object seam to build on.
36 std::string name;
37 choc::value::Value value;
38};
39
41 std::string name;
43};
44
46 std::string class_name;
47 std::vector<HostObjectProperty> properties;
48 std::vector<HostObjectMethod> methods;
49};
50
52 uint64_t total = 0;
53 uint64_t quickjs = 0;
54 uint64_t jsc = 0;
55 uint64_t v8 = 0;
56};
57
58// Which JS engine backend is active
59enum class JsEngineType {
63};
64
65// Returns a human-readable name for the engine type
66constexpr std::string_view engine_type_name(JsEngineType type) {
67 switch (type) {
68 case JsEngineType::quickjs: return "QuickJS";
69 case JsEngineType::jsc: return "JavaScriptCore";
70 case JsEngineType::v8: return "V8";
71 }
72 return "unknown";
73}
74
75// Heap ceiling applied to every newly created engine, in bytes. Sized well
76// above the largest in-repo scripted-UI bundle so ordinary editors never meet
77// it, while a runaway or hostile script hits a JS error instead of growing
78// until the hosting process is OOM-killed. 0 means unlimited.
79constexpr std::size_t kDefaultJsMemoryLimitBytes = std::size_t{512} * 1024 * 1024;
80
81// Abstract JS engine backend.
82// Each backend implements this interface to provide a uniform scripting API.
83// Not thread-safe — all calls must happen on the same thread.
84class JsEngine {
85public:
86 virtual ~JsEngine() = default;
87
88 // Which engine type this is
89 virtual JsEngineType type() const = 0;
90
91 // ── Runtime / provider identity ─────────────────────────────────────────
92 // Truthful identity of the JS runtime actually linked into this build.
93 // Backends that can report a concrete runtime version (e.g. V8) override
94 // these; the defaults return empty strings so non-reporting backends
95 // (QuickJS, JSC) and forward-compatibility callers degrade cleanly.
96
97 // The engine's own reported runtime version string (e.g. V8's
98 // "15.7.112.5"). Empty when the backend does not report one.
99 virtual std::string runtime_version() const { return {}; }
100
101 // Where the runtime came from, as wired at build time: "v8builder" for the
102 // sealed v8-builder seal, "libnode"/"v8_monolith"/"external" for the A/B
103 // baselines, empty when not applicable.
104 virtual std::string provider_kind() const { return {}; }
105
106 // Absolute path to the provider library that was linked, when known.
107 virtual std::string provider_path() const { return {}; }
108
109 // The version the build expected from the provider (parsed from the pinned
110 // V8 tag in tools/deps/manifest.json), for cross-checking against
111 // runtime_version(). Empty when no expectation was pinned.
112 virtual std::string expected_runtime_version() const { return {}; }
113
114 // Check if the engine is in a valid state
115 virtual bool is_valid() const = 0;
116
117 // Evaluate JS code synchronously, returns the result.
118 // Throws std::runtime_error on parse/runtime errors.
119 virtual choc::value::Value evaluate(const std::string& code) = 0;
120
121 // Evaluate a whole script — a bundle or prelude loaded once per realm, not
122 // an expression probe. Same result and error semantics as evaluate(). An
123 // engine may reuse the compiled form of a script it has already compiled
124 // in this process (QuickJS keeps its bytecode), so a second realm loading
125 // the same bundle skips parsing it.
126 virtual choc::value::Value evaluate_script(const std::string& code) {
127 return evaluate(code);
128 }
129
130 // Evaluate and serialize directly inside the backend while enforcing the
131 // byte/depth/cycle bounds during traversal. Backends that cannot provide
132 // that resource guarantee must leave this unsupported.
133 virtual bool supports_bounded_json_evaluation() const { return false; }
134 virtual std::string evaluate_bounded_json(const std::string&, std::size_t) {
135 throw std::runtime_error("bounded JSON evaluation is unsupported");
136 }
137
138 // Run JS code as a module with import resolution. Completion is invoked
139 // when the engine reports module execution has either succeeded or failed.
140 virtual void run_module(const std::string& code,
141 ModuleResolver resolver,
142 ModuleCompletionHandler completion = {}) = 0;
143
144 // Register a C++ function callable from JS as a global.
145 void register_function(const std::string& name, NativeFunction fn) {
146 claim_native_symbol(name);
147 register_function_impl(name, std::move(fn));
148 }
149
150 // Invoke a global JS function by name with no arguments
151 virtual choc::value::Value invoke(std::string_view name) = 0;
152
153 // Invoke a global JS function with positional arguments
154 virtual choc::value::Value invoke(std::string_view name,
155 const choc::value::Value* args,
156 size_t num_args) = 0;
157
158 // Set a log callback for console.log/warn/error/info/debug
159 virtual void set_log_callback(LogCallback callback) = 0;
160
161 // Register a native-backed global object with snapshot properties and
162 // native method callbacks.
163 void register_host_object(const std::string& name, HostObjectDescriptor descriptor) {
164 claim_native_symbol(name);
165 register_host_object_impl(name, std::move(descriptor));
166 }
167
168 // Expose a native callback as a JS function that returns a real Promise
169 // and resolves on the JS microtask queue.
170 // This does not yet provide a held native resolver for later completion.
171 void register_promise_function(const std::string& name, NativePromiseFunction fn) {
172 claim_native_symbol(name);
173 register_promise_function_impl(name, std::move(fn));
174 }
175
176 // Hint that now is a good time to collect garbage (advisory)
177 virtual void gc_hint() {}
178
179 // Cap the heap this realm may allocate, in bytes (0 = unlimited). Backends
180 // that enforce the cap raise a JS "out of memory" error on the allocation
181 // that would exceed it. Returns false on backends that cannot enforce one,
182 // leaving the realm unbounded.
183 virtual bool set_memory_limit(std::size_t) {
184 return false;
185 }
186
187 // Pump any engine-specific message loop / microtask queue. This is a no-op
188 // for engines that do not expose an explicit pump hook.
189 virtual void pump_message_loop() {}
190
191 // ── Cooperative interrupt ────────────────────────────────────────────────
192 // Whether this backend can cooperatively abort a running evaluation that
193 // was started on the engine thread, from a *different* thread. QuickJS
194 // supports this via its host interrupt handler; backends without an
195 // equivalent hook return false.
196 virtual bool supports_interrupt() const { return false; }
197
198 // Request that the currently-running evaluation abort at the engine's next
199 // interrupt check, surfacing as a thrown "interrupted" exception on the
200 // engine thread. This ONLY sets a flag — it does not itself run JS — so it
201 // is safe to call from a thread other than the engine thread (the one true
202 // cross-thread entry point on this otherwise single-threaded interface).
203 // A no-op on backends where supports_interrupt() is false. Arming the flag
204 // while no evaluation is running would abort the *next* one, so callers
205 // (see ScriptInspectorBridge) only arm it while an evaluation is in flight.
206 virtual void request_interrupt() {}
207
208 // Consume an interrupt that arrived after the backend's final check. Called
209 // on the engine thread after evaluation quiesces so a persistent cancel
210 // flag cannot abort the next otherwise-unrelated evaluation.
211 // Returns true when a still-pending flag was cleared, proving that the
212 // completed evaluation did not consume the interrupt request.
213 virtual bool clear_pending_interrupt() { return true; }
214
215 // ── Forward-compatibility capability flags (HostObject / TypedArray / Promise) ──
216 // These are defined now so all backends can be designed with them in mind.
217 // Default implementations return false / no-op. Backends enable as ready.
218
219 virtual bool supports_host_objects() const { return false; }
220 // This means the engine can surface JS TypedArray / ArrayBuffer values
221 // through the current Pulp API seam without collapsing them into an
222 // opaque generic object. It does not imply zero-copy bridging yet.
223 virtual bool supports_typed_arrays() const { return false; }
224 virtual bool supports_promises() const { return false; }
225
226 // Non-copyable, non-movable (owned via unique_ptr)
227 JsEngine(const JsEngine&) = delete;
228 JsEngine& operator=(const JsEngine&) = delete;
229 JsEngine(JsEngine&&) = delete;
231
232protected:
233 JsEngine() = default;
234
235 virtual void register_function_impl(const std::string& name, NativeFunction fn) = 0;
236
237 virtual void register_host_object_impl(const std::string& name, HostObjectDescriptor descriptor) {
238 const auto quote_string = [] (std::string_view text) {
239 return choc::json::toString(choc::value::createString(std::string(text)));
240 };
241
242 const auto js_literal = [&] (const choc::value::Value& value) -> std::string {
243 if (value.isVoid())
244 return "undefined";
245 return choc::json::toString(value);
246 };
247
248 static uint64_t global_host_symbol = 0;
249
250 std::string target = "globalThis[" + quote_string(name) + "]";
251 std::string script;
252 script.reserve(256 + descriptor.properties.size() * 64 + descriptor.methods.size() * 96);
253 script += target + " = {};\n";
254
255 if (!descriptor.class_name.empty())
256 script += target + "[\"_objectName\"] = " + quote_string(descriptor.class_name) + ";\n";
257
258 for (const auto& property : descriptor.properties)
259 script += target + "[" + quote_string(property.name) + "] = " + js_literal(property.value) + ";\n";
260
261 for (auto& method : descriptor.methods) {
262 auto hidden_name = "__pulp_host_object_" + std::to_string(global_host_symbol++);
263 register_function(hidden_name, std::move(method.fn));
264 script += target + "[" + quote_string(method.name) + "] = globalThis[" + quote_string(hidden_name) + "];\n";
265 script += "delete globalThis[" + quote_string(hidden_name) + "];\n";
266 }
267
268 evaluate(script + target + ";");
269 }
270
271 virtual void register_promise_function_impl(const std::string& name, NativePromiseFunction fn) {
272 const auto quote_string = [] (std::string_view text) {
273 return choc::json::toString(choc::value::createString(std::string(text)));
274 };
275
276 static uint64_t global_promise_symbol = 0;
277 auto hidden_name = "__pulp_promise_function_" + std::to_string(global_promise_symbol++);
278 register_function(hidden_name, std::move(fn));
279
280 std::string quoted_hidden = quote_string(hidden_name);
281 std::string quoted_name = quote_string(name);
282 std::string script =
283 "globalThis[" + quoted_name + "] = ((fn) => (...args) => Promise.resolve().then(() => fn(...args)))(globalThis[" + quoted_hidden + "]);\n"
284 "delete globalThis[" + quoted_hidden + "];\n"
285 "globalThis[" + quoted_name + "];";
286 evaluate(script);
287 }
288
289private:
290 std::unordered_set<std::string> registered_native_symbols_;
291
292 void claim_native_symbol(const std::string& name) {
293 if (!registered_native_symbols_.insert(name).second)
294 throw std::runtime_error("JsEngine native symbol already registered: " + name);
295 }
296};
297
298// ── Engine factory ─────────────────────────────────────────────────────────
299
300// Create a JS engine of the specified type.
301// Throws std::runtime_error if the requested engine is not available on this platform/build.
302std::unique_ptr<JsEngine> create_js_engine(JsEngineType type);
303
304// Create a JS engine using the build default. QuickJS remains the
305// compatibility default unless a PULP_DEFAULT_ENGINE_* option selects
306// JavaScriptCore or V8 at compile time.
307std::unique_ptr<JsEngine> create_default_js_engine();
308
309// Query which engines are available in this build
311
312// Diagnostic hook used by native-import validation to prove that a generated
313// C++ view path did not initialize a scripting backend while building/rendering.
316
331 std::uint64_t compiles = 0;
332 std::uint64_t hits = 0;
333 std::uint64_t bypassed = 0;
334 std::uint64_t precompiled = 0;
335 std::uint64_t waits = 0;
336 std::uint64_t embedded = 0;
337 std::size_t entries = 0;
338 std::size_t bytes = 0;
339};
343
354std::size_t precompile_scripts(const std::vector<std::string>& sources,
355 const std::atomic<bool>* cancel = nullptr);
357bool script_bytecode_cached(const std::string& source);
358
382std::vector<std::uint8_t> build_embedded_script_bytecode(const std::string& source);
385bool register_embedded_script_bytecode(const std::uint8_t* blob, std::size_t size);
390
391namespace detail {
395void set_precompile_claim_hook_for_tests(std::function<void(const std::string&)> hook);
396std::function<void(const std::string&)> precompile_claim_hook_for_tests();
399} // namespace detail
400
401} // namespace pulp::view
JsEngine & operator=(JsEngine &&)=delete
virtual std::string provider_path() const
Definition js_engine.hpp:107
void register_promise_function(const std::string &name, NativePromiseFunction fn)
Definition js_engine.hpp:171
JsEngine(const JsEngine &)=delete
virtual void register_function_impl(const std::string &name, NativeFunction fn)=0
virtual void register_host_object_impl(const std::string &name, HostObjectDescriptor descriptor)
Definition js_engine.hpp:237
virtual bool set_memory_limit(std::size_t)
Definition js_engine.hpp:183
virtual void request_interrupt()
Definition js_engine.hpp:206
virtual bool supports_bounded_json_evaluation() const
Definition js_engine.hpp:133
virtual choc::value::Value invoke(std::string_view name)=0
virtual bool supports_interrupt() const
Definition js_engine.hpp:196
virtual void pump_message_loop()
Definition js_engine.hpp:189
virtual std::string expected_runtime_version() const
Definition js_engine.hpp:112
virtual std::string runtime_version() const
Definition js_engine.hpp:99
virtual bool supports_host_objects() const
Definition js_engine.hpp:219
virtual bool clear_pending_interrupt()
Definition js_engine.hpp:213
virtual JsEngineType type() const =0
virtual std::string evaluate_bounded_json(const std::string &, std::size_t)
Definition js_engine.hpp:134
virtual bool is_valid() const =0
virtual void set_log_callback(LogCallback callback)=0
virtual std::string provider_kind() const
Definition js_engine.hpp:104
void register_function(const std::string &name, NativeFunction fn)
Definition js_engine.hpp:145
JsEngine & operator=(const JsEngine &)=delete
virtual ~JsEngine()=default
JsEngine(JsEngine &&)=delete
virtual void register_promise_function_impl(const std::string &name, NativePromiseFunction fn)
Definition js_engine.hpp:271
virtual void gc_hint()
Definition js_engine.hpp:177
virtual choc::value::Value invoke(std::string_view name, const choc::value::Value *args, size_t num_args)=0
virtual void run_module(const std::string &code, ModuleResolver resolver, ModuleCompletionHandler completion={})=0
virtual choc::value::Value evaluate_script(const std::string &code)
Definition js_engine.hpp:126
virtual bool supports_promises() const
Definition js_engine.hpp:224
virtual bool supports_typed_arrays() const
Definition js_engine.hpp:223
virtual choc::value::Value evaluate(const std::string &code)=0
void register_host_object(const std::string &name, HostObjectDescriptor descriptor)
Definition js_engine.hpp:163
Definition accessibility_provider.hpp:65
void clear_embedded_script_bytecode_for_tests()
Tests only: forget every registered embedded bytecode blob.
void set_precompile_claim_hook_for_tests(std::function< void(const std::string &)> hook)
Tests only: called by precompile_scripts() on its own thread after it has claimed source and before i...
std::function< void(const std::string &)> precompile_claim_hook_for_tests()
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ path
Definition anchor_strategy.hpp:42
constexpr std::size_t kDefaultJsMemoryLimitBytes
Definition js_engine.hpp:79
@ text
Plain text (default color).
Definition code_editor_tokenizer.hpp:37
@ type
Built-in / common type name (e.g. int, Number).
Definition code_editor_tokenizer.hpp:39
JsEngineType
Definition js_engine.hpp:59
@ jsc
Definition js_engine.hpp:61
@ quickjs
Definition js_engine.hpp:60
@ v8
Definition js_engine.hpp:62
std::function< choc::value::Value(const choc::value::Value *args, size_t num_args)> NativeFunction
Definition js_engine.hpp:28
std::unique_ptr< JsEngine > create_default_js_engine()
JsEngineCreationStats js_engine_creation_stats()
std::size_t precompile_scripts(const std::vector< std::string > &sources, const std::atomic< bool > *cancel=nullptr)
Compile whole scripts into the bytecode cache without running them, on the calling thread,...
std::string script_bytecode_engine_fingerprint()
The engine's bytecode fingerprint: the QuickJS version plus a digest of the bytecode this engine emit...
void reset_js_engine_creation_stats_for_tests()
ScriptBytecodeCacheStats script_bytecode_cache_stats()
void clear_script_bytecode_cache()
Drop every stored script (tests; a host that wants the memory back).
std::function< void(const std::string &error, const choc::value::Value &result)> ModuleCompletionHandler
Definition js_engine.hpp:22
std::vector< std::uint8_t > build_embedded_script_bytecode(const std::string &source)
Build-time script bytecode (QuickJS backend).
bool is_engine_available(JsEngineType type)
std::unique_ptr< JsEngine > create_js_engine(JsEngineType type)
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
bool register_embedded_script_bytecode(const std::uint8_t *blob, std::size_t size)
Returns false (and registers nothing) for a malformed blob, or one whose engine fingerprint differs f...
bool script_bytecode_cached(const std::string &source)
True when byte-identical source has stored bytecode (tests, diagnostics).
constexpr std::string_view engine_type_name(JsEngineType type)
Definition js_engine.hpp:66
@ cancel
Definition window_classes.hpp:126
NativeFunction NativePromiseFunction
Definition js_engine.hpp:29
std::function< std::optional< std::string >(std::string_view path)> ModuleResolver
Definition js_engine.hpp:21
std::function< void(std::string_view level, std::string_view message)> LogCallback
Definition js_engine.hpp:25
@ error
Definition design_ir.hpp:697
Definition js_engine.hpp:45
std::vector< HostObjectProperty > properties
Definition js_engine.hpp:47
std::string class_name
Definition js_engine.hpp:46
std::vector< HostObjectMethod > methods
Definition js_engine.hpp:48
Definition js_engine.hpp:40
std::string name
Definition js_engine.hpp:41
NativeFunction fn
Definition js_engine.hpp:42
Definition js_engine.hpp:35
choc::value::Value value
Definition js_engine.hpp:37
std::string name
Definition js_engine.hpp:36
Definition js_engine.hpp:51
uint64_t v8
Definition js_engine.hpp:55
uint64_t quickjs
Definition js_engine.hpp:53
uint64_t jsc
Definition js_engine.hpp:54
uint64_t total
Definition js_engine.hpp:52
Process-wide reuse of compiled script bytecode (QuickJS backend).
Definition js_engine.hpp:330
std::uint64_t bypassed
too small to cache, or cache disabled
Definition js_engine.hpp:333
std::uint64_t precompiled
of compiles, done by precompile_scripts()
Definition js_engine.hpp:334
std::uint64_t waits
evaluations that waited for an in-flight compile
Definition js_engine.hpp:335
std::uint64_t hits
evaluations served from stored bytecode
Definition js_engine.hpp:332
std::size_t entries
Definition js_engine.hpp:337
std::uint64_t embedded
misses served from build-time bytecode, not compiled
Definition js_engine.hpp:336
std::uint64_t compiles
scripts compiled and stored
Definition js_engine.hpp:331
std::size_t bytes
bytecode + key bytes held
Definition js_engine.hpp:338