@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,529 @@
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
+ #pragma once
9
+
10
+ #include <jsi/jsi.h>
11
+ #include <pthread.h>
12
+ #include <quickjs.h>
13
+
14
+ #include <atomic>
15
+ #include <chrono>
16
+ #include <cstdint>
17
+ #include <functional>
18
+ #include <mutex>
19
+ #include <new>
20
+ #include <string>
21
+ #include <thread>
22
+ #include <unordered_set>
23
+ #include <vector>
24
+
25
+ #include "QuickJSRuntimeConfig.h"
26
+
27
+ namespace qjs {
28
+
29
+ namespace jsi = facebook::jsi;
30
+
31
+ /// A jsi::Runtime backed by quickjs-ng.
32
+ class QuickJSRuntime : public jsi::Runtime {
33
+ public:
34
+ explicit QuickJSRuntime(QuickJSRuntimeConfig config);
35
+ ~QuickJSRuntime() override;
36
+
37
+ QuickJSRuntime(const QuickJSRuntime &) = delete;
38
+ QuickJSRuntime &operator=(const QuickJSRuntime &) = delete;
39
+
40
+ JSRuntime *runtime() const noexcept {
41
+ return runtime_;
42
+ }
43
+ /// Called after each script is evaluated from source, with the URL it was
44
+ /// given and the source itself. The debugger uses it to announce the script
45
+ /// and to bind breakpoints that named its URL. Set after construction,
46
+ /// because whoever installs it needs the context this runtime owns.
47
+ void setScriptEvaluatedHook(
48
+ std::function<void(const std::string &url, const std::string &source)>
49
+ hook) noexcept {
50
+ onScriptEvaluated_ = std::move(hook);
51
+ }
52
+
53
+ JSContext *context() const noexcept {
54
+ return context_;
55
+ }
56
+
57
+ /**
58
+ * Serialises access to the engine so more than one embedder can run
59
+ * JavaScript on different threads. Lock where native enters JavaScript,
60
+ * unlock when it leaves; the JSI methods in between do no locking.
61
+ *
62
+ * All-or-nothing: quickjs locks nothing itself, so once a second thread runs
63
+ * JavaScript every entry point must hold this. A single-threaded embedder
64
+ * never calls it and pays nothing. Dropping a jsi::Value is the exception --
65
+ * an off-thread release is queued rather than blocked.
66
+ */
67
+ void lock() noexcept;
68
+ void unlock() noexcept;
69
+
70
+ class Lock {
71
+ public:
72
+ explicit Lock(QuickJSRuntime &runtime) noexcept : runtime_(runtime) {
73
+ runtime_.lock();
74
+ }
75
+ ~Lock() {
76
+ runtime_.unlock();
77
+ }
78
+ Lock(const Lock &) = delete;
79
+ Lock &operator=(const Lock &) = delete;
80
+
81
+ private:
82
+ QuickJSRuntime &runtime_;
83
+ };
84
+
85
+ bool isOnJSThread() const noexcept {
86
+ return jsThread_.load(std::memory_order_relaxed) ==
87
+ std::this_thread::get_id();
88
+ }
89
+
90
+ /// Makes the calling thread the owner and re-bases the recursion bound onto
91
+ /// its stack. A no-op when it already owns the engine. lock() calls it on
92
+ /// every handover; an embedder that never locks calls it at its first
93
+ /// evaluate, since React Native builds the runtime on one thread and runs
94
+ /// JavaScript on another.
95
+ void adoptCurrentThread() noexcept;
96
+
97
+ static size_t remainingNativeStackBytes() noexcept;
98
+
99
+ /// Holds one reference on a JSValue. invalidate() may run on any thread, so
100
+ /// the release is routed through releaseValue(). Nested because
101
+ /// jsi::Runtime::PointerValue is protected.
102
+ class QuickJSPointerValue final : public PointerValue {
103
+ public:
104
+ QuickJSPointerValue(
105
+ QuickJSRuntime &runtime, JSValue value, bool owned = true)
106
+ : runtime_(runtime), owned_(owned), value_(value) {}
107
+
108
+ void invalidate() noexcept override {
109
+ if (owned_) {
110
+ runtime_.releaseValue(value_);
111
+ }
112
+ runtime_.recyclePointerValue(this);
113
+ }
114
+
115
+ JSValue value() const noexcept {
116
+ return value_;
117
+ }
118
+
119
+ private:
120
+ friend class QuickJSRuntime;
121
+ ~QuickJSPointerValue() override = default;
122
+
123
+ QuickJSRuntime &runtime_;
124
+ bool owned_;
125
+ union {
126
+ JSValue value_;
127
+ QuickJSPointerValue *nextFree_;
128
+ };
129
+ };
130
+
131
+ /// Backing store for jsi::PropNameID. Atoms are interned and comparable by
132
+ /// identity, which is what PropNameID needs.
133
+ class QuickJSAtomPointerValue final : public PointerValue {
134
+ public:
135
+ QuickJSAtomPointerValue(QuickJSRuntime &runtime, JSAtom atom)
136
+ : runtime_(runtime), atom_(atom) {}
137
+
138
+ void invalidate() noexcept override {
139
+ runtime_.releaseAtom(atom_);
140
+ runtime_.recycleAtomPointerValue(this);
141
+ }
142
+
143
+ JSAtom atom() const noexcept {
144
+ return atom_;
145
+ }
146
+
147
+ private:
148
+ friend class QuickJSRuntime;
149
+ ~QuickJSAtomPointerValue() override = default;
150
+
151
+ QuickJSRuntime &runtime_;
152
+ union {
153
+ JSAtom atom_;
154
+ QuickJSAtomPointerValue *nextFree_;
155
+ };
156
+ };
157
+
158
+ /// Safe to call from any thread: an off-thread release is queued and freed on
159
+ /// the JS thread.
160
+ void releaseValue(JSValue value) noexcept;
161
+ void releaseAtom(JSAtom atom) noexcept;
162
+
163
+ /// Registration of the native payloads attached to JS objects, so teardown
164
+ /// can release them without waiting for the collector to reach their owners.
165
+ void registerHostObject(void *proxy);
166
+ void unregisterHostObject(void *proxy);
167
+ void registerHostFunction(void *proxy);
168
+ void unregisterHostFunction(void *proxy);
169
+ void registerNativeState(void *proxy);
170
+ void unregisterNativeState(void *proxy);
171
+
172
+ /// Public because the class callbacks are free functions.
173
+ JSClassID hostObjectClassID() const noexcept {
174
+ return hostObjectClassID_;
175
+ }
176
+ JSClassID hostFunctionClassID() const noexcept {
177
+ return hostFunctionClassID_;
178
+ }
179
+ JSClassID nativeStateClassID() const noexcept {
180
+ return nativeStateClassID_;
181
+ }
182
+
183
+ // jsi::Runtime -- public API
184
+ jsi::Value evaluateJavaScript(
185
+ const std::shared_ptr<const jsi::Buffer> &buffer,
186
+ const std::string &sourceURL) override;
187
+ std::shared_ptr<const jsi::PreparedJavaScript> prepareJavaScript(
188
+ const std::shared_ptr<const jsi::Buffer> &buffer,
189
+ std::string sourceURL) override;
190
+ jsi::Value evaluatePreparedJavaScript(
191
+ const std::shared_ptr<const jsi::PreparedJavaScript> &js) override;
192
+
193
+ void queueMicrotask(const jsi::Function &callback) override;
194
+ bool drainMicrotasks(int maxMicrotasksHint = -1) override;
195
+
196
+ /// Runs a collection deferred by QuickJSRuntimeConfig::deferGC. Call with no
197
+ /// JS on the stack; drainMicrotasks already does when the queue empties.
198
+ void runPendingGC() noexcept;
199
+
200
+ /// Safepoint with an idleness test: collects only if nothing drove JS for
201
+ /// gcIdleGapMs before `taskStart`, if the collection has waited longer than
202
+ /// gcMaxDeferralMs, or if the heap has grown past the pressure limit.
203
+ void runPendingGCIfIdle(
204
+ std::chrono::steady_clock::time_point taskStart) noexcept;
205
+
206
+ /// `live + clamp(live, minSlack, maxSlack)`, re-derived because the engine
207
+ /// computes its own ceiling once, before it has seen the app's heap.
208
+ size_t deferredGCCeiling() const noexcept;
209
+ size_t deferredGCSlack() const noexcept;
210
+ void refreshDeferredGCLimit() noexcept;
211
+
212
+ jsi::Object global() override;
213
+ std::string description() override;
214
+ bool isInspectable() override;
215
+
216
+ protected:
217
+ // jsi::Runtime -- pointer lifetime
218
+ PointerValue *cloneSymbol(const Runtime::PointerValue *pv) override;
219
+ PointerValue *cloneBigInt(const Runtime::PointerValue *pv) override;
220
+ PointerValue *cloneString(const Runtime::PointerValue *pv) override;
221
+ PointerValue *cloneObject(const Runtime::PointerValue *pv) override;
222
+ PointerValue *clonePropNameID(const Runtime::PointerValue *pv) override;
223
+
224
+ // PropNameID
225
+ jsi::PropNameID createPropNameIDFromAscii(
226
+ const char *str, size_t length) override;
227
+ jsi::PropNameID createPropNameIDFromUtf8(
228
+ const uint8_t *utf8, size_t length) override;
229
+ jsi::PropNameID createPropNameIDFromUtf16(
230
+ const char16_t *utf16, size_t length) override;
231
+ jsi::PropNameID createPropNameIDFromString(const jsi::String &str) override;
232
+ jsi::PropNameID createPropNameIDFromSymbol(const jsi::Symbol &sym) override;
233
+ std::string utf8(const jsi::PropNameID &) override;
234
+ bool compare(const jsi::PropNameID &, const jsi::PropNameID &) override;
235
+
236
+ // Symbol
237
+ std::string symbolToString(const jsi::Symbol &) override;
238
+
239
+ // BigInt
240
+ jsi::BigInt createBigIntFromInt64(int64_t) override;
241
+ jsi::BigInt createBigIntFromUint64(uint64_t) override;
242
+ bool bigintIsInt64(const jsi::BigInt &) override;
243
+ bool bigintIsUint64(const jsi::BigInt &) override;
244
+ uint64_t truncate(const jsi::BigInt &) override;
245
+ jsi::String bigintToString(const jsi::BigInt &, int) override;
246
+
247
+ // String
248
+ jsi::String createStringFromAscii(const char *str, size_t length) override;
249
+ jsi::String createStringFromUtf8(const uint8_t *utf8, size_t length) override;
250
+ jsi::String createStringFromUtf16(
251
+ const char16_t *utf16, size_t length) override;
252
+ std::string utf8(const jsi::String &) override;
253
+
254
+ // Object
255
+ jsi::Object createObject() override;
256
+ jsi::Object createObject(std::shared_ptr<jsi::HostObject> ho) override;
257
+ std::shared_ptr<jsi::HostObject> getHostObject(const jsi::Object &) override;
258
+ jsi::HostFunctionType &getHostFunction(const jsi::Function &) override;
259
+
260
+ jsi::Object createObjectWithPrototype(const jsi::Value &prototype) override;
261
+ void setPrototypeOf(
262
+ const jsi::Object &object, const jsi::Value &prototype) override;
263
+ jsi::Value getPrototypeOf(const jsi::Object &object) override;
264
+
265
+ bool hasNativeState(const jsi::Object &) override;
266
+ std::shared_ptr<jsi::NativeState> getNativeState(
267
+ const jsi::Object &) override;
268
+ void setNativeState(
269
+ const jsi::Object &, std::shared_ptr<jsi::NativeState>) override;
270
+
271
+ jsi::Value getProperty(
272
+ const jsi::Object &, const jsi::PropNameID &name) override;
273
+ jsi::Value getProperty(const jsi::Object &, const jsi::String &name) override;
274
+ bool hasProperty(const jsi::Object &, const jsi::PropNameID &name) override;
275
+ bool hasProperty(const jsi::Object &, const jsi::String &name) override;
276
+ void setPropertyValue(
277
+ const jsi::Object &, const jsi::PropNameID &name,
278
+ const jsi::Value &value) override;
279
+ void setPropertyValue(
280
+ const jsi::Object &, const jsi::String &name,
281
+ const jsi::Value &value) override;
282
+
283
+ bool isArray(const jsi::Object &) const override;
284
+ bool isArrayBuffer(const jsi::Object &) const override;
285
+ bool isFunction(const jsi::Object &) const override;
286
+ bool isHostObject(const jsi::Object &) const override;
287
+ bool isHostFunction(const jsi::Function &) const override;
288
+ jsi::Array getPropertyNames(const jsi::Object &) override;
289
+
290
+ // WeakObject
291
+ jsi::WeakObject createWeakObject(const jsi::Object &) override;
292
+ jsi::Value lockWeakObject(const jsi::WeakObject &) override;
293
+
294
+ // Array / ArrayBuffer
295
+ jsi::Array createArray(size_t length) override;
296
+ jsi::ArrayBuffer createArrayBuffer(
297
+ std::shared_ptr<jsi::MutableBuffer> buffer) override;
298
+ size_t size(const jsi::Array &) override;
299
+ size_t size(const jsi::ArrayBuffer &) override;
300
+ uint8_t *data(const jsi::ArrayBuffer &) override;
301
+ jsi::Value getValueAtIndex(const jsi::Array &, size_t i) override;
302
+ void setValueAtIndexImpl(
303
+ const jsi::Array &, size_t i, const jsi::Value &value) override;
304
+
305
+ // Function
306
+ jsi::Function createFunctionFromHostFunction(
307
+ const jsi::PropNameID &name, unsigned int paramCount,
308
+ jsi::HostFunctionType func) override;
309
+ jsi::Value call(
310
+ const jsi::Function &, const jsi::Value &jsThis, const jsi::Value *args,
311
+ size_t count) override;
312
+ jsi::Value callAsConstructor(
313
+ const jsi::Function &, const jsi::Value *args, size_t count) override;
314
+
315
+ // Comparison
316
+ bool strictEquals(const jsi::Symbol &a, const jsi::Symbol &b) const override;
317
+ bool strictEquals(const jsi::BigInt &a, const jsi::BigInt &b) const override;
318
+ bool strictEquals(const jsi::String &a, const jsi::String &b) const override;
319
+ bool strictEquals(const jsi::Object &a, const jsi::Object &b) const override;
320
+ bool instanceOf(const jsi::Object &o, const jsi::Function &f) override;
321
+
322
+ void setExternalMemoryPressure(
323
+ const jsi::Object &obj, size_t amount) override;
324
+
325
+ public:
326
+ // Conversions. Public because the class callbacks are free functions; they
327
+ // are not part of the JSI surface.
328
+
329
+ /// Takes ownership of the reference.
330
+ jsi::Value createValue(JSValue value);
331
+
332
+ /**
333
+ * Wraps a JSValue without taking a reference of its own, so the caller's
334
+ * reference must outlive the wrapper.
335
+ *
336
+ * Used for host function arguments: quickjs keeps argv alive for the whole
337
+ * call, so dup'ing each argument on entry and freeing it on return is a pair
338
+ * of refcount operations that provably cancel. Every path that keeps an
339
+ * argument copies it, and every copy path takes a real reference, so a
340
+ * borrowed wrapper cannot outlive the call that made it.
341
+ */
342
+ jsi::Value borrowValue(JSValue value);
343
+
344
+ /// Takes the reference out of a jsi::Value that is about to be destroyed,
345
+ /// rather than dup'ing a value whose destructor is about to free the
346
+ /// original. Symmetric with borrowValue.
347
+ JSValue takeJSValue(jsi::Value &&value);
348
+
349
+ jsi::Object createObjectFrom(JSValue value);
350
+ jsi::Symbol createSymbolFrom(JSValue value);
351
+ jsi::String createStringFrom(JSValue value);
352
+ jsi::PropNameID createPropNameIDFrom(JSAtom atom);
353
+
354
+ /// Returns a borrowed reference; the caller must not free it.
355
+ static JSValue toJSValue(const jsi::Pointer &pointer);
356
+ JSValue toJSValue(const jsi::Value &value) const;
357
+ static JSAtom toJSAtom(const jsi::PropNameID &name);
358
+
359
+ /// Throws the pending quickjs exception as a jsi::JSError. Never returns.
360
+ [[noreturn]] void throwPendingError();
361
+
362
+ /// Describes `exception` using nothing that can re-enter JavaScript. Always
363
+ /// returns a non-empty string for a non-empty exception and never leaves a
364
+ /// pending exception behind.
365
+ std::string describeExceptionWithoutRunningJS(JSValue exception);
366
+
367
+ /// Throws if `value` is the exception sentinel, otherwise returns it.
368
+ JSValue checkException(JSValue value);
369
+ /// Throws if `result` is negative, the quickjs error convention.
370
+ void checkException(int result);
371
+
372
+ /// Converts an in-flight C++ exception into a pending quickjs exception.
373
+ /// Called from class callbacks, which must not let C++ exceptions escape into
374
+ /// the engine's C frames. `origin` names the kind of callback that threw.
375
+ JSValue throwAsJSException(
376
+ const std::exception *e, const char *origin = "HostFunction") noexcept;
377
+
378
+ private:
379
+ void registerClasses();
380
+ void releaseNativePayloads() noexcept;
381
+ void drainPendingReleases() noexcept;
382
+ void rebaselineHeap() noexcept;
383
+ void noteCollection() noexcept;
384
+ void checkCanHoldNativeState(const jsi::Object &object);
385
+ JSValue evalInternal(const char *source) noexcept;
386
+
387
+ jsi::Value evaluateSource(
388
+ const uint8_t *source, size_t size, const std::string &sourceURL);
389
+ jsi::Value evaluateBytecode(const uint8_t *payload, size_t size);
390
+
391
+ // toJSValue borrows, so the array holds no references and needs no cleanup:
392
+ // the jsi::Values the caller owns outlive the call.
393
+ class ArgumentList {
394
+ public:
395
+ ArgumentList(
396
+ const QuickJSRuntime &runtime, const jsi::Value *args, size_t count) {
397
+ if (count > kInline) {
398
+ heap_.resize(count);
399
+ values_ = heap_.data();
400
+ }
401
+ for (size_t i = 0; i < count; ++i) {
402
+ values_[i] = runtime.toJSValue(args[i]);
403
+ }
404
+ }
405
+
406
+ JSValue *data() noexcept {
407
+ return values_;
408
+ }
409
+
410
+ private:
411
+ static constexpr size_t kInline = 8;
412
+ JSValue inline_[kInline];
413
+ std::vector<JSValue> heap_;
414
+ JSValue *values_{inline_};
415
+ };
416
+
417
+ // Pooled: one PointerValue per jsi value crossing the boundary, and
418
+ // new/delete measured ~20 ns against a ~4.5 ns property lookup. Inline
419
+ // because the call itself was a measurable part of that cost.
420
+ QuickJSPointerValue *allocPointerValue(JSValue value, bool owned = true) {
421
+ if (freeValues_ == nullptr) {
422
+ refillValueSlab();
423
+ }
424
+ QuickJSPointerValue *slot = freeValues_;
425
+ freeValues_ = slot->nextFree_;
426
+ return new (static_cast<void *>(slot))
427
+ QuickJSPointerValue(*this, value, owned);
428
+ }
429
+
430
+ QuickJSAtomPointerValue *allocAtomPointerValue(JSAtom atom) {
431
+ if (freeAtoms_ == nullptr) {
432
+ refillAtomSlab();
433
+ }
434
+ QuickJSAtomPointerValue *slot = freeAtoms_;
435
+ freeAtoms_ = slot->nextFree_;
436
+ return new (static_cast<void *>(slot)) QuickJSAtomPointerValue(*this, atom);
437
+ }
438
+
439
+ void recyclePointerValue(QuickJSPointerValue *pv) noexcept {
440
+ if (isOnJSThread()) {
441
+ pv->nextFree_ = freeValues_;
442
+ freeValues_ = pv;
443
+ return;
444
+ }
445
+ queuePointerValue(pv);
446
+ }
447
+
448
+ void recycleAtomPointerValue(QuickJSAtomPointerValue *pv) noexcept {
449
+ if (isOnJSThread()) {
450
+ pv->nextFree_ = freeAtoms_;
451
+ freeAtoms_ = pv;
452
+ return;
453
+ }
454
+ queueAtomPointerValue(pv);
455
+ }
456
+
457
+ void refillValueSlab();
458
+ void refillAtomSlab();
459
+ void queuePointerValue(QuickJSPointerValue *pv) noexcept;
460
+ void queueAtomPointerValue(QuickJSAtomPointerValue *pv) noexcept;
461
+ void freePointerValuePool() noexcept;
462
+
463
+ static constexpr size_t kPointerValueSlabSize = 256;
464
+ static constexpr int kMaxTeardownGCPasses = 2;
465
+
466
+ QuickJSRuntimeConfig config_;
467
+ JSRuntime *runtime_{nullptr};
468
+ JSContext *context_{nullptr};
469
+ std::function<void(const std::string &, const std::string &)>
470
+ onScriptEvaluated_;
471
+
472
+ // Atomic because ownership moves while other threads read it to route their
473
+ // releases. Recursive so a host function called from JavaScript can lock
474
+ // without deadlocking against the boundary lock it already runs under.
475
+ std::atomic<std::thread::id> jsThread_;
476
+ std::recursive_mutex engineMutex_;
477
+
478
+ // Building a jsi::JSError reads .message and .stack, which can throw again.
479
+ int errorDepth_{0};
480
+ static constexpr int kMaxErrorDepth = 8;
481
+
482
+ // Described on the way in, because by the time the nesting bound is hit the
483
+ // original exception is gone and the bail-out would name only itself.
484
+ std::string firstErrorDescription_;
485
+
486
+ JSClassID hostObjectClassID_{0};
487
+ JSClassID hostFunctionClassID_{0};
488
+ JSClassID nativeStateClassID_{0};
489
+
490
+ // Intrusive list heads of the live native payloads, held as void* to keep the
491
+ // payload types private to the implementation.
492
+ void *hostObjectProxies_{nullptr};
493
+ void *hostFunctionProxies_{nullptr};
494
+ void *nativeStateProxies_{nullptr};
495
+
496
+ // Engine facilities with no C API, evaluated once at startup.
497
+ JSValue enumeratePropertyNames_{JS_UNDEFINED};
498
+ JSValue symbolToString_{JS_UNDEFINED};
499
+ JSValue weakRefConstructor_{JS_UNDEFINED};
500
+ JSValue weakRefDeref_{JS_UNDEFINED};
501
+ JSValue bigIntToString_{JS_UNDEFINED};
502
+
503
+ // NativeState is an ordinary property under a private symbol, which script
504
+ // has no way to name, list or overwrite. A WeakMap would also be invisible
505
+ // but costs a call into JavaScript on every read and write.
506
+ JSAtom nativeStateKey_{JS_ATOM_NULL};
507
+
508
+ QuickJSPointerValue *freeValues_{nullptr};
509
+ QuickJSAtomPointerValue *freeAtoms_{nullptr};
510
+ std::vector<void *> valueSlabs_;
511
+ std::vector<void *> atomSlabs_;
512
+
513
+ std::atomic<bool> hasPendingReleases_{false};
514
+ // End of the most recent JS task. The gap to the start of the next one is
515
+ // what says whether anything is still driving JS.
516
+ std::chrono::steady_clock::time_point lastTaskEnd_{};
517
+ std::chrono::steady_clock::time_point pendingSince_{};
518
+ // What heap growth is measured from. Reset by a collection and after each
519
+ // top-level script, so `grown` means growth since the app last settled.
520
+ size_t heapBaseline_{0};
521
+ bool startupGCBracketUsed_{false};
522
+ std::mutex pendingMutex_;
523
+ std::vector<JSValue> pendingValues_;
524
+ std::vector<JSAtom> pendingAtoms_;
525
+ std::vector<QuickJSPointerValue *> pendingPointerValues_;
526
+ std::vector<QuickJSAtomPointerValue *> pendingAtomPointerValues_;
527
+ };
528
+
529
+ } // namespace qjs
@@ -0,0 +1,103 @@
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
+ #pragma once
9
+
10
+ #include <cstddef>
11
+ #include <string>
12
+
13
+ namespace qjs {
14
+
15
+ struct QuickJSRuntimeConfig {
16
+ /// Where compiled bytecode is cached between launches. Empty disables it.
17
+ std::string codeCacheDir;
18
+
19
+ /// Hard cap on the JS heap in bytes. 0 means unlimited.
20
+ size_t memoryLimit{0};
21
+
22
+ /// JS stack size in bytes. 0 uses the quickjs default.
23
+ size_t stackSize{0};
24
+
25
+ /// Installs the `HermesInternal` object React Native probes for. Without it
26
+ /// React Native's `hasPromise()` check fails and every app replaces the
27
+ /// native Promise with a setImmediate polyfill, at 1205 ns per construct and
28
+ /// chain against 775 ns native.
29
+ bool reactNativeCompat{true};
30
+
31
+ /// Must be decided before the bundle is parsed: statement traps are emitted
32
+ /// at parse time, so a runtime created with this off cannot be attached to
33
+ /// later. A frontend would connect, accept breakpoints and never stop.
34
+ bool enableDebugger{false};
35
+
36
+ /// Skips a collection triggered while JS is on the stack and runs it at the
37
+ /// next safepoint instead. Only cycles wait -- acyclic garbage still dies at
38
+ /// its last decref. Measured on a list-rebuilding workload: worst frame
39
+ /// 45.07 ms -> 3.33 ms, frames over budget 68/300 -> 0.
40
+ bool deferGC{true};
41
+
42
+ /// Heap size past which a collection runs wherever execution happens to be.
43
+ /// 0 derives it from the live heap, re-derived after every collection.
44
+ ///
45
+ /// The engine's own default is `malloc_gc_threshold * 4` fixed at
46
+ /// construction time, which is 1.00 MiB forever. Deferral is gated on
47
+ /// `live + size < ceiling`, and React Native's entry cascade alone peaks at
48
+ /// 1.557 MB, so that default stops engaging before an app finishes starting.
49
+ size_t deferredGCLimit{0};
50
+
51
+ /// The derived ceiling is `live + clamp(live, minSlack, maxSlack)`. Additive
52
+ /// rather than a multiple, which at a 69 MB heap would give 1.1 GB.
53
+ ///
54
+ /// What it bounds is garbage accumulated while deferred, so the rule is
55
+ /// `minSlack > 2 x heap growth between quiet windows` -- measured at 48.4 MB
56
+ /// on the example app with a 50,000-row list. Peak heap can reach
57
+ /// `live + slack`, so lowering these trades memory for frame drops.
58
+ size_t deferredGCMinSlack{48 * 1024 * 1024};
59
+ size_t deferredGCMaxSlack{256 * 1024 * 1024};
60
+
61
+ /// How far the GC threshold may drift above the live heap before it is pulled
62
+ /// back down, as a multiple of it. 0 disables the retune.
63
+ ///
64
+ /// The engine only recomputes the threshold at a collection, so refcounting
65
+ /// can shrink the heap far below it and nothing lowers it again. Measured on
66
+ /// device: frozen at 144 MB against a 63 MB heap, two collections in 20
67
+ /// cycles. The engine targets 1.5x, so 3.0 leaves a wide dead band.
68
+ double gcThresholdRetuneRatio{3.0};
69
+
70
+ /// Heap growth allowed to the first top-level script before the collector may
71
+ /// run during it. 0 disables the bracket.
72
+ ///
73
+ /// Module instantiation builds a graph and keeps it, so the collector finds
74
+ /// 14 collectable objects across React Native's whole entry cascade and fires
75
+ /// three times before any of them exist -- 772 us freeing nothing. Bounded,
76
+ /// so a larger script resumes normal collection by itself.
77
+ size_t startupGCBracketBytes{8 * 1024 * 1024};
78
+
79
+ /// How long a gap before the current task counts as the app going quiet.
80
+ ///
81
+ /// An empty JS stack is not idleness: during a gesture native drives JS every
82
+ /// frame, so collecting at the first safepoint just moves the pause between
83
+ /// two frames of the same gesture. Measured, a scroll took 74 collections at
84
+ /// 0 and 1 at anything from 16 ms up, so this only has to clear the frame
85
+ /// cadence by enough that one slow frame does not read as quiet.
86
+ size_t gcIdleGapMs{120};
87
+
88
+ /// The longest a collection may stay pending before the next safepoint takes
89
+ /// it regardless. 0 disables.
90
+ ///
91
+ /// Idleness alone never fires for an app with a ticker. Measured with a 16 ms
92
+ /// setInterval: one collection pending across thousands of safepoints, zero
93
+ /// idle drains, and every collection that ran was forced by the pressure
94
+ /// valve -- mid-gesture, which is what deferral exists to avoid.
95
+ size_t gcMaxDeferralMs{2000};
96
+
97
+ /// Heap size past which the idle requirement is dropped, though the
98
+ /// collection still waits for a safepoint. Sits below deferredGCLimit. 0
99
+ /// derives it from the slack.
100
+ size_t gcPressureBytes{0};
101
+ };
102
+
103
+ } // namespace qjs
@@ -0,0 +1,34 @@
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 "QuickJSRuntimeFactory.h"
9
+
10
+ #include "QuickJSCompat.h"
11
+ #include "QuickJSModule.h"
12
+ #include "QuickJSRuntime.h"
13
+
14
+ namespace qjs {
15
+
16
+ std::unique_ptr<facebook::jsi::Runtime> makeQuickJSRuntime(
17
+ QuickJSRuntimeConfig config) {
18
+ const bool reactNativeCompat = config.reactNativeCompat;
19
+
20
+ auto runtime = std::make_unique<QuickJSRuntime>(std::move(config));
21
+
22
+ // Both install before any application JavaScript runs, because React
23
+ // Native's polyfill checks run very early: anything installed afterwards is
24
+ // shadowed by a polyfill that has already decided the engine lacks it.
25
+ if (reactNativeCompat) {
26
+ installReactNativeCompat(*runtime);
27
+ }
28
+
29
+ installModules(*runtime);
30
+
31
+ return runtime;
32
+ }
33
+
34
+ } // namespace qjs
@@ -0,0 +1,24 @@
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
+ #pragma once
9
+
10
+ #include <jsi/jsi.h>
11
+
12
+ #include <memory>
13
+
14
+ #include "QuickJSRuntimeConfig.h"
15
+
16
+ namespace qjs {
17
+
18
+ /// Creates a QuickJS-backed jsi::Runtime with the compatibility shims and every
19
+ /// registered module installed. The only entry point a non-React consumer
20
+ /// needs.
21
+ std::unique_ptr<facebook::jsi::Runtime> makeQuickJSRuntime(
22
+ QuickJSRuntimeConfig config = {});
23
+
24
+ } // namespace qjs