@react-native-quickjs/quickjs 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +31 -0
  4. package/README.md +217 -0
  5. package/ReactNativeQuickJS.podspec +197 -0
  6. package/android/CMakeLists.txt +134 -0
  7. package/android/build.gradle +214 -0
  8. package/android/quickjs.gradle +39 -0
  9. package/android/src/main/AndroidManifest.xml +2 -0
  10. package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSInstance.kt +39 -0
  11. package/android/src/main/java/com/reactnativequickjs/quickjs/QuickJSPackage.kt +32 -0
  12. package/android/src/main/jni/JJSRuntimeFactory.h +37 -0
  13. package/android/src/main/jni/JQuickJSInstance.cpp +27 -0
  14. package/android/src/main/jni/JQuickJSInstance.h +46 -0
  15. package/android/src/main/jni/OnLoad.cpp +15 -0
  16. package/app.plugin.js +11 -0
  17. package/apple/RCTQuickJSInstanceFactory.h +32 -0
  18. package/apple/RCTQuickJSInstanceFactory.mm +16 -0
  19. package/bin/qjsc/darwin-arm64/qjsc +0 -0
  20. package/bin/qjsc/darwin-x64/qjsc +0 -0
  21. package/bin/qjsc/linux-arm64/qjsc +0 -0
  22. package/bin/qjsc/linux-x64/qjsc +0 -0
  23. package/bin/qjsc/manifest.json +11 -0
  24. package/bin/qjsc/win32-x64/qjsc.exe +0 -0
  25. package/bin/react-native-quickjs.js +28 -0
  26. package/cmake/jsi.cmake +24 -0
  27. package/cmake/quickjs.cmake +125 -0
  28. package/engine/quickjs-rel/LICENSE +24 -0
  29. package/engine/quickjs-rel/MANIFEST.json +55 -0
  30. package/engine/quickjs-rel/builtin-array-fromasync.h +120 -0
  31. package/engine/quickjs-rel/builtin-iterator-zip-keyed.h +332 -0
  32. package/engine/quickjs-rel/builtin-iterator-zip.h +337 -0
  33. package/engine/quickjs-rel/cutils.h +1998 -0
  34. package/engine/quickjs-rel/dtoa.c +1619 -0
  35. package/engine/quickjs-rel/dtoa.h +87 -0
  36. package/engine/quickjs-rel/libregexp-opcode.h +73 -0
  37. package/engine/quickjs-rel/libregexp.c +3478 -0
  38. package/engine/quickjs-rel/libregexp.h +101 -0
  39. package/engine/quickjs-rel/libunicode-table.h +5173 -0
  40. package/engine/quickjs-rel/libunicode.c +2069 -0
  41. package/engine/quickjs-rel/libunicode.h +172 -0
  42. package/engine/quickjs-rel/list.h +107 -0
  43. package/engine/quickjs-rel/quickjs-atom.h +280 -0
  44. package/engine/quickjs-rel/quickjs-c-atomics.h +54 -0
  45. package/engine/quickjs-rel/quickjs-opcode.h +385 -0
  46. package/engine/quickjs-rel/quickjs.c +64854 -0
  47. package/engine/quickjs-rel/quickjs.h +1577 -0
  48. package/modules/cdp/quickjs-cdp.c +1386 -0
  49. package/modules/cdp/quickjs-cdp.h +122 -0
  50. package/modules/cdp/react/QuickJSInspector.cpp +322 -0
  51. package/modules/cdp/react/QuickJSInspector.h +114 -0
  52. package/modules/hermes-compat/include/hermes/DebuggerAPI.h +20 -0
  53. package/modules/hermes-compat/include/hermes/Public/CrashManager.h +47 -0
  54. package/modules/hermes-compat/include/hermes/Public/CtorConfig.h +88 -0
  55. package/modules/hermes-compat/include/hermes/Public/GCConfig.h +83 -0
  56. package/modules/hermes-compat/include/hermes/Public/HermesExport.h +17 -0
  57. package/modules/hermes-compat/include/hermes/Public/RuntimeConfig.h +66 -0
  58. package/modules/hermes-compat/include/hermes/Public/SamplingProfiler.h +22 -0
  59. package/modules/hermes-compat/include/hermes/hermes.h +195 -0
  60. package/modules/hermes-compat/include/hermes/inspector/RuntimeAdapter.h +43 -0
  61. package/modules/hermes-compat/include/hermes/inspector-modern/chrome/Registration.h +31 -0
  62. package/modules/hermes-compat/include/hermes-compat/Diagnostics.h +35 -0
  63. package/modules/hermes-compat/src/HermesCompat.cpp +557 -0
  64. package/package.json +104 -0
  65. package/scripts/bytecode/compile.js +99 -0
  66. package/scripts/expo/plugin.js +231 -0
  67. package/scripts/postinstall.js +74 -0
  68. package/scripts/react_native_quickjs_pods.rb +237 -0
  69. package/scripts/setup/edits.js +311 -0
  70. package/scripts/setup/run.js +121 -0
  71. package/src/bytecode/QuickJSBytecode.cpp +50 -0
  72. package/src/bytecode/QuickJSBytecode.h +50 -0
  73. package/src/module/QuickJSCompat.cpp +92 -0
  74. package/src/module/QuickJSCompat.h +19 -0
  75. package/src/module/QuickJSModule.cpp +70 -0
  76. package/src/module/QuickJSModule.h +108 -0
  77. package/src/module/QuickJSModuleNative.h +93 -0
  78. package/src/runtime/QuickJSInstance.cpp +37 -0
  79. package/src/runtime/QuickJSInstance.h +74 -0
  80. package/src/runtime/QuickJSRuntime.cpp +1864 -0
  81. package/src/runtime/QuickJSRuntime.h +529 -0
  82. package/src/runtime/QuickJSRuntimeConfig.h +103 -0
  83. package/src/runtime/QuickJSRuntimeFactory.cpp +34 -0
  84. package/src/runtime/QuickJSRuntimeFactory.h +24 -0
