-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathforge.cpp
More file actions
2497 lines (2044 loc) · 86.7 KB
/
Copy pathforge.cpp
File metadata and controls
2497 lines (2044 loc) · 86.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include <stdio.h>
#include <string.h>
#include "js/CallAndConstruct.h"
#include "js/String.h"
#include <string>
#include <utility>
#include "js/CompileOptions.h"
#include "js/CompilationAndEvaluation.h"
#include "js/Context.h"
#include "js/GCAPI.h"
#include "js/GlobalObject.h"
#include "js/Initialization.h"
#include "js/RealmOptions.h"
#include "js/RootingAPI.h"
#include "js/SourceText.h"
#include "js/Promise.h"
#include "js/Object.h"
#include "js/TracingAPI.h"
#include "js/Value.h"
#include "js/CallArgs.h"
#include "js/CharacterEncoding.h"
#include "js/PropertyAndElement.h"
#include "js/Conversions.h"
#include "js/Exception.h"
#include "js/ErrorReport.h"
#include "js/ArrayBuffer.h"
#include "js/experimental/TypedData.h"
#include "js/PropertyDescriptor.h" // JSPROP_ENUMERATE (Phase 7.3, fs.statSync)
// JS_NewPlainObject (Phase 7.3, fs's namespace object and statSync's
// returned {size} object) is not exposed under js/public -- confirmed by
// reading js/public/Object.h directly, which has no object-construction
// free function. It lives in jsapi.h instead (js/src/jsapi.h, confirmed by
// reading it directly), the classic top-level SpiderMonkey embedding
// header every other js/public/*.h include in this file was deliberately
// kept narrower than -- included here just for this one function.
#include "jsapi.h"
#include "forge-core/platform/IoLoop.h"
#include "forge-core/File.h"
#include "forge-core/HashMap.h"
#include "forge-core/Path.h"
#include "forge-core/Queue.h"
#include "forge-core/Result.h"
#include "forge-core/String.h"
#include "forge-core/StringView.h"
#include "forge-core/memory/DefaultAllocator.h"
#include "forge-core/memory/MakeUnique.h"
#include "forge-core/memory/UniquePtr.h"
#include "forge-core/memory/Vector.h"
// cx points to SpiderMonkey's context object.
static const JSClass globalClass = {
"global",
JSCLASS_GLOBAL_FLAGS | JSCLASS_HAS_RESERVED_SLOTS(1),
&JS::DefaultGlobalClassOps,
};
enum GlobalSlots { RuntimeSlot = JSCLASS_GLOBAL_SLOT_COUNT };
static bool EnqueueMicrotask(JSContext* cx, JS::HandleObject callback);
// Forward declarations: ForgeTimerFired (defined further down, right after
// JsTimerRegistry) needs to reach the current Runtime's timer registry,
// but Runtime itself is only fully defined later in this file (it embeds
// JsTimerRegistry by value, so it can't be forward-declared-only at that
// point). Same pattern already used for EnqueueMicrotask above.
class JsTimerRegistry;
static JsTimerRegistry& GetRuntimeTimers(JSContext* cx);
// Prints (and consumes) whatever exception is currently pending on `cx`,
// tagged with `context` for the "could not even retrieve it" fallback
// case. Shared by every callback boundary that calls into JS and cannot
// propagate a C++ exception on failure (there are no C++ exceptions in
// this codebase — see forge-core's Result<T>/AGENTS.md philosophy; this is
// the JS-engine-boundary equivalent: report, don't throw, don't silently
// drop it).
static void ReportPendingException(JSContext* cx, const char* context) {
if (!JS_IsExceptionPending(cx)) {
return;
}
JS::ExceptionStack exn(cx);
if (JS::StealPendingExceptionStack(cx, &exn)) {
JS::ErrorReportBuilder report(cx);
if (report.init(cx, exn, JS::ErrorReportBuilder::NoSideEffects)) {
JS::PrintError(stderr, report.report(), false);
return;
}
}
fprintf(stderr, "Forge: %s failed (exception pending, but could not be retrieved)\n",
context);
}
// No `arguments` field here (unlike JsTimer below): ForgeJobQueue::runJobs()
// always invokes a microtask's callback with JS::HandleValueArray::empty(),
// never task->arguments, so a PersistentRootedVector<JS::Value> on every
// queued microtask was dead weight — allocated (well, self-registered with
// `cx`) and destroyed on every single one for a field nothing ever read.
// Only JsTimer actually needs argument forwarding, for setTimeout/
// setInterval's extra arguments passed through to the callback.
struct Microtask {
JS::Heap<JSObject*> callback;
};
//==============================================================================
// Runtime Integration (Phase 6)
//
// The microtask queue and timer registry below are backed by forge-core's
// own containers (Queue<T>/HashMap<K,V>) rather than std::vector, and
// EnqueueMicrotask/SetTimeout/SetInterval go through forge-core's
// MakeUnique<T> (Result<UniquePtr<T>>) rather than std::make_unique, so
// that an allocation failure is a reported JS OOM error instead of an
// abrupt std::terminate() (this codebase's -fno-exceptions build has no
// other way to recover from operator new failing). See HISTORY.md's
// Phase 6 entry for the two real bugs this rewrite fixes:
// (1) GC-root-tracing gap: a JobQueue must trace every still-pending
// microtask's callback, not just the front one, or the collector
// could reclaim a callback a later Pop() still needs. Fixed via
// Queue<T>::operator[] (front-relative indexed access, added this
// phase) walked in TraceForgeRoots below.
// (2) Use-after-free-on-OOM-rollback: JsTimerRegistry::Add() schedules
// the native timer before it knows whether the JS-side wrapper can
// actually be stored. If storing it fails, the *already-armed*
// native timer must be cancelled before returning, or it fires
// against memory that's about to be freed. See the comment in
// Add() below.
//==============================================================================
forge::core::Queue<forge::core::memory::UniquePtr<Microtask>> microtasks;
class ForgeJobQueue : public JS::JobQueue {
public:
bool empty() const override { return microtasks.Empty(); }
bool isDrainingStopped() const override { return false; }
bool getHostDefinedData(JSContext* cx,
JS::MutableHandle<JSObject*> data) const override {
data.set(nullptr);
return true;
}
void runJobs(JSContext* cx) override {
while (!microtasks.Empty()) {
Microtask* task = microtasks.Front().Get();
JS::RootedObject callback(cx, task->callback);
JS::RootedValue thisValue(cx, JS::UndefinedValue());
JS::RootedValue rval(cx);
bool ok = JS::Call(cx, thisValue, callback, JS::HandleValueArray::empty(),
JS::MutableHandleValue(&rval));
if (!ok) {
ReportPendingException(cx, "microtask");
return;
}
microtasks.Pop();
}
}
bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
JS::HandleObject job, JS::HandleObject allocationSite,
JS::HandleObject hostDefinedData) override {
return EnqueueMicrotask(cx, job);
}
protected:
class SavedQueue;
js::UniquePtr<SavedJobQueue> saveJobQueue(JSContext* cx) override;
};
js::UniquePtr<JS::JobQueue::SavedJobQueue> ForgeJobQueue::saveJobQueue(
JSContext* cx) {
return nullptr;
}
//==============================================================================
// Timers
//
// Bridges the JS-facing setTimeout/setInterval/clearTimeout API onto
// forge::core::platform::IoLoop's TimerId-based scheduling (see
// ROADMAP.md Phase 2 / HISTORY.md — this replaces the old TimerQueue's
// manual "scan every timer, compare to now()" polling, driven by
// EventLoop's busy-wait, with the IOCP-backed loop actually sleeping until
// the next timer or I/O event).
//
// Two id spaces are kept deliberately separate: `jsId` is the small
// integer setTimeout()/setInterval() return to script (unchanged
// behaviour); `nativeId` is IoLoop's own forge::core::platform::TimerId,
// used only internally to cancel the right native timer.
//==============================================================================
static void ForgeTimerFired(void* userData) noexcept;
struct JsTimer {
JSContext* cx;
JS::Heap<JSObject*> callback;
JS::PersistentRootedVector<JS::Value> arguments;
int jsId{0};
forge::core::platform::TimerId nativeId{0};
bool repeat{false};
explicit JsTimer(JSContext* cx) : cx(cx), arguments(cx) {}
};
class JsTimerRegistry {
public:
explicit JsTimerRegistry(forge::core::platform::IoLoop& loop) : loop_(loop) {}
// Takes ownership of `timer` (already populated with callback/arguments/
// repeat/delayMs by the caller) and schedules it. Returns the JS-visible
// id, or -1 if native scheduling (or JS-side registration) failed —
// `timer` is destroyed in that case, nothing is left registered.
int Add(forge::core::memory::UniquePtr<JsTimer> timer, uint64_t delayMs) {
timer->jsId = nextJsId_++;
JsTimer* raw = timer.Get();
forge::core::Result<forge::core::platform::TimerId> scheduled =
loop_.ScheduleTimer(delayMs, raw->repeat, &ForgeTimerFired, raw);
if (scheduled.HasError()) {
return -1;
}
raw->nativeId = scheduled.Value();
int jsId = raw->jsId;
forge::core::Result<bool> inserted = timers_.Insert(jsId, std::move(timer));
if (inserted.HasError()) {
// HashMap::Insert's failure path (its internal GrowIfNeeded() running
// out of memory) returns before ever moving from its V&& argument —
// so `timer` (this function's local parameter) still owns *raw at
// this point. That means the native timer scheduled above is now
// armed against memory that's about to be freed the moment this
// function returns (timer's destructor runs, since nothing else took
// ownership) — a real use-after-free the next time it fired. Cancel
// it first so there's nothing left pointing at *raw once it's gone.
loop_.CancelTimer(raw->nativeId);
return -1;
}
return jsId;
}
void CancelByJsId(int jsId) {
forge::core::memory::UniquePtr<JsTimer>* found = timers_.Find(jsId);
if (found == nullptr) {
return;
}
loop_.CancelTimer((*found)->nativeId);
timers_.Erase(jsId);
}
// Called once a one-shot timer's callback has finished running, to
// release its JsTimer. The native side has already removed the
// corresponding entry itself (TimerScheduler::PopDue erases one-shots on
// firing) — this only releases the JS-side wrapper.
void RemoveFired(int jsId) {
timers_.Erase(jsId);
}
// Traces every live timer's JS callback so the GC does not collect it
// out from under a still-pending timer. JS::Heap<T> (unlike
// JS::PersistentRootedVector, which self-registers and needs no manual
// tracing — that's why `arguments` above needed no attention here) is
// *not* traced automatically; something owning it must do so explicitly.
void TraceRoots(JSTracer* trc) {
for (auto entry : timers_) {
JS::TraceEdge(trc, &entry.Value()->callback, "forge-timer-callback");
}
}
// Cancels and releases every still-pending timer. Must be called (via
// Runtime::Shutdown()) while `cx` is still alive and before
// JS_DestroyContext(cx) — each JsTimer holds a
// JS::PersistentRootedVector<JS::Value>, which needs to unregister
// itself from the still-live context when destroyed. Normally run()
// only stops once every timer is already gone, but it can now also stop
// early on a genuine I/O error (see Runtime::run()), which is exactly
// the case this exists for.
void Clear() {
for (auto entry : timers_) {
loop_.CancelTimer(entry.Value()->nativeId);
}
timers_.Clear();
}
private:
forge::core::platform::IoLoop& loop_;
forge::core::HashMap<int, forge::core::memory::UniquePtr<JsTimer>> timers_;
int nextJsId_{1};
};
static void ForgeTimerFired(void* userData) noexcept {
auto* timer = static_cast<JsTimer*>(userData);
JSContext* cx = timer->cx;
bool repeat = timer->repeat;
int jsId = timer->jsId;
JS::RootedObject callback(cx, timer->callback);
JS::RootedValue thisValue(cx, JS::UndefinedValue());
JS::RootedValue rval(cx);
bool ok = JS::Call(cx, thisValue, callback, timer->arguments,
JS::MutableHandleValue(&rval));
if (!ok) {
ReportPendingException(cx, "timer callback");
}
// Must be last: on a one-shot timer this destroys *timer (via the
// registry's owning HashMap), so nothing above may touch `timer` again
// after this — hence copying jsId/repeat out at the top instead of
// reading timer->... below.
if (!repeat) {
GetRuntimeTimers(cx).RemoveFired(jsId);
}
}
//==============================================================================
// Runtime / GC root tracing
//==============================================================================
static void TraceForgeRoots(JSTracer* trc, void* data) {
auto* timers = static_cast<JsTimerRegistry*>(data);
for (forge::core::Size i = 0; i < microtasks.Size(); ++i) {
JS::TraceEdge(trc, µtasks[i]->callback, "forge-microtask-callback");
}
timers->TraceRoots(trc);
}
class Runtime {
public:
explicit Runtime(JSContext* cx)
: cx_(cx), loop_(), timers_(loop_), jobQueue_() {
JS::SetJobQueue(cx, &jobQueue_);
}
// Must be called exactly once, after construction and before any timer
// is scheduled or run() is called.
[[nodiscard]] forge::core::Result<void> Initialize() {
if (forge::core::Result<void> result = loop_.Initialize(); result.HasError()) {
return result;
}
JS_AddExtraGCRootsTracer(cx_, TraceForgeRoots, &timers_);
return {};
}
// Drains microtasks, then blocks on the event loop for the next timer
// or I/O completion, repeating until there is genuinely nothing left
// pending — replaces the old EventLoop's busy-poll `while (...) { ...;
// sleep_for(1ms); }` with the loop actually sleeping until something is
// due (see ROADMAP.md Phase 2).
void run() {
for (;;) {
jobQueue_.runJobs(cx_);
if (jobQueue_.empty() && loop_.Empty()) {
break;
}
forge::core::Result<void> result = loop_.RunOnce();
if (result.HasError()) {
fprintf(stderr, "Forge: event loop I/O error (native code %d)\n",
result.Error().NativeCode());
break;
}
}
}
JsTimerRegistry& timers() { return timers_; }
// Releases every still-pending timer. Call this once, after run() has
// returned (however it returned), and before JS_DestroyContext(cx) — see
// JsTimerRegistry::Clear() for exactly why this ordering matters.
void Shutdown() { timers_.Clear(); }
private:
JSContext* cx_;
forge::core::platform::IoLoop loop_;
JsTimerRegistry timers_;
ForgeJobQueue jobQueue_;
};
Runtime* GetRuntime(JSContext* cx)
{
JS::RootedObject global(
cx,
JS::CurrentGlobalOrNull(cx)
);
if (!global) {
return nullptr;
}
JS::Value value =
JS::GetReservedSlot(global, RuntimeSlot);
return static_cast<Runtime*>(value.toPrivate());
}
// Thin helper so ForgeTimerFired (a free function, declared above Runtime)
// can reach the registry without needing Runtime's full definition visible
// at its point of use.
static JsTimerRegistry& GetRuntimeTimers(JSContext* cx) {
return GetRuntime(cx)->timers();
}
static bool Print(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
for (unsigned i = 0; i < args.length(); i++) {
JS::RootedString str(cx, JS::ToString(cx, args[i]));
if (!str) {
return false;
}
JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str);
if (!bytes) {
return false;
}
printf("%s", bytes.get());
if (i + 1 < args.length()) {
printf(" ");
}
}
printf("\n");
args.rval().setUndefined();
return true;
}
static bool SetTimeout(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (argc < 2) {
return false;
}
if (!args[0].isObject()) {
return false;
}
double delay = 0;
if (!JS::ToNumber(cx, args[1], &delay)) {
return false;
}
if (delay < 0 || !(delay == delay)) { // NaN-safe: NaN < 0 is false, so clamp NaN explicitly too.
delay = 0;
}
forge::core::Result<forge::core::memory::UniquePtr<JsTimer>> made =
forge::core::memory::MakeUnique<JsTimer>(cx);
if (made.HasError()) {
JS_ReportOutOfMemory(cx);
return false;
}
forge::core::memory::UniquePtr<JsTimer> timer = std::move(made.Value());
JS::RootedObject callback(cx, &args[0].toObject());
timer->callback = callback;
timer->repeat = false;
for (unsigned i = 2; i < args.length(); i++) {
if (!timer->arguments.append(args[i])) {
return false;
}
}
int id = GetRuntime(cx)->timers().Add(std::move(timer), (uint64_t)delay);
if (id < 0) {
JS_ReportErrorASCII(cx, "setTimeout: failed to schedule timer");
return false;
}
args.rval().setInt32(id);
return true;
}
static bool ClearTimeout(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (argc < 1) {
return false;
}
int32_t id;
if (!JS::ToInt32(cx, args[0], &id)) {
return false;
}
GetRuntime(cx)->timers().CancelByJsId(id);
args.rval().setUndefined();
return true;
}
static bool SetInterval(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (argc < 2) {
return false;
}
if (!args[0].isObject()) {
return false;
}
double delay = 0;
if (!JS::ToNumber(cx, args[1], &delay)) {
return false;
}
if (delay < 0 || !(delay == delay)) {
delay = 0;
}
forge::core::Result<forge::core::memory::UniquePtr<JsTimer>> made =
forge::core::memory::MakeUnique<JsTimer>(cx);
if (made.HasError()) {
JS_ReportOutOfMemory(cx);
return false;
}
forge::core::memory::UniquePtr<JsTimer> timer = std::move(made.Value());
JS::RootedObject callback(cx, &args[0].toObject());
timer->callback = callback;
timer->repeat = true;
for (unsigned i = 2; i < args.length(); i++) {
if (!timer->arguments.append(args[i])) {
return false;
}
}
int id = GetRuntime(cx)->timers().Add(std::move(timer), (uint64_t)delay);
if (id < 0) {
JS_ReportErrorASCII(cx, "setInterval: failed to schedule timer");
return false;
}
args.rval().setInt32(id);
return true;
}
static bool EnqueueMicrotask(JSContext* cx, JS::HandleObject callback) {
forge::core::Result<forge::core::memory::UniquePtr<Microtask>> made =
forge::core::memory::MakeUnique<Microtask>();
if (made.HasError()) {
JS_ReportOutOfMemory(cx);
return false;
}
forge::core::memory::UniquePtr<Microtask> task = std::move(made.Value());
task->callback = callback;
if (microtasks.Push(std::move(task)).HasError()) {
JS_ReportOutOfMemory(cx);
return false;
}
return true;
}
static bool QueueMicrotask(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
if (argc < 1) {
return false;
}
if (!args[0].isObject()) {
return false;
}
JS::RootedObject callback(cx, &args[0].toObject());
if (!EnqueueMicrotask(cx, callback)) {
return false;
}
args.rval().setUndefined();
return true;
}
//==============================================================================
// JS/Native Marshalling Primitives (Phase 7.2) -- real-build-confirmed
// 2026-07-30 (`python mach build` succeeded against this exact file).
//
// Implements JsBindings.md's frozen "Public API" section verbatim (same
// names, parameter types, return types) -- the conventions every future
// JS-visible binding (fs now; net/threads later) is meant to share rather
// than each reinventing its own error/marshalling glue. See JsBindings.md
// for the full design rationale; only implementation-specific notes are
// repeated here.
//
// All six are `static` (internal linkage), matching every other helper
// in this file (Print/SetTimeout/ReportPendingException/etc.) -- none of
// them need to be visible outside this translation unit. None of the six
// are called from any JS-visible entry point yet -- Phase 7.3 (the
// concrete `fs.*Sync` bindings from Fs.md) is what wires them up to a
// JS_DefineFunction. Until then, the self-test suite below (run via
// `forge --self-test`) is what exercises them for real: it gives every
// one of these six a genuine call site with a live JSContext/Realm, so
// none needs an [[maybe_unused]] marker despite nothing script-visible
// calling them yet.
//
// Verification history (per AGENTS.md's "Be Honest"): every SpiderMonkey
// function name/signature used below (JS_NewStringCopyUTF8N,
// JS_IsUint8Array, JS_NewUint8Array, JS_GetUint8ArrayData,
// JS_GetTypedArrayByteLength, JS::IsArrayBufferObject,
// JS::IsDetachedArrayBufferObject, JS::GetArrayBufferData,
// JS::GetArrayBufferByteLength, JS::AutoCheckCannotGC, and everything
// already proven by Print()/SetTimeout()/etc. above) was confirmed by
// reading the real headers under this tree's own
// js/public/{ArrayBuffer.h,experimental/TypedData.h,String.h,
// Exception.h,ErrorReport.h,Conversions.h,PropertyAndElement.h,
// Value.h,GCAPI.h,RootingAPI.h} directly before writing this code -- not
// guessed or inferred from memory. Before the real `mach build`, the
// forge-core-facing logic was separately compiled and run (real
// forge-core headers + a signature-faithful fake JSAPI shim) under
// g++/clang++ with full warnings, ASan+UBSan, and valgrind (16/16
// scenarios, 0 leaks) -- see HISTORY.md's Phase 7.2 entry for the full
// account. The real `mach build` succeeding confirms the SpiderMonkey
// call shapes themselves were right; the self-test suite below adds the
// one thing neither of those passes covered: actually calling these
// functions against a live JSContext and checking their results.
//==============================================================================
// ErrorCode -> error.code string table from JsBindings.md's "Error
// Handling Policy". Exhaustive over every ErrorCode enumerator (verified
// against forge-core/Error.h directly) so adding a new enumerator there
// without updating this switch is a compiler error (no `default:`), not
// a silent "Unknown" fallback for a code that should have had a real
// mapping -- the trailing return below only covers the (impossible in
// practice) case of `code` holding a value outside the enum's defined
// range.
static const char* ErrorCodeToString(forge::core::ErrorCode code) {
using forge::core::ErrorCode;
switch (code) {
case ErrorCode::None: return "None";
case ErrorCode::Unknown: return "Unknown";
case ErrorCode::InvalidArgument: return "InvalidArgument";
case ErrorCode::InvalidOperation: return "InvalidOperation";
case ErrorCode::NotSupported: return "NotSupported";
case ErrorCode::NotImplemented: return "NotImplemented";
case ErrorCode::NotFound: return "NotFound";
case ErrorCode::AlreadyExists: return "AlreadyExists";
case ErrorCode::PermissionDenied: return "PermissionDenied";
case ErrorCode::Busy: return "Busy";
case ErrorCode::Timeout: return "Timeout";
case ErrorCode::Cancelled: return "Cancelled";
case ErrorCode::EndOfFile: return "EndOfFile";
case ErrorCode::IOError: return "IOError";
case ErrorCode::OutOfMemory: return "OutOfMemory";
case ErrorCode::BufferTooSmall: return "BufferTooSmall";
case ErrorCode::Overflow: return "Overflow";
case ErrorCode::Underflow: return "Underflow";
case ErrorCode::InvalidData: return "InvalidData";
case ErrorCode::ParseError: return "ParseError";
case ErrorCode::PlatformError: return "PlatformError";
}
return "Unknown";
}
// Throws a JS Error annotated with `.code` (and, when available, `.path`/
// `.syscall`/`.nativeCode`) for a failed Result<T>/Result<void>. Per
// JsBindings.md, OutOfMemory is never routed through here -- callers use
// JS_ReportOutOfMemory directly, matching EnqueueMicrotask/SetTimeout/
// SetInterval above.
static void ThrowJsError(JSContext* cx,
const forge::core::Error& error,
const char* context,
const forge::core::Path* path = nullptr) {
const char* codeStr = ErrorCodeToString(error.Code());
// forge::core::Error carries no free-text message (see Error.h -- just
// Code()/NativeCode()), so the reported message is synthesized from
// `context` and the mapped code string rather than inventing an
// Error::Message() this codebase doesn't have.
JS_ReportErrorUTF8(cx, "%s failed: %s", context, codeStr);
if (!JS_IsExceptionPending(cx)) {
// JS_ReportErrorUTF8 itself hit trouble (e.g. OOM formatting the
// message) and left nothing pending to annotate further.
return;
}
JS::RootedValue excVal(cx);
if (!JS_GetPendingException(cx, &excVal) || !excVal.isObject()) {
// Not an Error object we can annotate (shouldn't normally happen for
// an exception JS_ReportErrorUTF8 itself just created) -- the base
// message is still thrown, just without the extra properties below.
return;
}
JS::RootedObject excObj(cx, &excVal.toObject());
// Every JS_SetProperty call below is best-effort: if one fails (e.g.
// OOM allocating the property string), the base thrown Error --
// already pending -- is left intact rather than compounding the
// original failure with a second, unrelated OOM report.
if (JSString* codeJsStr = JS_NewStringCopyZ(cx, codeStr)) {
JS::RootedValue codeVal(cx, JS::StringValue(codeJsStr));
JS_SetProperty(cx, excObj, "code", codeVal);
}
if (JSString* syscallJsStr = JS_NewStringCopyZ(cx, context)) {
JS::RootedValue syscallVal(cx, JS::StringValue(syscallJsStr));
JS_SetProperty(cx, excObj, "syscall", syscallVal);
}
if (path != nullptr) {
const forge::core::StringView pathView = path->View();
if (JSString* pathJsStr = JS_NewStringCopyUTF8N(
cx, JS::UTF8Chars(pathView.Data(), pathView.Size()))) {
JS::RootedValue pathVal(cx, JS::StringValue(pathJsStr));
JS_SetProperty(cx, excObj, "path", pathVal);
}
}
if (error.Code() == forge::core::ErrorCode::PlatformError) {
JS::RootedValue nativeCodeVal(cx, JS::NumberValue(error.NativeCode()));
JS_SetProperty(cx, excObj, "nativeCode", nativeCodeVal);
}
// `excObj` is the very heap object already set as cx's pending
// exception (JS_GetPendingException returned it, not a copy) -- setting
// properties on it in place is visible to whatever eventually catches
// it without needing a second JS_SetPendingException call.
}
// Converts an arbitrary JS value to a forge::core::String via ToString
// semantics (matches JS::ToString(cx, value) + JS_EncodeStringToUTF8,
// the exact pattern Print() above already uses and this sandbox cannot
// re-verify beyond that existing, working precedent).
//
// Design note (documented here per the instruction to record any design
// clarification before implementing it -- this is a behavioral
// clarification of JsBindings.md's contract, not a change to any frozen
// signature): a Result<T> failure returned by this function, ToForgePath,
// or any future sibling never leaves its own exception pending on `cx` --
// JS_ClearPendingException is called internally on every failure path
// where an underlying JS:: call (JS::ToString, JS_EncodeStringToUTF8)
// may already have reported one. This lets every caller uniformly do
// `if (result.HasError()) { ThrowJsError(cx, result.Error(), ...); return
// false; }` on any Result<T> from this module without ever risking two
// exceptions pending at once. JsBindings.md's "Error Handling Policy"
// section is updated alongside this change to state the same thing.
static forge::core::Result<forge::core::String> ToForgeString(JSContext* cx,
JS::HandleValue value) {
JS::RootedString jsStr(cx, JS::ToString(cx, value));
if (!jsStr) {
JS_ClearPendingException(cx);
return forge::core::Result<forge::core::String>(
forge::core::Failure{forge::core::Error(forge::core::ErrorCode::InvalidArgument)});
}
JS::UniqueChars utf8 = JS_EncodeStringToUTF8(cx, jsStr);
if (!utf8) {
JS_ClearPendingException(cx);
return forge::core::Result<forge::core::String>(
forge::core::Failure{forge::core::Error(forge::core::ErrorCode::OutOfMemory)});
}
// Treated as a null-terminated C string, same assumption Print() above
// already relies on (printf("%s", bytes.get())) -- a JS string
// containing an embedded U+0000 would be silently truncated here. Not
// a new limitation introduced by this function, just inherited from
// the same JS_EncodeStringToUTF8-based idiom already in production use
// in this file.
return forge::core::String::Create(forge::core::StringView(utf8.get()));
}
// Converts a JS value to a forge::core::Path by first converting it to a
// String (above) and then through Path::Create -- Path never touches the
// OS itself (see Path.md), so this is pure value conversion, no new
// failure modes beyond what ToForgeString/Path::Create already have.
static forge::core::Result<forge::core::Path> ToForgePath(
JSContext* cx, JS::HandleValue value) {
forge::core::Result<forge::core::String> str = ToForgeString(cx, value);
if (str.HasError()) {
return forge::core::Result<forge::core::Path>(
forge::core::Failure{str.Error()});
}
return forge::core::Path::Create(str.Value().View());
}
// Converts a forge::core::StringView to a new JS string (copies; does not
// take ownership of `text`'s storage, matching every JS_New*StringCopy*
// function's own documented convention in js/String.h, as opposed to
// JS_NewUCString's ownership-transferring convention).
//
// Returns nullptr on OOM. JS_NewStringCopyUTF8N is expected to report its
// own failure (standard JS_New*StringCopy* convention across this
// header), but since that isn't spelled out explicitly in js/String.h's
// comments and this sandbox has no way to compile-and-observe it
// directly, this reports defensively if the call somehow left nothing
// pending -- so a caller can always trust "null return means an
// exception is pending" without needing to know which of the two
// conventions is actually in effect.
static JSString* FromForgeString(JSContext* cx,
forge::core::StringView text) {
JSString* result = JS_NewStringCopyUTF8N(
cx, JS::UTF8Chars(text.Data(), text.Size()));
if (!result && !JS_IsExceptionPending(cx)) {
JS_ReportOutOfMemory(cx);
}
return result;
}
// Creates a new Uint8Array and copies `bytes`'s contents into it. Consumes
// `bytes` (taken by value; released when this function returns, per
// JsBindings.md's documented ownership: the JS engine owns the copy from
// here on, forge-core's Vector<u8> is not retained).
static JSObject* Uint8ArrayFromBytes(JSContext* cx,
forge::core::Vector<forge::core::u8> bytes) {
const size_t length = static_cast<size_t>(bytes.Size());
JS::RootedObject array(cx, JS_NewUint8Array(cx, length));
if (!array) {
// JS_NewUint8Array reports its own failure (OOM, or a RangeError if
// `length` exceeds the engine's maximum typed array size).
return nullptr;
}
if (length > 0) {
JS::AutoCheckCannotGC nogc(cx);
bool isSharedMemory = false;
uint8_t* data = JS_GetUint8ArrayData(array, &isSharedMemory, nogc);
// `array` was just created by JS_NewUint8Array immediately above, so
// it is guaranteed to be a private, non-shared, non-detached buffer:
// `data` is non-null and `isSharedMemory` is false here by
// construction, not by runtime luck.
memcpy(data, bytes.Data(), length);
}
return array;
}
// Reads a JS Uint8Array or ArrayBuffer's bytes without copying into a
// forge-core container -- the returned Span aliases the JS buffer's own
// storage and is valid only for the duration of the call site (per
// JsBindings.md), since a GC can move or (for a resizable/detachable
// buffer) invalidate the underlying storage afterward.
//
// Deliberately narrower than "any ArrayBufferView": JsBindings.md's
// Public API doc comment names exactly "a JS Uint8Array/ArrayBuffer",
// not every typed array element-type variant (Int16Array,
// Float64Array, etc.) -- so a caller passing e.g. a Float64Array gets
// InvalidArgument here rather than this function silently reinterpreting
// its bytes as raw uint8_t, which nothing in the frozen spec asked for.
static forge::core::Result<forge::core::Span<const forge::core::u8>> AsByteSpan(
JSContext* cx, JS::HandleValue value) {
using ByteSpan = forge::core::Span<const forge::core::u8>;
if (!value.isObject()) {
return forge::core::Result<ByteSpan>(
forge::core::Failure{forge::core::Error(forge::core::ErrorCode::InvalidArgument)});
}
JS::RootedObject obj(cx, &value.toObject());
if (JS_IsUint8Array(obj)) {
JS::AutoCheckCannotGC nogc(cx);
bool isSharedMemory = false;
uint8_t* data = JS_GetUint8ArrayData(obj, &isSharedMemory, nogc);
const size_t length = JS_GetTypedArrayByteLength(obj);
if (isSharedMemory || (length > 0 && data == nullptr)) {
// A SharedArrayBuffer-backed view could be mutated by another
// thread concurrently with whatever synchronous I/O the caller is
// about to do with this span -- not safe to alias directly. A
// null data pointer with nonzero length means a detached backing
// buffer -- nothing valid to read.
return forge::core::Result<ByteSpan>(
forge::core::Failure{forge::core::Error(forge::core::ErrorCode::InvalidOperation)});
}
return forge::core::Result<ByteSpan>(
ByteSpan(reinterpret_cast<const forge::core::u8*>(data), length));
}
if (JS::IsArrayBufferObject(obj)) {
if (JS::IsDetachedArrayBufferObject(obj)) {
return forge::core::Result<ByteSpan>(
forge::core::Failure{forge::core::Error(forge::core::ErrorCode::InvalidOperation)});
}
JS::AutoCheckCannotGC nogc(cx);
bool isSharedMemory = false;
uint8_t* data = JS::GetArrayBufferData(obj, &isSharedMemory, nogc);
const size_t length = JS::GetArrayBufferByteLength(obj);
// isSharedMemory is always false for a plain (non-Shared)
// ArrayBuffer per js/ArrayBuffer.h's own doc comment on
// JS::GetArrayBufferData -- checked anyway rather than assumed.
if (isSharedMemory) {
return forge::core::Result<ByteSpan>(
forge::core::Failure{forge::core::Error(forge::core::ErrorCode::InvalidOperation)});
}
return forge::core::Result<ByteSpan>(
ByteSpan(reinterpret_cast<const forge::core::u8*>(data), length));
}
return forge::core::Result<ByteSpan>(
forge::core::Failure{forge::core::Error(forge::core::ErrorCode::InvalidArgument)});
}
//==============================================================================
// Phase 7.2 smoke tests -- run via `forge --self-test` (see main()'s CLI
// dispatch below). Exercises each of the six marshalling helpers above
// against a real, live JSContext/Realm -- the one verification step the
// sandbox this was implemented in genuinely could not do (no real
// js/public build graph available there; see the block comment above and
// HISTORY.md's Phase 7.2 entry for what *was* done there). Not
// JS-visible, not part of Fs.md's surface -- a self-contained internal
// diagnostic for this phase, expected to be complemented (not replaced)
// by real fs.*Sync-driven coverage once Phase 7.3 wires these up.
//==============================================================================
// Evaluates a small JS expression and returns its value. Reuses the exact
// JS::SourceText<mozilla::Utf8Unit>/JS::CompileOptions/JS::Evaluate shape
// main() already uses for real script files below, just against an
// in-memory literal instead of a file -- lets the self-tests construct
// arbitrary JS values (a Symbol, a Uint8Array, an ArrayBuffer, ...) for
// the marshalling helpers to operate on. An expression that throws (e.g.
// `Symbol('x')` fed to something expecting ToString to succeed) leaves
// that exception pending for the caller to handle -- not swallowed here.
static bool EvaluateExpression(JSContext* cx, const char* expr,
JS::MutableHandleValue rval) {
JS::SourceText<mozilla::Utf8Unit> src;
if (!src.init(cx, expr, strlen(expr), JS::SourceOwnership::Borrowed)) {
return false;
}
JS::CompileOptions opts(cx);
opts.setFileAndLine("<self-test>", 1);
return JS::Evaluate(cx, opts, src, rval);
}
static bool SelfTest_ToForgeStringFromLiteral(JSContext* cx) {
JS::RootedValue val(cx);
if (!EvaluateExpression(cx, "'hello world'", &val)) {
fprintf(stderr, " FAIL ToForgeString(literal): evaluate failed\n");
JS_ClearPendingException(cx);
return false;
}
forge::core::Result<forge::core::String> result = ToForgeString(cx, val);
if (result.HasError()) {
fprintf(stderr, " FAIL ToForgeString(literal): unexpected error\n");
return false;
}
if (result.Value().View() != forge::core::StringView("hello world")) {
fprintf(stderr, " FAIL ToForgeString(literal): content mismatch\n");
return false;
}
return true;