@@ -0,0 +1,1864 @@
1
+ /*
2
+ * Copyright (c) Ammar Ahmed.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ #include "QuickJSRuntime.h"
9
+
10
+ #include <cstdint>
11
+ #include <cstring>
12
+ #include <string_view>
13
+ #include <type_traits>
14
+ #include <utility>
15
+ #include <vector>
16
+
17
+ #include "QuickJSBytecode.h"
18
+
19
+ namespace qjs {
20
+
21
+ static_assert(
22
+ std::atomic<std::thread::id>::is_always_lock_free,
23
+ "std::thread::id must be lock-free to be read on the release path");
24
+
25
+ static_assert(
26
+ !std::is_abstract<QuickJSRuntime>::value,
27
+ "QuickJSRuntime does not implement every jsi::Runtime method");
28
+
29
+ namespace {
30
+
31
+ struct NativePayloadLink {
32
+ NativePayloadLink *prev{nullptr};
33
+ NativePayloadLink *next{nullptr};
34
+ };
35
+
36
+ /// Opaque payload of a host object. Owned by the JS object and released by the
37
+ /// class finalizer.
38
+ struct HostObjectProxy {
39
+ NativePayloadLink link; // must stay first
40
+ QuickJSRuntime *runtime;
41
+ std::shared_ptr<jsi::HostObject> hostObject;
42
+ };
43
+
44
+ /// Opaque payload of a host function. `function` is returned by reference from
45
+ /// getHostFunction, so it must keep a stable address for the lifetime of the JS
46
+ /// function.
47
+ struct HostFunctionProxy {
48
+ NativePayloadLink link; // must stay first: the registry casts through it
49
+ QuickJSRuntime *runtime;
50
+ jsi::HostFunctionType function;
51
+ };
52
+
53
+ QuickJSRuntime *runtimeOf(JSContext *ctx) {
54
+ return static_cast<QuickJSRuntime *>(JS_GetContextOpaque(ctx));
55
+ }
56
+
57
+ void payloadLink(void *&head, void *proxy) {
58
+ auto *link = static_cast<NativePayloadLink *>(proxy);
59
+ link->prev = nullptr;
60
+ link->next = static_cast<NativePayloadLink *>(head);
61
+ if (link->next != nullptr) {
62
+ link->next->prev = link;
63
+ }
64
+ head = link;
65
+ }
66
+
67
+ void payloadUnlink(void *&head, void *proxy) {
68
+ auto *link = static_cast<NativePayloadLink *>(proxy);
69
+ if (link->prev != nullptr) {
70
+ link->prev->next = link->next;
71
+ } else if (head == link) {
72
+ head = link->next;
73
+ } else {
74
+ return;
75
+ }
76
+ if (link->next != nullptr) {
77
+ link->next->prev = link->prev;
78
+ }
79
+ link->prev = link->next = nullptr;
80
+ }
81
+
82
+ struct HostObjectHandlers {
83
+ static HostObjectProxy *proxyOf(JSContext *ctx, JSValueConst obj) {
84
+ return static_cast<HostObjectProxy *>(
85
+ JS_GetOpaque(obj, runtimeOf(ctx)->hostObjectClassID()));
86
+ }
87
+
88
+ static int getOwnProperty(
89
+ JSContext *ctx, JSPropertyDescriptor *desc, JSValueConst obj,
90
+ JSAtom prop) {
91
+ auto *proxy = proxyOf(ctx, obj);
92
+ if (proxy == nullptr) {
93
+ return false;
94
+ }
95
+ // A host object answers for every key, so an existence check needs no call
96
+ // into C++.
97
+ if (desc == nullptr) {
98
+ return true;
99
+ }
100
+
101
+ auto *runtime = proxy->runtime;
102
+ try {
103
+ jsi::PropNameID name =
104
+ runtime->createPropNameIDFrom(JS_DupAtom(ctx, prop));
105
+ jsi::Value value = proxy->hostObject->get(*runtime, name);
106
+ desc->flags = JS_PROP_C_W_E;
107
+ desc->value = JS_DupValue(ctx, runtime->toJSValue(value));
108
+ desc->getter = JS_UNDEFINED;
109
+ desc->setter = JS_UNDEFINED;
110
+ return true;
111
+ } catch (const std::exception &e) {
112
+ runtime->throwAsJSException(&e, "HostObject");
113
+ return -1;
114
+ } catch (...) {
115
+ runtime->throwAsJSException(nullptr, "HostObject");
116
+ return -1;
117
+ }
118
+ }
119
+
120
+ static int getOwnPropertyNames(
121
+ JSContext *ctx, JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj) {
122
+ auto *proxy = proxyOf(ctx, obj);
123
+ if (proxy == nullptr) {
124
+ *ptab = nullptr;
125
+ *plen = 0;
126
+ return 0;
127
+ }
128
+
129
+ auto *runtime = proxy->runtime;
130
+ std::vector<jsi::PropNameID> names;
131
+ try {
132
+ names = proxy->hostObject->getPropertyNames(*runtime);
133
+ } catch (const std::exception &e) {
134
+ runtime->throwAsJSException(&e, "HostObject");
135
+ return -1;
136
+ } catch (...) {
137
+ runtime->throwAsJSException(nullptr, "HostObject");
138
+ return -1;
139
+ }
140
+
141
+ // JSI allows duplicates here; the property enumeration protocol does not.
142
+ std::vector<JSAtom> unique;
143
+ std::unordered_set<JSAtom> seen;
144
+ unique.reserve(names.size());
145
+ for (const jsi::PropNameID &name : names) {
146
+ JSAtom atom = QuickJSRuntime::toJSAtom(name);
147
+ if (seen.insert(atom).second) {
148
+ unique.push_back(atom);
149
+ }
150
+ }
151
+
152
+ auto *table = static_cast<JSPropertyEnum *>(js_malloc(
153
+ ctx, sizeof(JSPropertyEnum) * std::max<size_t>(unique.size(), 1)));
154
+ if (table == nullptr) {
155
+ return -1;
156
+ }
157
+ for (size_t i = 0; i < unique.size(); ++i) {
158
+ table[i].is_enumerable = true;
159
+ table[i].atom = JS_DupAtom(ctx, unique[i]);
160
+ }
161
+
162
+ *ptab = table;
163
+ *plen = static_cast<uint32_t>(unique.size());
164
+ return 0;
165
+ }
166
+
167
+ static int setProperty(
168
+ JSContext *ctx, JSValueConst obj, JSAtom atom, JSValueConst value,
169
+ JSValueConst receiver, int flags) {
170
+ auto *proxy = proxyOf(ctx, obj);
171
+ if (proxy == nullptr) {
172
+ return false;
173
+ }
174
+
175
+ // OrdinarySetWithOwnDescriptor: an assignment that reached this host object
176
+ // by walking a receiver's prototype chain creates an own property on the
177
+ // receiver instead of calling HostObject::set. React Native's TurboModules
178
+ // depend on it -- TurboModule::get memoises each method by writing it back
179
+ // onto a plain object whose prototype is the module's HostObject.
180
+ const bool receiverIsThisObject =
181
+ JS_VALUE_GET_TAG(receiver) == JS_VALUE_GET_TAG(obj) &&
182
+ JS_VALUE_GET_PTR(receiver) == JS_VALUE_GET_PTR(obj);
183
+
184
+ if (!JS_IsUndefined(receiver) && !receiverIsThisObject) {
185
+ // Step 3.a returns false for a non-object receiver, reachable through
186
+ // Reflect.set(hostObj, k, v, 5). Defining would throw instead.
187
+ if (JS_VALUE_GET_TAG(receiver) != JS_TAG_OBJECT) {
188
+ return false;
189
+ }
190
+ // Defining is safe: quickjs only consults a prototype's set_property
191
+ // after failing to find an own property. Both throw flags are forwarded
192
+ // so the caller's own strictness policy is reproduced -- without them a
193
+ // strict assignment onto a frozen receiver returns false where the spec
194
+ // throws.
195
+ int defined = JS_DefinePropertyValue(
196
+ ctx, receiver, JS_DupAtom(ctx, atom), JS_DupValue(ctx, value),
197
+ JS_PROP_C_W_E | (flags & (JS_PROP_THROW | JS_PROP_THROW_STRICT)));
198
+ return defined < 0 ? -1 : defined;
199
+ }
200
+
201
+ auto *runtime = proxy->runtime;
202
+ try {
203
+ jsi::PropNameID name =
204
+ runtime->createPropNameIDFrom(JS_DupAtom(ctx, atom));
205
+ proxy->hostObject->set(
206
+ *runtime, name, runtime->createValue(JS_DupValue(ctx, value)));
207
+ return true;
208
+ } catch (const std::exception &e) {
209
+ runtime->throwAsJSException(&e, "HostObject");
210
+ return -1;
211
+ } catch (...) {
212
+ runtime->throwAsJSException(nullptr, "HostObject");
213
+ return -1;
214
+ }
215
+ }
216
+
217
+ static void finalizer(JSRuntime *rt, JSValueConst value) {
218
+ auto *runtime = static_cast<QuickJSRuntime *>(JS_GetRuntimeOpaque(rt));
219
+ auto *proxy = static_cast<HostObjectProxy *>(
220
+ JS_GetOpaque(value, runtime->hostObjectClassID()));
221
+ runtime->unregisterHostObject(proxy);
222
+ delete proxy;
223
+ }
224
+ };
225
+
226
+ JSClassExoticMethods gHostObjectExotic = {
227
+ /* get_own_property */ HostObjectHandlers::getOwnProperty,
228
+ /* get_own_property_names */ HostObjectHandlers::getOwnPropertyNames,
229
+ /* delete_property */ nullptr,
230
+ /* define_own_property */ nullptr,
231
+ /* has_property */ nullptr,
232
+ /* get_property */ nullptr,
233
+ /* set_property */ HostObjectHandlers::setProperty,
234
+ };
235
+
236
+ struct HostFunctionHandlers {
237
+ static JSValue call(
238
+ JSContext *ctx, JSValueConst funcObj, JSValueConst thisVal, int argc,
239
+ JSValueConst *argv, int /*flags*/) {
240
+ auto *runtime = runtimeOf(ctx);
241
+ auto *proxy = static_cast<HostFunctionProxy *>(
242
+ JS_GetOpaque(funcObj, runtime->hostFunctionClassID()));
243
+ if (proxy == nullptr) {
244
+ return JS_ThrowTypeError(ctx, "not a host function");
245
+ }
246
+
247
+ try {
248
+ // Borrowed, not dup'd: quickjs keeps argv and thisVal alive for the whole
249
+ // call, so a reference taken here and dropped on return would cancel out.
250
+ constexpr int kInlineArgs = 8;
251
+ jsi::Value inlineArgs[kInlineArgs];
252
+ std::vector<jsi::Value> heapArgs;
253
+ jsi::Value *args = inlineArgs;
254
+ if (argc > kInlineArgs) {
255
+ heapArgs.resize(static_cast<size_t>(argc));
256
+ args = heapArgs.data();
257
+ }
258
+ for (int i = 0; i < argc; ++i) {
259
+ args[i] = runtime->borrowValue(argv[i]);
260
+ }
261
+ jsi::Value thisValue = runtime->borrowValue(thisVal);
262
+ jsi::Value result =
263
+ proxy->function(*runtime, thisValue, args, static_cast<size_t>(argc));
264
+ return runtime->takeJSValue(std::move(result));
265
+ } catch (const std::exception &e) {
266
+ return runtime->throwAsJSException(&e);
267
+ } catch (...) {
268
+ return runtime->throwAsJSException(nullptr);
269
+ }
270
+ }
271
+
272
+ static void finalizer(JSRuntime *rt, JSValueConst value) {
273
+ auto *runtime = static_cast<QuickJSRuntime *>(JS_GetRuntimeOpaque(rt));
274
+ auto *proxy = static_cast<HostFunctionProxy *>(
275
+ JS_GetOpaque(value, runtime->hostFunctionClassID()));
276
+ runtime->unregisterHostFunction(proxy);
277
+ delete proxy;
278
+ }
279
+ };
280
+
281
+ struct NativeStateProxy {
282
+ NativePayloadLink link; // must stay first
283
+ std::shared_ptr<jsi::NativeState> state;
284
+ };
285
+
286
+ void nativeStateFinalizer(JSRuntime *rt, JSValueConst value) {
287
+ auto *runtime = static_cast<QuickJSRuntime *>(JS_GetRuntimeOpaque(rt));
288
+ auto *proxy = static_cast<NativeStateProxy *>(
289
+ JS_GetOpaque(value, runtime->nativeStateClassID()));
290
+ runtime->unregisterNativeState(proxy);
291
+ delete proxy;
292
+ }
293
+
294
+ /// Keeps a jsi::MutableBuffer alive for as long as the ArrayBuffer pointing
295
+ /// into it.
296
+ struct ArrayBufferProxy {
297
+ std::shared_ptr<jsi::MutableBuffer> buffer;
298
+ };
299
+
300
+ void freeArrayBufferProxy(JSRuntime * /*rt*/, void *opaque, void * /*ptr*/) {
301
+ delete static_cast<ArrayBufferProxy *>(opaque);
302
+ }
303
+
304
+ const char *kEnumeratePropertyNamesSource =
305
+ "(function (o) { const r = []; for (const k in o) r.push(k); return r; })";
306
+ JSValue microtaskJob(JSContext *ctx, int argc, JSValueConst *argv) {
307
+ if (argc < 1) {
308
+ return JS_UNDEFINED;
309
+ }
310
+ return JS_Call(ctx, argv[0], JS_UNDEFINED, 0, nullptr);
311
+ }
312
+
313
+ const char *kSymbolToStringSource = "(function (s) { return s.toString(); })";
314
+ const char *kBigIntToStringSource =
315
+ "(function (b, radix) { return b.toString(radix); })";
316
+
317
+ } // namespace
318
+
319
+ QuickJSRuntime::QuickJSRuntime(QuickJSRuntimeConfig config)
320
+ : config_(std::move(config)), jsThread_(std::this_thread::get_id()) {
321
+ runtime_ = JS_NewRuntime();
322
+ if (runtime_ == nullptr) {
323
+ throw jsi::JSINativeException("QuickJSRuntime: JS_NewRuntime failed");
324
+ }
325
+
326
+ if (config_.memoryLimit > 0) {
327
+ JS_SetMemoryLimit(runtime_, config_.memoryLimit);
328
+ }
329
+ if (config_.stackSize > 0) {
330
+ JS_SetMaxStackSize(runtime_, config_.stackSize);
331
+ }
332
+
333
+ if (config_.deferGC) {
334
+ JS_SetGCDeferred(runtime_, true, config_.deferredGCLimit);
335
+ }
336
+
337
+ context_ = JS_NewContext(runtime_);
338
+ if (context_ == nullptr) {
339
+ JS_FreeRuntime(runtime_);
340
+ runtime_ = nullptr;
341
+ throw jsi::JSINativeException("QuickJSRuntime: JS_NewContext failed");
342
+ }
343
+
344
+ JS_SetContextOpaque(context_, this);
345
+ JS_SetRuntimeOpaque(runtime_, this);
346
+
347
+ // Baseline for the pressure valve. Left at zero, the first `grown` reading is
348
+ // the whole live heap and the valve fires whatever the app is doing.
349
+ heapBaseline_ = JS_GetMallocSize(runtime_);
350
+
351
+ registerClasses();
352
+ enumeratePropertyNames_ = evalInternal(kEnumeratePropertyNamesSource);
353
+ symbolToString_ = evalInternal(kSymbolToStringSource);
354
+ bigIntToString_ = evalInternal(kBigIntToStringSource);
355
+
356
+ JSValue nativeStateKey = JS_NewPrivateSymbol(context_, "nativeState");
357
+ nativeStateKey_ = JS_ValueToAtom(context_, nativeStateKey);
358
+ JS_FreeValue(context_, nativeStateKey);
359
+
360
+ // WeakRef has no C API, so reach for the JS-visible constructor once.
361
+ JSValue global = JS_GetGlobalObject(context_);
362
+ weakRefConstructor_ = JS_GetPropertyStr(context_, global, "WeakRef");
363
+ if (JS_IsObject(weakRefConstructor_)) {
364
+ JSValue proto =
365
+ JS_GetPropertyStr(context_, weakRefConstructor_, "prototype");
366
+ weakRefDeref_ = JS_GetPropertyStr(context_, proto, "deref");
367
+ JS_FreeValue(context_, proto);
368
+ }
369
+ JS_FreeValue(context_, global);
370
+ }
371
+
372
+ void QuickJSRuntime::registerClasses() {
373
+ JS_NewClassID(runtime_, &hostObjectClassID_);
374
+ JSClassDef hostObjectDef = {};
375
+ hostObjectDef.class_name = "HostObject";
376
+ hostObjectDef.finalizer = HostObjectHandlers::finalizer;
377
+ hostObjectDef.exotic = &gHostObjectExotic;
378
+ JS_NewClass(runtime_, hostObjectClassID_, &hostObjectDef);
379
+
380
+ JS_NewClassID(runtime_, &hostFunctionClassID_);
381
+ JSClassDef hostFunctionDef = {};
382
+ hostFunctionDef.class_name = "HostFunction";
383
+ hostFunctionDef.finalizer = HostFunctionHandlers::finalizer;
384
+ hostFunctionDef.call = HostFunctionHandlers::call;
385
+ JS_NewClass(runtime_, hostFunctionClassID_, &hostFunctionDef);
386
+
387
+ JS_NewClassID(runtime_, &nativeStateClassID_);
388
+ JSClassDef nativeStateDef = {};
389
+ nativeStateDef.class_name = "NativeState";
390
+ nativeStateDef.finalizer = nativeStateFinalizer;
391
+ JS_NewClass(runtime_, nativeStateClassID_, &nativeStateDef);
392
+
393
+ // Host functions must be indistinguishable from JS functions: without
394
+ // Function.prototype, `instanceof Function` is false and bind/call/apply are
395
+ // missing.
396
+ JSValue global = JS_GetGlobalObject(context_);
397
+ JSValue functionConstructor = JS_GetPropertyStr(context_, global, "Function");
398
+ JS_SetClassProto(
399
+ context_, hostFunctionClassID_,
400
+ JS_GetPropertyStr(context_, functionConstructor, "prototype"));
401
+ JS_FreeValue(context_, functionConstructor);
402
+ JS_FreeValue(context_, global);
403
+ }
404
+
405
+ void QuickJSRuntime::registerHostObject(void *proxy) {
406
+ payloadLink(hostObjectProxies_, proxy);
407
+ }
408
+
409
+ void QuickJSRuntime::unregisterHostObject(void *proxy) {
410
+ payloadUnlink(hostObjectProxies_, proxy);
411
+ }
412
+
413
+ void QuickJSRuntime::registerNativeState(void *proxy) {
414
+ payloadLink(nativeStateProxies_, proxy);
415
+ }
416
+
417
+ void QuickJSRuntime::unregisterNativeState(void *proxy) {
418
+ payloadUnlink(nativeStateProxies_, proxy);
419
+ }
420
+
421
+ void QuickJSRuntime::registerHostFunction(void *proxy) {
422
+ payloadLink(hostFunctionProxies_, proxy);
423
+ }
424
+
425
+ void QuickJSRuntime::unregisterHostFunction(void *proxy) {
426
+ payloadUnlink(hostFunctionProxies_, proxy);
427
+ }
428
+
429
+ /// Leaves the lists intact: the proxies stay allocated and their finalizers
430
+ /// still run during JS_FreeRuntime, which is what unlinks them. Releasing a
431
+ /// payload can free jsi values and so re-enter, hence remembering `next` first.
432
+ void QuickJSRuntime::releaseNativePayloads() noexcept {
433
+ for (auto *l = static_cast<NativePayloadLink *>(hostObjectProxies_);
434
+ l != nullptr;) {
435
+ auto *next = l->next;
436
+ reinterpret_cast<HostObjectProxy *>(l)->hostObject.reset();
437
+ l = next;
438
+ }
439
+ for (auto *l = static_cast<NativePayloadLink *>(hostFunctionProxies_);
440
+ l != nullptr;) {
441
+ auto *next = l->next;
442
+ reinterpret_cast<HostFunctionProxy *>(l)->function = nullptr;
443
+ l = next;
444
+ }
445
+ for (auto *l = static_cast<NativePayloadLink *>(nativeStateProxies_);
446
+ l != nullptr;) {
447
+ auto *next = l->next;
448
+ reinterpret_cast<NativeStateProxy *>(l)->state.reset();
449
+ l = next;
450
+ }
451
+ }
452
+
453
+ /// Undefined rather than throwing: a runtime that cannot build a helper is
454
+ /// still usable for everything that does not need it.
455
+ JSValue QuickJSRuntime::evalInternal(const char *source) noexcept {
456
+ JSValue fn = JS_Eval(
457
+ context_, source, std::strlen(source), "<jsi-internal>",
458
+ JS_EVAL_TYPE_GLOBAL);
459
+ if (JS_IsException(fn)) {
460
+ JS_FreeValue(context_, JS_GetException(context_));
461
+ return JS_UNDEFINED;
462
+ }
463
+ return fn;
464
+ }
465
+
466
+ QuickJSRuntime::~QuickJSRuntime() {
467
+ drainPendingReleases();
468
+ releaseNativePayloads();
469
+ JS_FreeValue(context_, enumeratePropertyNames_);
470
+ JS_FreeValue(context_, symbolToString_);
471
+ JS_FreeValue(context_, weakRefConstructor_);
472
+ JS_FreeValue(context_, weakRefDeref_);
473
+ JS_FreeValue(context_, bigIntToString_);
474
+ JS_FreeAtom(context_, nativeStateKey_);
475
+
476
+ if (context_ != nullptr) {
477
+ JS_FreeContext(context_);
478
+ context_ = nullptr;
479
+ }
480
+
481
+ if (runtime_ != nullptr) {
482
+ // JS_FreeRuntime runs one GC pass then asserts the heap is empty, but a
483
+ // finalizer releasing a jsi value dirties it again, so settle first.
484
+ for (int pass = 0; pass < kMaxTeardownGCPasses; ++pass) {
485
+ JS_RunGC(runtime_);
486
+ drainPendingReleases();
487
+ }
488
+ JS_FreeRuntime(runtime_);
489
+ runtime_ = nullptr;
490
+ }
491
+
492
+ // After the engine: teardown still recycles into the free list.
493
+ freePointerValuePool();
494
+ }
495
+
496
+ void QuickJSRuntime::lock() noexcept {
497
+ engineMutex_.lock();
498
+ adoptCurrentThread();
499
+ drainPendingReleases();
500
+ }
501
+
502
+ void QuickJSRuntime::unlock() noexcept {
503
+ engineMutex_.unlock();
504
+ }
505
+
506
+ /**
507
+ * quickjs captures rt->stack_top inside JS_NewRuntime, on whichever thread
508
+ * called it. On React Native's JS thread that mark was 1,115,672 bytes above
509
+ * the real stack pointer, so every call threw "Maximum call stack size
510
+ * exceeded" and the app aborted while loading its bundle.
511
+ *
512
+ * The budget is recomputed rather than only re-based, because a 1 MiB default
513
+ * on a smaller thread puts the limit past the end of the mapping and trades a
514
+ * catchable RangeError for a stack smash. The quarter held back covers the
515
+ * native frames reached after the overflow check fires.
516
+ */
517
+ void QuickJSRuntime::adoptCurrentThread() noexcept {
518
+ if (isOnJSThread() || runtime_ == nullptr) {
519
+ return;
520
+ }
521
+ jsThread_.store(std::this_thread::get_id(), std::memory_order_relaxed);
522
+ JS_UpdateStackTop(runtime_);
523
+
524
+ if (config_.stackSize > 0) {
525
+ return;
526
+ }
527
+ const size_t remaining = remainingNativeStackBytes();
528
+ if (remaining > 0) {
529
+ JS_SetMaxStackSize(runtime_, remaining / 4 * 3);
530
+ }
531
+ }
532
+
533
+ /// 0 when the platform cannot say, so the caller leaves the engine default
534
+ /// alone rather than guessing.
535
+ size_t QuickJSRuntime::remainingNativeStackBytes() noexcept {
536
+ char here = 0;
537
+ const auto sp = reinterpret_cast<uintptr_t>(&here);
538
+ #if defined(__ANDROID__) || defined(__linux__)
539
+ pthread_attr_t attr;
540
+ if (pthread_getattr_np(pthread_self(), &attr) != 0) {
541
+ return 0;
542
+ }
543
+ void *base = nullptr;
544
+ size_t size = 0;
545
+ const int rc = pthread_attr_getstack(&attr, &base, &size);
546
+ pthread_attr_destroy(&attr);
547
+ if (rc != 0 || base == nullptr || size == 0) {
548
+ return 0;
549
+ }
550
+ // pthread_attr_getstack reports the lowest address; the stack grows down.
551
+ const auto lowest = reinterpret_cast<uintptr_t>(base);
552
+ return sp > lowest ? static_cast<size_t>(sp - lowest) : 0;
553
+ #elif defined(__APPLE__)
554
+ void *high = pthread_get_stackaddr_np(pthread_self());
555
+ const size_t size = pthread_get_stacksize_np(pthread_self());
556
+ if (high == nullptr || size == 0) {
557
+ return 0;
558
+ }
559
+ const auto lowest = reinterpret_cast<uintptr_t>(high) - size;
560
+ return sp > lowest ? static_cast<size_t>(sp - lowest) : 0;
561
+ #else
562
+ (void)sp;
563
+ return 0;
564
+ #endif
565
+ }
566
+
567
+ void QuickJSRuntime::releaseValue(JSValue value) noexcept {
568
+ if (isOnJSThread()) {
569
+ JS_FreeValueRT(runtime_, value);
570
+ return;
571
+ }
572
+ std::lock_guard<std::mutex> lock(pendingMutex_);
573
+ pendingValues_.push_back(value);
574
+ // Published after the push, so a drainer that sees the flag sees the item.
575
+ hasPendingReleases_.store(true, std::memory_order_release);
576
+ }
577
+
578
+ void QuickJSRuntime::releaseAtom(JSAtom atom) noexcept {
579
+ if (isOnJSThread()) {
580
+ JS_FreeAtomRT(runtime_, atom);
581
+ return;
582
+ }
583
+ std::lock_guard<std::mutex> lock(pendingMutex_);
584
+ pendingAtoms_.push_back(atom);
585
+ hasPendingReleases_.store(true, std::memory_order_release);
586
+ }
587
+
588
+ void QuickJSRuntime::drainPendingReleases() noexcept {
589
+ // Racing a producer is harmless: the flag is set after the push, so a release
590
+ // landing just after this load is drained by the next call.
591
+ if (!hasPendingReleases_.load(std::memory_order_acquire)) {
592
+ return;
593
+ }
594
+
595
+ std::vector<JSValue> values;
596
+ std::vector<JSAtom> atoms;
597
+ std::vector<QuickJSPointerValue *> pointerValues;
598
+ std::vector<QuickJSAtomPointerValue *> atomPointerValues;
599
+ {
600
+ std::lock_guard<std::mutex> lock(pendingMutex_);
601
+ hasPendingReleases_.store(false, std::memory_order_relaxed);
602
+ values.swap(pendingValues_);
603
+ atoms.swap(pendingAtoms_);
604
+ pointerValues.swap(pendingPointerValues_);
605
+ atomPointerValues.swap(pendingAtomPointerValues_);
606
+ }
607
+
608
+ for (JSValue value : values) {
609
+ JS_FreeValueRT(runtime_, value);
610
+ }
611
+ for (JSAtom atom : atoms) {
612
+ JS_FreeAtomRT(runtime_, atom);
613
+ }
614
+ for (QuickJSPointerValue *pv : pointerValues) {
615
+ pv->nextFree_ = freeValues_;
616
+ freeValues_ = pv;
617
+ }
618
+ for (QuickJSAtomPointerValue *pv : atomPointerValues) {
619
+ pv->nextFree_ = freeAtoms_;
620
+ freeAtoms_ = pv;
621
+ }
622
+ }
623
+
624
+ void QuickJSRuntime::refillValueSlab() {
625
+ auto *slab = static_cast<QuickJSPointerValue *>(
626
+ ::operator new(sizeof(QuickJSPointerValue) * kPointerValueSlabSize));
627
+ valueSlabs_.push_back(slab);
628
+ for (size_t i = 0; i < kPointerValueSlabSize; ++i) {
629
+ auto *slot = slab + i;
630
+ slot->nextFree_ = freeValues_;
631
+ freeValues_ = slot;
632
+ }
633
+ }
634
+
635
+ void QuickJSRuntime::refillAtomSlab() {
636
+ auto *slab = static_cast<QuickJSAtomPointerValue *>(
637
+ ::operator new(sizeof(QuickJSAtomPointerValue) * kPointerValueSlabSize));
638
+ atomSlabs_.push_back(slab);
639
+ for (size_t i = 0; i < kPointerValueSlabSize; ++i) {
640
+ auto *slot = slab + i;
641
+ slot->nextFree_ = freeAtoms_;
642
+ freeAtoms_ = slot;
643
+ }
644
+ }
645
+
646
+ void QuickJSRuntime::queuePointerValue(QuickJSPointerValue *pv) noexcept {
647
+ std::lock_guard<std::mutex> lock(pendingMutex_);
648
+ pendingPointerValues_.push_back(pv);
649
+ hasPendingReleases_.store(true, std::memory_order_release);
650
+ }
651
+
652
+ void QuickJSRuntime::queueAtomPointerValue(
653
+ QuickJSAtomPointerValue *pv) noexcept {
654
+ std::lock_guard<std::mutex> lock(pendingMutex_);
655
+ pendingAtomPointerValues_.push_back(pv);
656
+ hasPendingReleases_.store(true, std::memory_order_release);
657
+ }
658
+
659
+ void QuickJSRuntime::freePointerValuePool() noexcept {
660
+ for (void *slab : valueSlabs_) {
661
+ ::operator delete(slab);
662
+ }
663
+ for (void *slab : atomSlabs_) {
664
+ ::operator delete(slab);
665
+ }
666
+ valueSlabs_.clear();
667
+ atomSlabs_.clear();
668
+ freeValues_ = nullptr;
669
+ freeAtoms_ = nullptr;
670
+ }
671
+
672
+ JSValue QuickJSRuntime::toJSValue(const jsi::Pointer &pointer) {
673
+ return static_cast<const QuickJSPointerValue *>(getPointerValue(pointer))
674
+ ->value();
675
+ }
676
+
677
+ JSAtom QuickJSRuntime::toJSAtom(const jsi::PropNameID &name) {
678
+ return static_cast<const QuickJSAtomPointerValue *>(getPointerValue(name))
679
+ ->atom();
680
+ }
681
+
682
+ JSValue QuickJSRuntime::toJSValue(const jsi::Value &value) const {
683
+ if (value.isUndefined()) {
684
+ return JS_UNDEFINED;
685
+ }
686
+ if (value.isNull()) {
687
+ return JS_NULL;
688
+ }
689
+ if (value.isBool()) {
690
+ return JS_NewBool(context_, value.getBool());
691
+ }
692
+ if (value.isNumber()) {
693
+ return JS_NewFloat64(context_, value.getNumber());
694
+ }
695
+ return static_cast<const QuickJSPointerValue *>(getPointerValue(value))
696
+ ->value();
697
+ }
698
+
699
+ jsi::Value QuickJSRuntime::createValue(JSValue value) {
700
+ if (JS_IsException(value)) {
701
+ throwPendingError();
702
+ }
703
+ if (JS_IsUndefined(value)) {
704
+ return jsi::Value::undefined();
705
+ }
706
+ if (JS_IsNull(value)) {
707
+ return jsi::Value::null();
708
+ }
709
+ if (JS_IsBool(value)) {
710
+ return jsi::Value(static_cast<bool>(JS_ToBool(context_, value)));
711
+ }
712
+ if (JS_IsNumber(value)) {
713
+ double number = 0;
714
+ JS_ToFloat64(context_, &number, value);
715
+ return jsi::Value(number);
716
+ }
717
+ if (JS_IsString(value)) {
718
+ return jsi::Value(createStringFrom(value));
719
+ }
720
+ if (JS_IsSymbol(value)) {
721
+ return jsi::Value(createSymbolFrom(value));
722
+ }
723
+ if (JS_IsBigInt(value)) {
724
+ return jsi::Value(make<jsi::BigInt>(allocPointerValue(value)));
725
+ }
726
+ return jsi::Value(createObjectFrom(value));
727
+ }
728
+
729
+ jsi::Value QuickJSRuntime::borrowValue(JSValue value) {
730
+ // Primitives carry no reference, so they are already borrow-shaped.
731
+ if (JS_IsUndefined(value)) {
732
+ return jsi::Value::undefined();
733
+ }
734
+ if (JS_IsNull(value)) {
735
+ return jsi::Value::null();
736
+ }
737
+ if (JS_IsBool(value)) {
738
+ return jsi::Value(static_cast<bool>(JS_ToBool(context_, value)));
739
+ }
740
+ if (JS_IsNumber(value)) {
741
+ double number = 0;
742
+ JS_ToFloat64(context_, &number, value);
743
+ return jsi::Value(number);
744
+ }
745
+ if (JS_IsString(value)) {
746
+ return jsi::Value(make<jsi::String>(allocPointerValue(value, false)));
747
+ }
748
+ if (JS_IsSymbol(value)) {
749
+ return jsi::Value(make<jsi::Symbol>(allocPointerValue(value, false)));
750
+ }
751
+ if (JS_IsBigInt(value)) {
752
+ return jsi::Value(make<jsi::BigInt>(allocPointerValue(value, false)));
753
+ }
754
+ return jsi::Value(make<jsi::Object>(allocPointerValue(value, false)));
755
+ }
756
+
757
+ JSValue QuickJSRuntime::takeJSValue(jsi::Value &&value) {
758
+ if (value.isUndefined()) {
759
+ return JS_UNDEFINED;
760
+ }
761
+ if (value.isNull()) {
762
+ return JS_NULL;
763
+ }
764
+ if (value.isBool()) {
765
+ return JS_NewBool(context_, value.getBool());
766
+ }
767
+ if (value.isNumber()) {
768
+ return JS_NewFloat64(context_, value.getNumber());
769
+ }
770
+ auto *pv = const_cast<QuickJSPointerValue *>(
771
+ static_cast<const QuickJSPointerValue *>(getPointerValue(value)));
772
+ if (!pv->owned_) {
773
+ return JS_DupValue(context_, pv->value());
774
+ }
775
+ // Hand the reference over and disarm the destructor, which is about to run
776
+ // as `value` goes out of scope.
777
+ pv->owned_ = false;
778
+ return pv->value();
779
+ }
780
+
781
+ jsi::Object QuickJSRuntime::createObjectFrom(JSValue value) {
782
+ return make<jsi::Object>(allocPointerValue(value));
783
+ }
784
+
785
+ jsi::Symbol QuickJSRuntime::createSymbolFrom(JSValue value) {
786
+ return make<jsi::Symbol>(allocPointerValue(value));
787
+ }
788
+
789
+ jsi::String QuickJSRuntime::createStringFrom(JSValue value) {
790
+ return make<jsi::String>(allocPointerValue(value));
791
+ }
792
+
793
+ jsi::PropNameID QuickJSRuntime::createPropNameIDFrom(JSAtom atom) {
794
+ return make<jsi::PropNameID>(allocAtomPointerValue(atom));
795
+ }
796
+
797
+ /**
798
+ * Runs where describing an exception normally has already failed: JS_ToCString
799
+ * calls toString, and reading .stack is what jsi::JSError does and what was
800
+ * throwing. So own properties only, and anything they raise is swallowed --
801
+ * a pending exception left behind would surface later against unrelated code.
802
+ */
803
+ std::string QuickJSRuntime::describeExceptionWithoutRunningJS(
804
+ JSValue exception) {
805
+ if (JS_IsString(exception)) {
806
+ const char *s = JS_ToCString(context_, exception);
807
+ std::string out = s != nullptr
808
+ ? std::string("<string exception: ") + s + ">"
809
+ : std::string("<indescribable exception>");
810
+ if (s != nullptr) {
811
+ JS_FreeCString(context_, s);
812
+ }
813
+ return out;
814
+ }
815
+ if (!JS_IsObject(exception)) {
816
+ return "<non-object exception>";
817
+ }
818
+
819
+ auto readString = [&](const char *key) -> std::string {
820
+ JSValue v = JS_GetPropertyStr(context_, exception, key);
821
+ if (JS_IsException(v)) {
822
+ JS_FreeValue(context_, JS_GetException(context_));
823
+ return {};
824
+ }
825
+ std::string out;
826
+ if (JS_IsString(v)) {
827
+ const char *s = JS_ToCString(context_, v);
828
+ if (s != nullptr) {
829
+ out = s;
830
+ JS_FreeCString(context_, s);
831
+ }
832
+ }
833
+ JS_FreeValue(context_, v);
834
+ return out;
835
+ };
836
+
837
+ std::string name = readString("name");
838
+ std::string message = readString("message");
839
+ if (name.empty() && message.empty()) {
840
+ return "<indescribable exception>";
841
+ }
842
+ if (message.empty()) {
843
+ return name;
844
+ }
845
+ if (name.empty()) {
846
+ return message;
847
+ }
848
+ return name + ": " + message;
849
+ }
850
+
851
+ void QuickJSRuntime::throwPendingError() {
852
+ JSValue exception = JS_GetException(context_);
853
+ if (JS_IsNull(exception) || JS_IsUndefined(exception)) {
854
+ JS_FreeValue(context_, exception);
855
+ throw jsi::JSINativeException(
856
+ "QuickJSRuntime: operation failed with no pending exception");
857
+ }
858
+
859
+ if (errorDepth_ >= kMaxErrorDepth) {
860
+ JS_FreeValue(context_, exception);
861
+ std::string what =
862
+ "QuickJSRuntime: exception thrown while handling an exception";
863
+ if (!firstErrorDescription_.empty()) {
864
+ what += "; original exception: " + firstErrorDescription_;
865
+ }
866
+ // A recursive bail-out is usually a stack-limit problem, and the limit is
867
+ // the one thing invisible from JavaScript.
868
+ what += "; jsThreadOwnsEngine=";
869
+ what += isOnJSThread() ? "true" : "false";
870
+ what +=
871
+ ", remainingNativeStack=" + std::to_string(remainingNativeStackBytes());
872
+ what += ", configuredStackSize=" + std::to_string(config_.stackSize);
873
+ throw jsi::JSINativeException(what);
874
+ }
875
+
876
+ if (errorDepth_ == 0) {
877
+ firstErrorDescription_ = describeExceptionWithoutRunningJS(exception);
878
+ }
879
+
880
+ struct DepthGuard {
881
+ int &depth;
882
+ explicit DepthGuard(int &d) : depth(d) {
883
+ ++depth;
884
+ }
885
+ ~DepthGuard() {
886
+ --depth;
887
+ }
888
+ } guard(errorDepth_);
889
+
890
+ throw jsi::JSError(*this, createValue(exception));
891
+ }
892
+
893
+ JSValue QuickJSRuntime::checkException(JSValue value) {
894
+ if (JS_IsException(value)) {
895
+ throwPendingError();
896
+ }
897
+ return value;
898
+ }
899
+
900
+ void QuickJSRuntime::checkException(int result) {
901
+ if (result < 0) {
902
+ throwPendingError();
903
+ }
904
+ }
905
+
906
+ JSValue QuickJSRuntime::throwAsJSException(
907
+ const std::exception *e, const char *origin) noexcept {
908
+ // A JSError already carries a thrown JS value, so rethrow it unchanged.
909
+ if (const auto *jsError = dynamic_cast<const jsi::JSError *>(e)) {
910
+ return JS_Throw(
911
+ context_, JS_DupValue(context_, toJSValue(jsError->value())));
912
+ }
913
+ return JS_ThrowInternalError(
914
+ context_, "Exception in %s: %s", origin,
915
+ e != nullptr ? e->what() : "unknown C++ exception");
916
+ }
917
+
918
+ std::string QuickJSRuntime::description() {
919
+ return "QuickJSRuntime";
920
+ }
921
+
922
+ /// Tied to enableDebugger rather than an unconditional true: a runtime built
923
+ /// without instrumentation cannot bind a breakpoint, because the statement
924
+ /// traps are emitted at parse time.
925
+ bool QuickJSRuntime::isInspectable() {
926
+ return config_.enableDebugger;
927
+ }
928
+
929
+ namespace {
930
+
931
+ void throwIfHermesBytecode(
932
+ const uint8_t *data, size_t size, const std::string &sourceURL) {
933
+ if (!isHermesBytecode(data, size)) {
934
+ return;
935
+ }
936
+ throw jsi::JSINativeException(
937
+ "QuickJSRuntime: " +
938
+ (sourceURL.empty() ? std::string("this bundle") : sourceURL) +
939
+ " is Hermes bytecode, which this engine cannot execute. In a React "
940
+ "Native app that usually means hermesEnabled is still true in "
941
+ "android/gradle.properties, or :hermes_enabled in the Podfile. Turn it "
942
+ "off so Metro ships plain JavaScript, or precompile with qjsc.");
943
+ }
944
+
945
+ /**
946
+ * The name a script should be known by: its `//# sourceURL=` comment if it has
947
+ * one, otherwise the URL the embedder passed.
948
+ *
949
+ * Every other engine renames a script by that comment -- it is what
950
+ * `error.stack` shows and what `Debugger.setBreakpointByUrl` matches against.
951
+ * quickjs ignores it and records whatever filename JS_Eval was handed, so a
952
+ * script carrying the comment would be known by two names and a breakpoint set
953
+ * on it would never fire.
954
+ *
955
+ * Only the last 8 KiB are scanned, which makes this O(1) in bundle size rather
956
+ * than a full pass over a multi-megabyte bundle on the startup path. Tools
957
+ * append the comment at the end; one placed earlier is not honoured.
958
+ */
959
+ constexpr size_t kSourceURLScanBytes = 8192;
960
+
961
+ std::string effectiveSourceURL(
962
+ const uint8_t *data, size_t size, const std::string &sourceURL) {
963
+ const size_t start =
964
+ size > kSourceURLScanBytes ? size - kSourceURLScanBytes : 0;
965
+ const std::string_view tail(
966
+ reinterpret_cast<const char *>(data) + start, size - start);
967
+
968
+ size_t best = std::string_view::npos;
969
+ for (std::string_view form : {"//# sourceURL=", "//@ sourceURL="}) {
970
+ const size_t pos = tail.rfind(form);
971
+ if (pos != std::string_view::npos &&
972
+ (best == std::string_view::npos || pos > best)) {
973
+ best = pos + form.size();
974
+ }
975
+ }
976
+ if (best == std::string_view::npos) {
977
+ return sourceURL;
978
+ }
979
+
980
+ const size_t end = tail.find_first_of("\r\n", best);
981
+ const std::string_view value = tail.substr(
982
+ best,
983
+ end == std::string_view::npos ? std::string_view::npos : end - best);
984
+ const size_t first = value.find_first_not_of(" \t");
985
+ if (first == std::string_view::npos) {
986
+ return sourceURL;
987
+ }
988
+ const size_t last = value.find_last_not_of(" \t");
989
+ return std::string(value.substr(first, last - first + 1));
990
+ }
991
+
992
+ /// Raises the collection threshold for the first top-level script and hands
993
+ /// pacing back at the rung the engine would otherwise be on. Bounded rather
994
+ /// than a disable, so a larger bundle degrades to normal pacing by itself. See
995
+ /// QuickJSRuntimeConfig::startupGCBracketBytes.
996
+ class StartupGCBracket final {
997
+ public:
998
+ StartupGCBracket(JSRuntime *rt, size_t bytes, bool &used) {
999
+ if (rt == nullptr || bytes == 0 || used) {
1000
+ return;
1001
+ }
1002
+ used = true;
1003
+ rt_ = rt;
1004
+ JS_SetGCThreshold(rt_, JS_GetMallocSize(rt_) + bytes);
1005
+ }
1006
+
1007
+ ~StartupGCBracket() {
1008
+ if (rt_ == nullptr) {
1009
+ return;
1010
+ }
1011
+ const size_t live = JS_GetMallocSize(rt_);
1012
+ JS_SetGCThreshold(rt_, live + (live >> 1));
1013
+ }
1014
+
1015
+ StartupGCBracket(const StartupGCBracket &) = delete;
1016
+ StartupGCBracket &operator=(const StartupGCBracket &) = delete;
1017
+
1018
+ private:
1019
+ JSRuntime *rt_{nullptr};
1020
+ };
1021
+
1022
+ /// Compiled form of a script: a JS_WriteObject payload, whether it arrived as
1023
+ /// source or as a container.
1024
+ class QuickJSPreparedJavaScript final : public jsi::PreparedJavaScript {
1025
+ public:
1026
+ explicit QuickJSPreparedJavaScript(std::vector<uint8_t> bytecode)
1027
+ : bytecode_(std::move(bytecode)) {}
1028
+
1029
+ const std::vector<uint8_t> &bytecode() const noexcept {
1030
+ return bytecode_;
1031
+ }
1032
+
1033
+ private:
1034
+ std::vector<uint8_t> bytecode_;
1035
+ };
1036
+
1037
+ } // namespace
1038
+
1039
+ jsi::Value QuickJSRuntime::evaluateSource(
1040
+ const uint8_t *source, size_t size, const std::string &sourceURL) {
1041
+ // JS_Eval wants a NUL-terminated buffer, which jsi::Buffer does not promise.
1042
+ const std::string owned(reinterpret_cast<const char *>(source), size);
1043
+ return createValue(JS_Eval(
1044
+ context_, owned.c_str(), owned.size(), sourceURL.c_str(),
1045
+ JS_EVAL_TYPE_GLOBAL));
1046
+ }
1047
+
1048
+ jsi::Value QuickJSRuntime::evaluateBytecode(
1049
+ const uint8_t *payload, size_t size) {
1050
+ JSValue function =
1051
+ JS_ReadObject(context_, payload, size, JS_READ_OBJ_BYTECODE);
1052
+ if (JS_IsException(function)) {
1053
+ // Almost always an engine mismatch. quickjs bytecode loads only in the
1054
+ // build that wrote it, and "bytecode function expected" does not say so.
1055
+ JS_FreeValue(context_, JS_GetException(context_));
1056
+ throw jsi::JSINativeException(
1057
+ "QuickJSRuntime: could not load bytecode. It was most likely compiled "
1058
+ "by a different build of the engine -- recompile it with the qjsc "
1059
+ "from this checkout.");
1060
+ }
1061
+ return createValue(JS_EvalFunction(context_, function));
1062
+ }
1063
+
1064
+ jsi::Value QuickJSRuntime::evaluateJavaScript(
1065
+ const std::shared_ptr<const jsi::Buffer> &buffer,
1066
+ const std::string &sourceURL) {
1067
+ adoptCurrentThread();
1068
+ drainPendingReleases();
1069
+
1070
+ const uint8_t *data = buffer->data();
1071
+ const size_t size = buffer->size();
1072
+
1073
+ StartupGCBracket bracket(
1074
+ runtime_, config_.startupGCBracketBytes, startupGCBracketUsed_);
1075
+
1076
+ jsi::Value result;
1077
+ if (isBytecodeContainer(data, size)) {
1078
+ result = evaluateBytecode(
1079
+ data + kBytecodeHeaderSize, size - kBytecodeHeaderSize);
1080
+ } else {
1081
+ throwIfHermesBytecode(data, size, sourceURL);
1082
+ const std::string url = effectiveSourceURL(data, size, sourceURL);
1083
+ // Before evaluating, not after: a script is parsed before it runs, and a
1084
+ // stack captured while it is running has to be able to name it.
1085
+ if (onScriptEvaluated_) {
1086
+ onScriptEvaluated_(
1087
+ url, std::string(reinterpret_cast<const char *>(data), size));
1088
+ }
1089
+ result = evaluateSource(data, size, url);
1090
+ }
1091
+
1092
+ rebaselineHeap();
1093
+ return result;
1094
+ }
1095
+
1096
+ std::shared_ptr<const jsi::PreparedJavaScript>
1097
+ QuickJSRuntime::prepareJavaScript(
1098
+ const std::shared_ptr<const jsi::Buffer> &buffer, std::string sourceURL) {
1099
+ // Compiling is an engine entry like any other, and it recurses: the parser
1100
+ // descends once per nesting level. Without this the recursion bound is still
1101
+ // keyed to whichever thread built the runtime, and a script prepared on the
1102
+ // JS thread before anything has evaluated runs off the end of its stack.
1103
+ adoptCurrentThread();
1104
+ drainPendingReleases();
1105
+
1106
+ const uint8_t *data = buffer->data();
1107
+ const size_t size = buffer->size();
1108
+
1109
+ if (isBytecodeContainer(data, size)) {
1110
+ return std::make_shared<const QuickJSPreparedJavaScript>(
1111
+ std::vector<uint8_t>(data + kBytecodeHeaderSize, data + size));
1112
+ }
1113
+ throwIfHermesBytecode(data, size, sourceURL);
1114
+
1115
+ // The name compiled in here is the only one that survives into the function
1116
+ // bytecode, so it has to be the name the debugger will look the script up by.
1117
+ sourceURL = effectiveSourceURL(data, size, sourceURL);
1118
+
1119
+ const std::string owned(reinterpret_cast<const char *>(data), size);
1120
+ JSValue compiled = checkException(JS_Eval(
1121
+ context_, owned.c_str(), owned.size(), sourceURL.c_str(),
1122
+ JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_COMPILE_ONLY));
1123
+
1124
+ size_t length = 0;
1125
+ uint8_t *bytes =
1126
+ JS_WriteObject(context_, &length, compiled, JS_WRITE_OBJ_BYTECODE);
1127
+ JS_FreeValue(context_, compiled);
1128
+ if (bytes == nullptr) {
1129
+ throwPendingError();
1130
+ }
1131
+
1132
+ std::vector<uint8_t> bytecode(bytes, bytes + length);
1133
+ js_free(context_, bytes);
1134
+ return std::make_shared<const QuickJSPreparedJavaScript>(std::move(bytecode));
1135
+ }
1136
+
1137
+ jsi::Value QuickJSRuntime::evaluatePreparedJavaScript(
1138
+ const std::shared_ptr<const jsi::PreparedJavaScript> &js) {
1139
+ adoptCurrentThread();
1140
+ drainPendingReleases();
1141
+
1142
+ auto prepared =
1143
+ std::dynamic_pointer_cast<const QuickJSPreparedJavaScript>(js);
1144
+ if (!prepared) {
1145
+ throw jsi::JSINativeException(
1146
+ "QuickJSRuntime: PreparedJavaScript was created by a different runtime "
1147
+ "implementation");
1148
+ }
1149
+
1150
+ StartupGCBracket bracket(
1151
+ runtime_, config_.startupGCBracketBytes, startupGCBracketUsed_);
1152
+
1153
+ jsi::Value result = evaluateBytecode(
1154
+ prepared->bytecode().data(), prepared->bytecode().size());
1155
+ rebaselineHeap();
1156
+ return result;
1157
+ }
1158
+
1159
+ void QuickJSRuntime::queueMicrotask(const jsi::Function &callback) {
1160
+ JSValue function = toJSValue(callback);
1161
+ checkException(JS_EnqueueJob(context_, microtaskJob, 1, &function));
1162
+ }
1163
+
1164
+ bool QuickJSRuntime::drainMicrotasks(int maxMicrotasksHint) {
1165
+ const auto taskStart = std::chrono::steady_clock::now();
1166
+ adoptCurrentThread();
1167
+ drainPendingReleases();
1168
+
1169
+ int executed = 0;
1170
+ while (maxMicrotasksHint < 0 || executed < maxMicrotasksHint) {
1171
+ JSContext *jobContext = nullptr;
1172
+ int result = JS_ExecutePendingJob(runtime_, &jobContext);
1173
+ if (result == 0) {
1174
+ // Queue drained, so the JS stack is empty: a safepoint. Every safepoint
1175
+ // ends a task, whether or not it collected.
1176
+ runPendingGCIfIdle(taskStart);
1177
+ lastTaskEnd_ = std::chrono::steady_clock::now();
1178
+ return true;
1179
+ }
1180
+ if (result < 0) {
1181
+ // JSI discards the exceptional job and keeps going; the queue is only
1182
+ // reported as not drained if work remains.
1183
+ JSContext *ctx = jobContext != nullptr ? jobContext : context_;
1184
+ JS_FreeValue(ctx, JS_GetException(ctx));
1185
+ }
1186
+ ++executed;
1187
+ }
1188
+ return !JS_IsJobPending(runtime_);
1189
+ }
1190
+
1191
+ void QuickJSRuntime::runPendingGC() noexcept {
1192
+ // Unconditional: the embedder is asserting the app is idle. Safe only with an
1193
+ // empty JS stack, never from inside a host function.
1194
+ if (runtime_ == nullptr) {
1195
+ return;
1196
+ }
1197
+ JS_RunPendingGC(runtime_);
1198
+ noteCollection();
1199
+ lastTaskEnd_ = std::chrono::steady_clock::now();
1200
+ }
1201
+
1202
+ /// The live set just changed, so the growth baseline and the ceiling derived
1203
+ /// from it are stale.
1204
+ void QuickJSRuntime::rebaselineHeap() noexcept {
1205
+ heapBaseline_ = JS_GetMallocSize(runtime_);
1206
+ refreshDeferredGCLimit();
1207
+ }
1208
+
1209
+ /// A collection ran, so the clock the max-deferral valve measures from restarts
1210
+ /// too. Only a collection may reset it: a script that merely rebaselines the
1211
+ /// heap would restart the wait without ending it, and an app that keeps
1212
+ /// evaluating would defer a pending collection forever.
1213
+ void QuickJSRuntime::noteCollection() noexcept {
1214
+ rebaselineHeap();
1215
+ pendingSince_ = {};
1216
+ }
1217
+
1218
+ size_t QuickJSRuntime::deferredGCSlack() const noexcept {
1219
+ size_t slack = JS_GetMallocSize(runtime_);
1220
+ if (slack < config_.deferredGCMinSlack) {
1221
+ slack = config_.deferredGCMinSlack;
1222
+ }
1223
+ if (slack > config_.deferredGCMaxSlack) {
1224
+ slack = config_.deferredGCMaxSlack;
1225
+ }
1226
+ return slack;
1227
+ }
1228
+
1229
+ size_t QuickJSRuntime::deferredGCCeiling() const noexcept {
1230
+ if (config_.deferredGCLimit != 0) {
1231
+ return config_.deferredGCLimit;
1232
+ }
1233
+ return JS_GetMallocSize(runtime_) + deferredGCSlack();
1234
+ }
1235
+
1236
+ void QuickJSRuntime::refreshDeferredGCLimit() noexcept {
1237
+ if (runtime_ == nullptr || !config_.deferGC) {
1238
+ return;
1239
+ }
1240
+ if (config_.deferredGCLimit != 0) {
1241
+ return; // an explicit ceiling belongs to the embedder
1242
+ }
1243
+ JS_SetGCDeferred(runtime_, true, deferredGCCeiling());
1244
+ }
1245
+
1246
+ void QuickJSRuntime::runPendingGCIfIdle(
1247
+ std::chrono::steady_clock::time_point taskStart) noexcept {
1248
+ if (runtime_ == nullptr) {
1249
+ return;
1250
+ }
1251
+
1252
+ // Before the pending check on purpose: the case this fixes is where nothing
1253
+ // is pending because the trigger has drifted out of reach of the live heap.
1254
+ if (config_.gcThresholdRetuneRatio > 0.0) {
1255
+ const size_t liveNow = JS_GetMallocSize(runtime_);
1256
+ const size_t threshold = JS_GetGCThreshold(runtime_);
1257
+ const double drift = liveNow > 0 ? static_cast<double>(threshold) /
1258
+ static_cast<double>(liveNow)
1259
+ : 0.0;
1260
+ if (liveNow > 0 && drift > config_.gcThresholdRetuneRatio) {
1261
+ JS_SetGCThreshold(runtime_, liveNow + (liveNow >> 1));
1262
+ }
1263
+ }
1264
+
1265
+ if (!JS_HasPendingGC(runtime_)) {
1266
+ return;
1267
+ }
1268
+
1269
+ // Measured against the end of the previous task, not the last collection:
1270
+ // timing from the collection makes any long stretch of work look idle.
1271
+ const auto gap = std::chrono::duration_cast<std::chrono::milliseconds>(
1272
+ taskStart - lastTaskEnd_)
1273
+ .count();
1274
+ const bool idle = lastTaskEnd_.time_since_epoch().count() == 0 ||
1275
+ gap >= static_cast<long long>(config_.gcIdleGapMs);
1276
+
1277
+ if (pendingSince_.time_since_epoch().count() == 0) {
1278
+ pendingSince_ = taskStart;
1279
+ }
1280
+ bool stale = false;
1281
+ if (!idle && config_.gcMaxDeferralMs > 0) {
1282
+ const auto waited = std::chrono::duration_cast<std::chrono::milliseconds>(
1283
+ taskStart - pendingSince_)
1284
+ .count();
1285
+ stale = waited >= static_cast<long long>(config_.gcMaxDeferralMs);
1286
+ }
1287
+
1288
+ // Pressure is growth since the last collection, not size. Comparing live size
1289
+ // against half the ceiling reduces to `live > slack`, which is true on every
1290
+ // safepoint for any real app, turning the valve into "collect constantly".
1291
+ const size_t live = JS_GetMallocSize(runtime_);
1292
+ const size_t grown = live > heapBaseline_ ? live - heapBaseline_ : 0;
1293
+ size_t pressureLimit = config_.gcPressureBytes;
1294
+ if (pressureLimit == 0) {
1295
+ // Also capped by the live heap, or the valve is unreachable on an app whose
1296
+ // whole heap is smaller than the slack -- measured as a collection pending
1297
+ // forever on an app with a ticker, which never reaches an idle gap either.
1298
+ const size_t bySlack = deferredGCSlack() / 2;
1299
+ const size_t byHeap = live / 2;
1300
+ pressureLimit = bySlack < byHeap ? bySlack : byHeap;
1301
+ }
1302
+ if (!idle && !stale && grown < pressureLimit) {
1303
+ return;
1304
+ }
1305
+
1306
+ JS_RunPendingGC(runtime_);
1307
+ noteCollection();
1308
+ }
1309
+
1310
+ jsi::Object QuickJSRuntime::global() {
1311
+ return createObjectFrom(JS_GetGlobalObject(context_));
1312
+ }
1313
+
1314
+ jsi::Runtime::PointerValue *QuickJSRuntime::cloneSymbol(
1315
+ const Runtime::PointerValue *pv) {
1316
+ const auto *value = static_cast<const QuickJSPointerValue *>(pv);
1317
+ return allocPointerValue(JS_DupValue(context_, value->value()));
1318
+ }
1319
+
1320
+ jsi::Runtime::PointerValue *QuickJSRuntime::cloneBigInt(
1321
+ const Runtime::PointerValue *pv) {
1322
+ const auto *value = static_cast<const QuickJSPointerValue *>(pv);
1323
+ return allocPointerValue(JS_DupValue(context_, value->value()));
1324
+ }
1325
+
1326
+ jsi::Runtime::PointerValue *QuickJSRuntime::cloneString(
1327
+ const Runtime::PointerValue *pv) {
1328
+ const auto *value = static_cast<const QuickJSPointerValue *>(pv);
1329
+ return allocPointerValue(JS_DupValue(context_, value->value()));
1330
+ }
1331
+
1332
+ jsi::Runtime::PointerValue *QuickJSRuntime::cloneObject(
1333
+ const Runtime::PointerValue *pv) {
1334
+ const auto *value = static_cast<const QuickJSPointerValue *>(pv);
1335
+ return allocPointerValue(JS_DupValue(context_, value->value()));
1336
+ }
1337
+
1338
+ jsi::Runtime::PointerValue *QuickJSRuntime::clonePropNameID(
1339
+ const Runtime::PointerValue *pv) {
1340
+ const auto *value = static_cast<const QuickJSAtomPointerValue *>(pv);
1341
+ return allocAtomPointerValue(JS_DupAtom(context_, value->atom()));
1342
+ }
1343
+
1344
+ jsi::PropNameID QuickJSRuntime::createPropNameIDFromAscii(
1345
+ const char *str, size_t length) {
1346
+ return createPropNameIDFrom(JS_NewAtomLen(context_, str, length));
1347
+ }
1348
+
1349
+ jsi::PropNameID QuickJSRuntime::createPropNameIDFromUtf8(
1350
+ const uint8_t *utf8, size_t length) {
1351
+ return createPropNameIDFrom(
1352
+ JS_NewAtomLen(context_, reinterpret_cast<const char *>(utf8), length));
1353
+ }
1354
+
1355
+ jsi::PropNameID QuickJSRuntime::createPropNameIDFromUtf16(
1356
+ const char16_t *utf16, size_t length) {
1357
+ JSValue string = JS_NewStringUTF16(
1358
+ context_, reinterpret_cast<const uint16_t *>(utf16), length);
1359
+ checkException(string);
1360
+ JSAtom atom = JS_ValueToAtom(context_, string);
1361
+ JS_FreeValue(context_, string);
1362
+ return createPropNameIDFrom(atom);
1363
+ }
1364
+
1365
+ jsi::PropNameID QuickJSRuntime::createPropNameIDFromString(
1366
+ const jsi::String &str) {
1367
+ return createPropNameIDFrom(JS_ValueToAtom(context_, toJSValue(str)));
1368
+ }
1369
+
1370
+ jsi::PropNameID QuickJSRuntime::createPropNameIDFromSymbol(
1371
+ const jsi::Symbol &sym) {
1372
+ return createPropNameIDFrom(JS_ValueToAtom(context_, toJSValue(sym)));
1373
+ }
1374
+
1375
+ std::string QuickJSRuntime::utf8(const jsi::PropNameID &name) {
1376
+ size_t length = 0;
1377
+ const char *chars = JS_AtomToCStringLen(context_, &length, toJSAtom(name));
1378
+ if (chars == nullptr) {
1379
+ throwPendingError();
1380
+ }
1381
+ std::string result(chars, length);
1382
+ JS_FreeCString(context_, chars);
1383
+ return result;
1384
+ }
1385
+
1386
+ bool QuickJSRuntime::compare(
1387
+ const jsi::PropNameID &a, const jsi::PropNameID &b) {
1388
+ return toJSAtom(a) == toJSAtom(b);
1389
+ }
1390
+
1391
+ std::string QuickJSRuntime::symbolToString(const jsi::Symbol &symbol) {
1392
+ JSValue argument = toJSValue(symbol);
1393
+ JSValue result =
1394
+ JS_Call(context_, symbolToString_, JS_UNDEFINED, 1, &argument);
1395
+ checkException(result);
1396
+
1397
+ size_t length = 0;
1398
+ const char *chars = JS_ToCStringLen(context_, &length, result);
1399
+ JS_FreeValue(context_, result);
1400
+ if (chars == nullptr) {
1401
+ throwPendingError();
1402
+ }
1403
+ std::string string(chars, length);
1404
+ JS_FreeCString(context_, chars);
1405
+ return string;
1406
+ }
1407
+
1408
+ jsi::BigInt QuickJSRuntime::createBigIntFromInt64(int64_t value) {
1409
+ JSValue bigint = JS_NewBigInt64(context_, value);
1410
+ checkException(bigint);
1411
+ return make<jsi::BigInt>(allocPointerValue(bigint));
1412
+ }
1413
+
1414
+ jsi::BigInt QuickJSRuntime::createBigIntFromUint64(uint64_t value) {
1415
+ JSValue bigint = JS_NewBigUint64(context_, value);
1416
+ checkException(bigint);
1417
+ return make<jsi::BigInt>(allocPointerValue(bigint));
1418
+ }
1419
+
1420
+ bool QuickJSRuntime::bigintIsInt64(const jsi::BigInt &bigint) {
1421
+ int64_t truncated = 0;
1422
+ if (JS_ToBigInt64(context_, &truncated, toJSValue(bigint)) != 0) {
1423
+ JS_FreeValue(context_, JS_GetException(context_));
1424
+ return false;
1425
+ }
1426
+ // JS_ToBigInt64 wraps rather than failing, so round-trip to detect loss.
1427
+ JSValue roundTrip = JS_NewBigInt64(context_, truncated);
1428
+ bool fits = JS_IsStrictEqual(context_, roundTrip, toJSValue(bigint));
1429
+ JS_FreeValue(context_, roundTrip);
1430
+ return fits;
1431
+ }
1432
+
1433
+ bool QuickJSRuntime::bigintIsUint64(const jsi::BigInt &bigint) {
1434
+ uint64_t truncated = 0;
1435
+ if (JS_ToBigUint64(context_, &truncated, toJSValue(bigint)) != 0) {
1436
+ JS_FreeValue(context_, JS_GetException(context_));
1437
+ return false;
1438
+ }
1439
+ JSValue roundTrip = JS_NewBigUint64(context_, truncated);
1440
+ bool fits = JS_IsStrictEqual(context_, roundTrip, toJSValue(bigint));
1441
+ JS_FreeValue(context_, roundTrip);
1442
+ return fits;
1443
+ }
1444
+
1445
+ uint64_t QuickJSRuntime::truncate(const jsi::BigInt &bigint) {
1446
+ uint64_t truncated = 0;
1447
+ if (JS_ToBigUint64(context_, &truncated, toJSValue(bigint)) != 0) {
1448
+ throwPendingError();
1449
+ }
1450
+ return truncated;
1451
+ }
1452
+
1453
+ jsi::String QuickJSRuntime::bigintToString(
1454
+ const jsi::BigInt &bigint, int radix) {
1455
+ if (radix < 2 || radix > 36) {
1456
+ throw jsi::JSINativeException(
1457
+ "QuickJSRuntime: radix must be between 2 and 36");
1458
+ }
1459
+ JSValue arguments[2] = {toJSValue(bigint), JS_NewInt32(context_, radix)};
1460
+ JSValue result =
1461
+ JS_Call(context_, bigIntToString_, JS_UNDEFINED, 2, arguments);
1462
+ checkException(result);
1463
+ return createStringFrom(result);
1464
+ }
1465
+
1466
+ jsi::String QuickJSRuntime::createStringFromAscii(
1467
+ const char *str, size_t length) {
1468
+ return createStringFromUtf8(reinterpret_cast<const uint8_t *>(str), length);
1469
+ }
1470
+
1471
+ jsi::String QuickJSRuntime::createStringFromUtf8(
1472
+ const uint8_t *utf8, size_t length) {
1473
+ JSValue string =
1474
+ JS_NewStringLen(context_, reinterpret_cast<const char *>(utf8), length);
1475
+ checkException(string);
1476
+ return createStringFrom(string);
1477
+ }
1478
+
1479
+ jsi::String QuickJSRuntime::createStringFromUtf16(
1480
+ const char16_t *utf16, size_t length) {
1481
+ JSValue string = JS_NewStringUTF16(
1482
+ context_, reinterpret_cast<const uint16_t *>(utf16), length);
1483
+ checkException(string);
1484
+ return createStringFrom(string);
1485
+ }
1486
+
1487
+ std::string QuickJSRuntime::utf8(const jsi::String &string) {
1488
+ size_t length = 0;
1489
+ const char *chars = JS_ToCStringLen(context_, &length, toJSValue(string));
1490
+ if (chars == nullptr) {
1491
+ throwPendingError();
1492
+ }
1493
+ std::string result(chars, length);
1494
+ JS_FreeCString(context_, chars);
1495
+ return result;
1496
+ }
1497
+
1498
+ jsi::Object QuickJSRuntime::createObject() {
1499
+ return createObjectFrom(checkException(JS_NewObject(context_)));
1500
+ }
1501
+
1502
+ jsi::Object QuickJSRuntime::createObject(std::shared_ptr<jsi::HostObject> ho) {
1503
+ JSValue object = JS_NewObjectClass(context_, hostObjectClassID_);
1504
+ checkException(object);
1505
+ auto *proxy = new HostObjectProxy{{}, this, std::move(ho)};
1506
+ registerHostObject(proxy);
1507
+ JS_SetOpaque(object, proxy);
1508
+ return createObjectFrom(object);
1509
+ }
1510
+
1511
+ std::shared_ptr<jsi::HostObject> QuickJSRuntime::getHostObject(
1512
+ const jsi::Object &object) {
1513
+ auto *proxy = static_cast<HostObjectProxy *>(
1514
+ JS_GetOpaque(toJSValue(object), hostObjectClassID_));
1515
+ if (proxy == nullptr) {
1516
+ throw jsi::JSINativeException("QuickJSRuntime: not a host object");
1517
+ }
1518
+ return proxy->hostObject;
1519
+ }
1520
+
1521
+ jsi::HostFunctionType &QuickJSRuntime::getHostFunction(
1522
+ const jsi::Function &function) {
1523
+ auto *proxy = static_cast<HostFunctionProxy *>(
1524
+ JS_GetOpaque(toJSValue(function), hostFunctionClassID_));
1525
+ if (proxy == nullptr) {
1526
+ throw jsi::JSINativeException("QuickJSRuntime: not a host function");
1527
+ }
1528
+ return proxy->function;
1529
+ }
1530
+
1531
+ jsi::Object QuickJSRuntime::createObjectWithPrototype(
1532
+ const jsi::Value &prototype) {
1533
+ if (!prototype.isObject() && !prototype.isNull()) {
1534
+ throw jsi::JSINativeException(
1535
+ "QuickJSRuntime: prototype must be an object or null");
1536
+ }
1537
+ return createObjectFrom(
1538
+ checkException(JS_NewObjectProto(context_, toJSValue(prototype))));
1539
+ }
1540
+
1541
+ void QuickJSRuntime::setPrototypeOf(
1542
+ const jsi::Object &object, const jsi::Value &prototype) {
1543
+ if (!prototype.isObject() && !prototype.isNull()) {
1544
+ throw jsi::JSINativeException(
1545
+ "QuickJSRuntime: prototype must be an object or null");
1546
+ }
1547
+ checkException(
1548
+ JS_SetPrototype(context_, toJSValue(object), toJSValue(prototype)));
1549
+ }
1550
+
1551
+ jsi::Value QuickJSRuntime::getPrototypeOf(const jsi::Object &object) {
1552
+ return createValue(
1553
+ checkException(JS_GetPrototype(context_, toJSValue(object))));
1554
+ }
1555
+
1556
+ /// JSI reserves native state for ordinary objects: a proxy would route the
1557
+ /// property through its traps, and a host object answers for every key.
1558
+ void QuickJSRuntime::checkCanHoldNativeState(const jsi::Object &object) {
1559
+ JSValue value = toJSValue(object);
1560
+ if (JS_IsProxy(value)) {
1561
+ throw jsi::JSINativeException(
1562
+ "QuickJSRuntime: cannot set native state on a proxy");
1563
+ }
1564
+ if (JS_GetOpaque(value, hostObjectClassID_) != nullptr) {
1565
+ throw jsi::JSINativeException(
1566
+ "QuickJSRuntime: cannot set native state on a host object");
1567
+ }
1568
+ }
1569
+
1570
+ bool QuickJSRuntime::hasNativeState(const jsi::Object &object) {
1571
+ JSValue holder = JS_GetProperty(context_, toJSValue(object), nativeStateKey_);
1572
+ if (JS_IsException(holder)) {
1573
+ JS_FreeValue(context_, JS_GetException(context_));
1574
+ return false;
1575
+ }
1576
+ const bool present = JS_GetOpaque(holder, nativeStateClassID_) != nullptr;
1577
+ JS_FreeValue(context_, holder);
1578
+ return present;
1579
+ }
1580
+
1581
+ std::shared_ptr<jsi::NativeState> QuickJSRuntime::getNativeState(
1582
+ const jsi::Object &object) {
1583
+ JSValue holder = checkException(
1584
+ JS_GetProperty(context_, toJSValue(object), nativeStateKey_));
1585
+ auto *proxy = static_cast<NativeStateProxy *>(
1586
+ JS_GetOpaque(holder, nativeStateClassID_));
1587
+ JS_FreeValue(context_, holder);
1588
+ if (proxy == nullptr) {
1589
+ throw jsi::JSINativeException("QuickJSRuntime: object has no native state");
1590
+ }
1591
+ // May be null: JSI allows clearing the state, after which hasNativeState
1592
+ // still reports true.
1593
+ return proxy->state;
1594
+ }
1595
+
1596
+ void QuickJSRuntime::setNativeState(
1597
+ const jsi::Object &object, std::shared_ptr<jsi::NativeState> state) {
1598
+ checkCanHoldNativeState(object);
1599
+
1600
+ // Overwriting is a pointer swap: the holder is reachable only from this
1601
+ // property, so nothing else can be looking at it.
1602
+ JSValue existing = checkException(
1603
+ JS_GetProperty(context_, toJSValue(object), nativeStateKey_));
1604
+ auto *held = static_cast<NativeStateProxy *>(
1605
+ JS_GetOpaque(existing, nativeStateClassID_));
1606
+ JS_FreeValue(context_, existing);
1607
+ if (held != nullptr) {
1608
+ held->state = std::move(state);
1609
+ return;
1610
+ }
1611
+
1612
+ JSValue holder = JS_NewObjectClass(context_, nativeStateClassID_);
1613
+ checkException(holder);
1614
+ auto *proxy = new NativeStateProxy{{}, std::move(state)};
1615
+ registerNativeState(proxy);
1616
+ JS_SetOpaque(holder, proxy);
1617
+
1618
+ // Writable and configurable so a later call can replace it, which JSI
1619
+ // requires. Neither weakens anything: script has no way to name the key.
1620
+ checkException(JS_DefinePropertyValue(
1621
+ context_, toJSValue(object), JS_DupAtom(context_, nativeStateKey_),
1622
+ holder, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE));
1623
+ }
1624
+
1625
+ jsi::Value QuickJSRuntime::getProperty(
1626
+ const jsi::Object &object, const jsi::PropNameID &name) {
1627
+ return createValue(
1628
+ JS_GetProperty(context_, toJSValue(object), toJSAtom(name)));
1629
+ }
1630
+
1631
+ jsi::Value QuickJSRuntime::getProperty(
1632
+ const jsi::Object &object, const jsi::String &name) {
1633
+ JSAtom atom = JS_ValueToAtom(context_, toJSValue(name));
1634
+ JSValue value = JS_GetProperty(context_, toJSValue(object), atom);
1635
+ JS_FreeAtom(context_, atom);
1636
+ return createValue(value);
1637
+ }
1638
+
1639
+ bool QuickJSRuntime::hasProperty(
1640
+ const jsi::Object &object, const jsi::PropNameID &name) {
1641
+ int result = JS_HasProperty(context_, toJSValue(object), toJSAtom(name));
1642
+ checkException(result);
1643
+ return result != 0;
1644
+ }
1645
+
1646
+ bool QuickJSRuntime::hasProperty(
1647
+ const jsi::Object &object, const jsi::String &name) {
1648
+ JSAtom atom = JS_ValueToAtom(context_, toJSValue(name));
1649
+ int result = JS_HasProperty(context_, toJSValue(object), atom);
1650
+ JS_FreeAtom(context_, atom);
1651
+ checkException(result);
1652
+ return result != 0;
1653
+ }
1654
+
1655
+ void QuickJSRuntime::setPropertyValue(
1656
+ const jsi::Object &object, const jsi::PropNameID &name,
1657
+ const jsi::Value &value) {
1658
+ checkException(JS_SetProperty(
1659
+ context_, toJSValue(object), toJSAtom(name),
1660
+ JS_DupValue(context_, toJSValue(value))));
1661
+ }
1662
+
1663
+ void QuickJSRuntime::setPropertyValue(
1664
+ const jsi::Object &object, const jsi::String &name,
1665
+ const jsi::Value &value) {
1666
+ JSAtom atom = JS_ValueToAtom(context_, toJSValue(name));
1667
+ int result = JS_SetProperty(
1668
+ context_, toJSValue(object), atom,
1669
+ JS_DupValue(context_, toJSValue(value)));
1670
+ JS_FreeAtom(context_, atom);
1671
+ checkException(result);
1672
+ }
1673
+
1674
+ bool QuickJSRuntime::isArray(const jsi::Object &object) const {
1675
+ return JS_IsArray(toJSValue(object));
1676
+ }
1677
+
1678
+ bool QuickJSRuntime::isArrayBuffer(const jsi::Object &object) const {
1679
+ return JS_IsArrayBuffer(toJSValue(object));
1680
+ }
1681
+
1682
+ bool QuickJSRuntime::isFunction(const jsi::Object &object) const {
1683
+ return JS_IsFunction(context_, toJSValue(object));
1684
+ }
1685
+
1686
+ bool QuickJSRuntime::isHostObject(const jsi::Object &object) const {
1687
+ return JS_GetOpaque(toJSValue(object), hostObjectClassID_) != nullptr;
1688
+ }
1689
+
1690
+ bool QuickJSRuntime::isHostFunction(const jsi::Function &function) const {
1691
+ return JS_GetOpaque(toJSValue(function), hostFunctionClassID_) != nullptr;
1692
+ }
1693
+
1694
+ jsi::Array QuickJSRuntime::getPropertyNames(const jsi::Object &object) {
1695
+ // Own and inherited enumerable string keys is exactly for-in, which has no
1696
+ // C API.
1697
+ JSValue argument = toJSValue(object);
1698
+ JSValue names =
1699
+ JS_Call(context_, enumeratePropertyNames_, JS_UNDEFINED, 1, &argument);
1700
+ checkException(names);
1701
+ return make<jsi::Object>(allocPointerValue(names)).getArray(*this);
1702
+ }
1703
+
1704
+ jsi::WeakObject QuickJSRuntime::createWeakObject(const jsi::Object &object) {
1705
+ if (!JS_IsObject(weakRefConstructor_)) {
1706
+ throw jsi::JSINativeException(
1707
+ "QuickJSRuntime: WeakRef is unavailable in this context");
1708
+ }
1709
+ JSValue argument = toJSValue(object);
1710
+ JSValue weakRef =
1711
+ JS_CallConstructor(context_, weakRefConstructor_, 1, &argument);
1712
+ checkException(weakRef);
1713
+ return make<jsi::WeakObject>(allocPointerValue(weakRef));
1714
+ }
1715
+
1716
+ jsi::Value QuickJSRuntime::lockWeakObject(const jsi::WeakObject &weakObject) {
1717
+ JSValue result =
1718
+ JS_Call(context_, weakRefDeref_, toJSValue(weakObject), 0, nullptr);
1719
+ checkException(result);
1720
+ return createValue(result);
1721
+ }
1722
+
1723
+ jsi::Array QuickJSRuntime::createArray(size_t length) {
1724
+ JSValue array = JS_NewArray(context_);
1725
+ checkException(array);
1726
+ if (JS_SetLength(context_, array, static_cast<int64_t>(length)) < 0) {
1727
+ JS_FreeValue(context_, array);
1728
+ throwPendingError();
1729
+ }
1730
+ return make<jsi::Object>(allocPointerValue(array)).getArray(*this);
1731
+ }
1732
+
1733
+ jsi::ArrayBuffer QuickJSRuntime::createArrayBuffer(
1734
+ std::shared_ptr<jsi::MutableBuffer> buffer) {
1735
+ uint8_t *data = buffer->data();
1736
+ size_t size = buffer->size();
1737
+ auto *proxy = new ArrayBufferProxy{std::move(buffer)};
1738
+
1739
+ JSValue arrayBuffer = JS_NewArrayBuffer(
1740
+ context_, data, size, freeArrayBufferProxy, proxy, false);
1741
+ if (JS_IsException(arrayBuffer)) {
1742
+ delete proxy;
1743
+ throwPendingError();
1744
+ }
1745
+ return make<jsi::Object>(allocPointerValue(arrayBuffer))
1746
+ .getArrayBuffer(*this);
1747
+ }
1748
+
1749
+ size_t QuickJSRuntime::size(const jsi::Array &array) {
1750
+ int64_t length = 0;
1751
+ checkException(JS_GetLength(context_, toJSValue(array), &length));
1752
+ return static_cast<size_t>(length);
1753
+ }
1754
+
1755
+ size_t QuickJSRuntime::size(const jsi::ArrayBuffer &arrayBuffer) {
1756
+ size_t size = 0;
1757
+ if (JS_GetArrayBuffer(context_, &size, toJSValue(arrayBuffer)) == nullptr) {
1758
+ throwPendingError();
1759
+ }
1760
+ return size;
1761
+ }
1762
+
1763
+ uint8_t *QuickJSRuntime::data(const jsi::ArrayBuffer &arrayBuffer) {
1764
+ size_t size = 0;
1765
+ uint8_t *data = JS_GetArrayBuffer(context_, &size, toJSValue(arrayBuffer));
1766
+ if (data == nullptr) {
1767
+ throwPendingError();
1768
+ }
1769
+ return data;
1770
+ }
1771
+
1772
+ jsi::Value QuickJSRuntime::getValueAtIndex(
1773
+ const jsi::Array &array, size_t index) {
1774
+ return createValue(JS_GetPropertyUint32(
1775
+ context_, toJSValue(array), static_cast<uint32_t>(index)));
1776
+ }
1777
+
1778
+ void QuickJSRuntime::setValueAtIndexImpl(
1779
+ const jsi::Array &array, size_t index, const jsi::Value &value) {
1780
+ checkException(JS_SetPropertyUint32(
1781
+ context_, toJSValue(array), static_cast<uint32_t>(index),
1782
+ JS_DupValue(context_, toJSValue(value))));
1783
+ }
1784
+
1785
+ jsi::Function QuickJSRuntime::createFunctionFromHostFunction(
1786
+ const jsi::PropNameID &name, unsigned int paramCount,
1787
+ jsi::HostFunctionType func) {
1788
+ JSValue function = JS_NewObjectClass(context_, hostFunctionClassID_);
1789
+ checkException(function);
1790
+ auto *proxy = new HostFunctionProxy{{}, this, std::move(func)};
1791
+ registerHostFunction(proxy);
1792
+ JS_SetOpaque(function, proxy);
1793
+ JS_SetConstructorBit(context_, function, true);
1794
+
1795
+ // `name` and `length` are ordinary own properties on JS functions, and code
1796
+ // in the wild reads them.
1797
+ JS_DefinePropertyValueStr(
1798
+ context_, function, "length",
1799
+ JS_NewInt32(context_, static_cast<int32_t>(paramCount)),
1800
+ JS_PROP_CONFIGURABLE);
1801
+ JS_DefinePropertyValueStr(
1802
+ context_, function, "name", JS_AtomToString(context_, toJSAtom(name)),
1803
+ JS_PROP_CONFIGURABLE);
1804
+
1805
+ return make<jsi::Object>(allocPointerValue(function)).getFunction(*this);
1806
+ }
1807
+
1808
+ jsi::Value QuickJSRuntime::call(
1809
+ const jsi::Function &function, const jsi::Value &jsThis,
1810
+ const jsi::Value *args, size_t count) {
1811
+ adoptCurrentThread();
1812
+ drainPendingReleases();
1813
+
1814
+ ArgumentList arguments(*this, args, count);
1815
+ return createValue(JS_Call(
1816
+ context_, toJSValue(function), toJSValue(jsThis), static_cast<int>(count),
1817
+ arguments.data()));
1818
+ }
1819
+
1820
+ jsi::Value QuickJSRuntime::callAsConstructor(
1821
+ const jsi::Function &function, const jsi::Value *args, size_t count) {
1822
+ adoptCurrentThread();
1823
+ drainPendingReleases();
1824
+
1825
+ ArgumentList arguments(*this, args, count);
1826
+ return createValue(JS_CallConstructor(
1827
+ context_, toJSValue(function), static_cast<int>(count),
1828
+ arguments.data()));
1829
+ }
1830
+
1831
+ bool QuickJSRuntime::strictEquals(
1832
+ const jsi::Symbol &a, const jsi::Symbol &b) const {
1833
+ return JS_IsStrictEqual(context_, toJSValue(a), toJSValue(b));
1834
+ }
1835
+
1836
+ bool QuickJSRuntime::strictEquals(
1837
+ const jsi::BigInt &a, const jsi::BigInt &b) const {
1838
+ return JS_IsStrictEqual(context_, toJSValue(a), toJSValue(b));
1839
+ }
1840
+
1841
+ bool QuickJSRuntime::strictEquals(
1842
+ const jsi::String &a, const jsi::String &b) const {
1843
+ return JS_IsStrictEqual(context_, toJSValue(a), toJSValue(b));
1844
+ }
1845
+
1846
+ bool QuickJSRuntime::strictEquals(
1847
+ const jsi::Object &a, const jsi::Object &b) const {
1848
+ return JS_IsStrictEqual(context_, toJSValue(a), toJSValue(b));
1849
+ }
1850
+
1851
+ bool QuickJSRuntime::instanceOf(
1852
+ const jsi::Object &object, const jsi::Function &function) {
1853
+ int result =
1854
+ JS_IsInstanceOf(context_, toJSValue(object), toJSValue(function));
1855
+ checkException(result);
1856
+ return result != 0;
1857
+ }
1858
+
1859
+ void QuickJSRuntime::setExternalMemoryPressure(
1860
+ const jsi::Object & /*object*/, size_t /*amount*/) {
1861
+ // quickjs has no external memory pressure hook.
1862
+ }
1863
+
1864
+ } // namespace qjs