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
|
package wasm
import (
"context"
"errors"
"fmt"
"os"
"strings"
"sync"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/emscripten"
)
// ---------------------------------------------------------------------------
// EH panic sentinel
//
// All panics from EH host functions (__cxa_throw, __cxa_rethrow,
// __resumeException, __cxa_rethrow_primary_exception) use this type.
// callInvoke checks for it to distinguish normal EH throws from true fatal
// traps (abort, OOB, stack overflow, etc.) — the latter have no corresponding
// C++ exception in flight and must poison rt.fatalErr.
// ---------------------------------------------------------------------------
type ehPanic struct{ msg string }
func (e ehPanic) Error() string { return e.msg }
func newEHPanic(format string, args ...any) ehPanic {
return ehPanic{msg: fmt.Sprintf(format, args...)}
}
// ---------------------------------------------------------------------------
// EH debug logging — enabled by GONIX_EH_DEBUG=1
// ---------------------------------------------------------------------------
var (
ehDebugOnce sync.Once
ehDebugFile *os.File
)
func ehLog(format string, args ...any) {
ehDebugOnce.Do(func() {
if os.Getenv("GONIX_EH_DEBUG") != "" {
f, err := os.OpenFile("/tmp/gonix-eh.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err == nil {
ehDebugFile = f
}
}
})
if ehDebugFile != nil {
fmt.Fprintf(ehDebugFile, format+"\n", args...)
}
}
// ehTypeName tries to read the mangled type name from WASM memory.
// In wasm32 std::type_info layout: vtable ptr at [typePtr+0], name ptr at [typePtr+4].
func ehTypeName(mem api.Memory, typePtr uint32) string {
if typePtr == 0 || mem == nil {
return "?"
}
namePtr, ok := mem.ReadUint32Le(typePtr + 4)
if !ok || namePtr == 0 {
return fmt.Sprintf("0x%x", typePtr)
}
buf := make([]byte, 128)
for i := range buf {
b, ok := mem.ReadByte(namePtr + uint32(i))
if !ok || b == 0 {
return string(buf[:i])
}
buf[i] = b
}
return string(buf)
}
// ---------------------------------------------------------------------------
// C++ exception state (Emscripten EH with -fexceptions)
//
// The Emscripten EH ABI stores exception metadata in WASM linear memory in a
// __cxa_exception struct immediately before the exception object pointer:
//
// struct __cxa_exception { // total 24 bytes (wasm32)
// uint32_t referenceCount; // [ptr-24] +0
// uint32_t exceptionType; // [ptr-20] +4 ← type_info ptr
// uint32_t exceptionDestructor; // [ptr-16] +8
// uint8_t caught; // [ptr-12] +12
// uint8_t rethrown; // [ptr-11] +13
// uint8_t _pad[2]; // [ptr-10] +14
// uint32_t adjustedPtr; // [ptr-8] +16
// uint32_t _pad2; // [ptr-4] +20
// }; // ptr = object start = struct_base+24
//
// We read/write this struct directly in WASM linear memory rather than
// maintaining Go-side copies, exactly as the Emscripten JS runtime does.
//
// Protocol:
// __cxa_throw(ptr, type, dtor)
// → writes type/dtor into struct at ptr-24, sets g_exceptionLast = ptr
// → WASM emits `unreachable` after the call (dead code marker)
// → invoke_* wrapper catches the resulting trap, calls setThrew(1,0)
//
// __cxa_find_matching_catch_N(clauseType1, clauseType2, ...)
// → reads thrownType from WASM memory at g_exceptionLast-20
// → calls __cxa_can_catch for each clause type to find a match
// → sets tempret to the matching clause type pointer (or 0 for catch-all)
// → returns g_exceptionLast
//
// __cxa_begin_catch(ptr)
// → marks caught=1 in WASM struct, pushes ptr onto g_exceptionCaught
// → returns adjustedPtr from WASM struct
//
// __cxa_end_catch()
// → pops g_exceptionCaught; if refcount hits zero, frees exception
//
// __resumeException(ptr)
// → sets g_exceptionLast = ptr (does NOT call setThrew — invoke_* does that)
// ---------------------------------------------------------------------------
// __cxa_exception field offsets relative to the exception object pointer.
const (
cxaOffType = -20 // exceptionType (i32)
cxaOffDestructor = -16 // exceptionDestructor (i32)
cxaOffCaught = -12 // caught (i8)
cxaOffRethrown = -11 // rethrown (i8)
cxaOffAdjustedPtr = -8 // adjustedPtr (i32)
)
// EH state — mirrors the Emscripten JS runtime globals exactly.
// Protected by g_ehMu (single-threaded WASM, but Go host may be called
// from multiple goroutines in future).
var (
g_ehMu sync.Mutex
g_exceptionLast uint32 // exceptionLast — ptr of last thrown exception
g_exceptionCaught []uint32 // exceptionCaught — stack of active catch ptrs
g_uncaughtExceptionCount int // uncaughtExceptionCount
)
// EHCatchDepth returns the current depth of the g_exceptionCaught stack.
// After a fully-handled exception chain the depth must be 0.
// Exposed for testing; not safe to call concurrently with WASM execution.
func EHCatchDepth() int {
g_ehMu.Lock()
defer g_ehMu.Unlock()
return len(g_exceptionCaught)
}
// ehReadType reads the exceptionType field from WASM linear memory.
func ehReadType(mem api.Memory, ptr uint32) uint32 {
if ptr == 0 {
return 0
}
v, ok := mem.ReadUint32Le(ptr - 20)
if !ok {
return 0
}
return v
}
// instantiateEnv registers the "env" host module and a private "_invoke"
// trampoline module.
//
// The "_invoke" trampoline holds wazero's own emscripten invoke_* functions,
// which correctly call WASM-defined table entries via LookupFunction. Our
// "env" invoke_* handlers intercept calls to host-imported table entries
// (where LookupFunction would crash) and fall through to the trampoline for
// everything else.
func (rt *Runtime) instantiateEnv(ctx context.Context, compiled wazero.CompiledModule) error {
// ------------------------------------------------------------------
// Step A: build the "_invoke" trampoline module.
// ------------------------------------------------------------------
emscriptenExporter, err := emscripten.NewFunctionExporterForModule(compiled)
if err != nil {
return fmt.Errorf("emscripten exporter: %w", err)
}
trampolineBuilder := rt.r.NewHostModuleBuilder("_invoke")
emscriptenExporter.ExportFunctions(trampolineBuilder)
trampolineCompiled, err := trampolineBuilder.Compile(ctx)
if err != nil {
return fmt.Errorf("compile _invoke trampoline: %w", err)
}
trampolineMod, err := rt.r.InstantiateModule(ctx, trampolineCompiled, wazero.NewModuleConfig().WithName("_invoke"))
if err != nil {
return fmt.Errorf("instantiate _invoke trampoline: %w", err)
}
// Cache trampoline GoModuleFunction values via ExportedFunctionDefinitions
// (safe for host modules; does not call Engine.NewFunction).
rt.trampolineFns = map[string]api.GoModuleFunction{}
trampolineDefs := trampolineMod.ExportedFunctionDefinitions()
for _, fn := range compiled.ImportedFunctions() {
importMod, name, isImport := fn.Import()
if !isImport || importMod != "env" || !invokeVariantNeeded(name) {
continue
}
def, ok := trampolineDefs[name]
if !ok {
continue
}
goFn, ok := def.GoFunction().(api.GoModuleFunction)
if !ok {
continue
}
rt.trampolineFns[name] = goFn
}
_ = trampolineMod
// ------------------------------------------------------------------
// Step B: build the "env" host module.
// ------------------------------------------------------------------
b := rt.r.NewHostModuleBuilder("env")
rt.envHandlers = map[string]api.GoModuleFunc{}
// helper: register a handler, wrap with counter, cache unwrapped handler
reg := func(name string, fn api.GoModuleFunc, params, results []api.ValueType) {
rt.envHandlers[name] = fn
counter := rt.counterFor(name)
wrapped := api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
counter.Add(1)
fn.Call(ctx, mod, stack)
})
b.NewFunctionBuilder().WithGoModuleFunction(wrapped, params, results).Export(name)
}
// Register our own invoke_* functions.
for _, fn := range compiled.ImportedFunctions() {
importMod, name, isImport := fn.Import()
if !isImport || importMod != "env" || !invokeVariantNeeded(name) {
continue
}
invokeName := name
params := fn.ParamTypes()
results := fn.ResultTypes()
handler := api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
rt.callInvoke(ctx, mod, invokeName, stack)
})
reg(invokeName, handler, params, results)
}
// -----------------------------------------------------------------------
// Thread stubs (no-ops — single-threaded WASM).
// -----------------------------------------------------------------------
noop := api.GoModuleFunc(func(_ context.Context, _ api.Module, _ []uint64) {})
for _, export := range []struct {
name string
params []api.ValueType
}{
{"_emscripten_thread_set_strongref", []api.ValueType{api.ValueTypeI32}},
{"_emscripten_init_main_thread_js", []api.ValueType{api.ValueTypeI32}},
{"_emscripten_thread_mailbox_await", []api.ValueType{api.ValueTypeI32}},
{"_emscripten_thread_cleanup", []api.ValueType{api.ValueTypeI32}},
{"_emscripten_notify_mailbox_postmessage", []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}},
{"emscripten_exit_with_live_runtime", nil},
{"emscripten_check_blocking_allowed", nil},
{"emscripten_notify_memory_growth", []api.ValueType{api.ValueTypeI32}},
} {
e := export
reg(e.name, noop, e.params, nil)
}
reg("emscripten_num_logical_cores",
api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = 1 }),
nil, []api.ValueType{api.ValueTypeI32})
reg("_emscripten_receive_on_main_thread_js",
api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = 0 }),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32},
[]api.ValueType{api.ValueTypeF64})
// -----------------------------------------------------------------------
// Syscall stubs.
// -----------------------------------------------------------------------
reg("__syscall_getcwd",
api.GoModuleFunc(func(_ context.Context, m api.Module, stack []uint64) {
buf := api.DecodeU32(stack[0])
size := api.DecodeU32(stack[1])
if size >= 2 {
m.Memory().WriteByte(buf, '/')
m.Memory().WriteByte(buf+1, 0)
stack[0] = api.EncodeI32(2)
} else {
stack[0] = api.EncodeI32(-28) // -ENOSPC
}
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32},
[]api.ValueType{api.ValueTypeI32})
reg("__syscall_poll",
api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = 0 }),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32},
[]api.ValueType{api.ValueTypeI32})
// -----------------------------------------------------------------------
// C++ exception ABI
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// C++ exception ABI — mirrors the Emscripten JS runtime (libexceptions.js)
// exactly. Variable names match the JS source.
// -----------------------------------------------------------------------
// __cxa_throw(ptr, type, destructor)
// JS: info.init(type,destructor); exceptionLast=ptr; uncaughtExceptionCount++; throw ptr
//
// We panic after setting state so that wazero traps before hitting the
// `unreachable` dead-code marker Emscripten emits after every __cxa_throw
// call site. The panic propagates up through any WASM frames to the
// enclosing invoke_* handler (callInvoke), which catches it via
// callGoModuleFunc's recover, then calls setThrew(1,0) so the WASM EH
// state machine branches to the catch block.
//
// This mirrors the JS runtime where __cxa_throw throws a JS exception that
// unwinds through the JS stack — it never returns to its caller.
reg("__cxa_throw",
api.GoModuleFunc(func(_ context.Context, m api.Module, stack []uint64) {
ptr := api.DecodeU32(stack[0])
typ := api.DecodeU32(stack[1])
dtor := api.DecodeU32(stack[2])
mem := m.Memory()
// ExceptionInfo.init(type, destructor): set_adjusted_ptr(0), set_type, set_destructor
mem.WriteUint32Le(ptr-8, 0) // adjustedPtr = 0
mem.WriteUint32Le(ptr-20, typ) // exceptionType
mem.WriteUint32Le(ptr-16, dtor) // exceptionDestructor
g_ehMu.Lock()
prev := g_exceptionLast
depth := len(g_exceptionCaught)
g_exceptionLast = ptr
g_uncaughtExceptionCount++
g_ehMu.Unlock()
ehLog("THROW ptr=0x%x type=0x%x(%s) dtor=0x%x prev=0x%x catch_depth=%d uncaught=%d",
ptr, typ, ehTypeName(mem, typ), dtor, prev, depth, g_uncaughtExceptionCount)
// Panic so wazero traps before the unreachable — invoke_* catches this.
panic(newEHPanic("__cxa_throw: ptr=0x%x type=0x%x", ptr, typ))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32}, nil)
// __resumeException(ptr)
// JS: if (!exceptionLast) { exceptionLast = ptr }; throw exceptionLast
//
// This never returns in the JS runtime — it throws a JS exception.
// We panic with ehPanic so wazero traps before the unreachable dead-code
// marker, and invoke_* catches it via setThrew.
reg("__resumeException",
api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) {
ptr := api.DecodeU32(stack[0])
g_ehMu.Lock()
if g_exceptionLast == 0 {
g_exceptionLast = ptr
}
cur := g_exceptionLast
g_ehMu.Unlock()
ehLog("RESUME ptr=0x%x cur=0x%x", ptr, cur)
panic(newEHPanic("__resumeException: ptr=0x%x", cur))
}),
[]api.ValueType{api.ValueTypeI32}, nil)
// llvm_eh_typeid_for(type) → type (identity function)
// JS: llvm_eh_typeid_for: (type) => type
reg("llvm_eh_typeid_for",
api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) {
// stack[0] in = typeInfoPtr, stack[0] out = same value (identity)
}),
[]api.ValueType{api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
// findMatchingCatch(args) — shared implementation for all __cxa_find_matching_catch_N.
// JS:
// var thrown = exceptionLast
// if (!thrown) { setTempRet0(0); return 0 }
// info.set_adjusted_ptr(thrown)
// var thrownType = info.get_type()
// if (!thrownType) { setTempRet0(0); return thrown }
// for caughtType of args:
// if caughtType===0 || caughtType===thrownType: break // catch-all or exact
// adjusted_ptr_addr = info.ptr + 16 (= ptr-24+16 = ptr-8)
// if ___cxa_can_catch(caughtType, thrownType, adjusted_ptr_addr):
// setTempRet0(caughtType); return thrown
// setTempRet0(thrownType) // ← always thrownType if no typed match
// return thrown
findMatchingCatch := func(nTypeParams int) api.GoModuleFunc {
return api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
g_ehMu.Lock()
thrown := g_exceptionLast
g_ehMu.Unlock()
tempretSet := rt.mod.ExportedFunction("_emscripten_tempret_set")
setTempRet0 := func(val uint32) {
if tempretSet != nil {
tempretSet.Call(ctx, uint64(val)) //nolint:errcheck
}
}
if thrown == 0 {
setTempRet0(0)
stack[0] = 0
ehLog("FIND_CATCH thrown=0 → 0")
return
}
mem := mod.Memory()
// info.set_adjusted_ptr(thrown)
mem.WriteUint32Le(thrown-8, thrown)
// thrownType = info.get_type()
thrownType := ehReadType(mem, thrown)
if thrownType == 0 {
setTempRet0(0)
stack[0] = uint64(thrown)
ehLog("FIND_CATCH thrown=0x%x thrownType=0 → pass through", thrown)
return
}
canCatch := rt.mod.ExportedFunction("__cxa_can_catch")
// log clause types before potentially modifying stack
clauses := make([]string, nTypeParams)
clauseVals := make([]uint32, nTypeParams)
for i := 0; i < nTypeParams; i++ {
clauseVals[i] = api.DecodeU32(stack[i])
clauses[i] = fmt.Sprintf("0x%x(%s)", clauseVals[i], ehTypeName(mem, clauseVals[i]))
}
for i := 0; i < nTypeParams; i++ {
caughtType := clauseVals[i]
if caughtType == 0 || caughtType == thrownType {
// catch-all or exact match — break, fall through to setTempRet0(thrownType)
break
}
// adjusted_ptr_addr = info.ptr + adjustedPtr_offset = (thrown-24) + 16 = thrown-8
adjustedPtrAddr := thrown - 8
if canCatch != nil {
res, err := canCatch.Call(ctx,
uint64(caughtType), uint64(thrownType), uint64(adjustedPtrAddr))
if err == nil && len(res) > 0 && res[0] != 0 {
ehLog("FIND_CATCH thrown=0x%x thrownType=0x%x(%s) clauses=%v → selector=0x%x (can_catch)",
thrown, thrownType, ehTypeName(mem, thrownType), clauses, caughtType)
setTempRet0(caughtType)
stack[0] = uint64(thrown)
return
}
}
}
// No typed match (or catch-all / exact): setTempRet0(thrownType)
ehLog("FIND_CATCH thrown=0x%x thrownType=0x%x(%s) clauses=%v → selector=thrownType",
thrown, thrownType, ehTypeName(mem, thrownType), clauses)
setTempRet0(thrownType)
stack[0] = uint64(thrown)
})
}
oneI32 := []api.ValueType{api.ValueTypeI32}
reg("__cxa_find_matching_catch_2", findMatchingCatch(0), nil, oneI32)
for _, spec := range []struct {
suffix string
n int
}{{"_3", 1}, {"_4", 2}, {"_5", 3}, {"_6", 4}} {
params := make([]api.ValueType, spec.n)
for i := range params {
params[i] = api.ValueTypeI32
}
reg("__cxa_find_matching_catch"+spec.suffix, findMatchingCatch(spec.n), params, oneI32)
}
// __cxa_begin_catch(ptr) → get_exception_ptr(ptr)
// JS:
// if (!info.get_caught()) { info.set_caught(true); uncaughtExceptionCount-- }
// info.set_rethrown(false)
// exceptionCaught.push(info)
// ___cxa_increment_exception_refcount(ptr)
// return ___cxa_get_exception_ptr(ptr)
reg("__cxa_begin_catch",
api.GoModuleFunc(func(ctx context.Context, m api.Module, stack []uint64) {
ptr := api.DecodeU32(stack[0])
if ptr != 0 {
mem := m.Memory()
caught, _ := mem.ReadByte(ptr - 12)
if caught == 0 {
mem.WriteByte(ptr-12, 1) // set_caught(true)
g_ehMu.Lock()
g_uncaughtExceptionCount--
g_ehMu.Unlock()
}
mem.WriteByte(ptr-11, 0) // set_rethrown(false)
g_ehMu.Lock()
g_exceptionCaught = append(g_exceptionCaught, ptr)
depth := len(g_exceptionCaught)
g_ehMu.Unlock()
ehLog("BEGIN_CATCH ptr=0x%x depth=%d uncaught=%d", ptr, depth, g_uncaughtExceptionCount)
if rt.mod != nil {
if f := rt.mod.ExportedFunction("__cxa_increment_exception_refcount"); f != nil {
f.Call(ctx, uint64(ptr)) //nolint:errcheck
}
}
}
// return ___cxa_get_exception_ptr(ptr) — WASM-exported, returns adjustedPtr
if ptr != 0 && rt.mod != nil {
if f := rt.mod.ExportedFunction("__cxa_get_exception_ptr"); f != nil {
res, err := f.Call(ctx, uint64(ptr))
if err == nil && len(res) > 0 {
stack[0] = res[0]
return
}
}
}
stack[0] = uint64(ptr)
}),
[]api.ValueType{api.ValueTypeI32}, oneI32)
// __cxa_end_catch()
// JS:
// _setThrew(0, 0) ← CRITICAL: clears __THREW__
// var info = exceptionCaught.pop()
// ___cxa_decrement_exception_refcount(info.excPtr)
// exceptionLast = 0
reg("__cxa_end_catch",
api.GoModuleFunc(func(ctx context.Context, _ api.Module, _ []uint64) {
// Clear __THREW__ — this is the most critical step.
// Without it the WASM code after the catch block still sees __THREW__==1.
if rt.mod != nil {
if f := rt.mod.ExportedFunction("setThrew"); f != nil {
f.Call(ctx, 0, 0) //nolint:errcheck
}
}
g_ehMu.Lock()
var ptr uint32
if n := len(g_exceptionCaught); n > 0 {
ptr = g_exceptionCaught[n-1]
g_exceptionCaught = g_exceptionCaught[:n-1]
}
g_exceptionLast = 0
depth := len(g_exceptionCaught)
g_ehMu.Unlock()
ehLog("END_CATCH ptr=0x%x remaining_depth=%d", ptr, depth)
if ptr != 0 && rt.mod != nil {
if f := rt.mod.ExportedFunction("__cxa_decrement_exception_refcount"); f != nil {
f.Call(ctx, uint64(ptr)) //nolint:errcheck
}
}
}),
nil, nil)
// __cxa_rethrow()
// JS:
// var info = exceptionCaught.pop()
// if (!info.get_rethrown()) { exceptionCaught.push(info); info.set_rethrown(true); info.set_caught(false); uncaughtExceptionCount++ }
// exceptionLast = ptr; throw ptr
//
// Like __cxa_throw, this never returns — we panic so wazero traps before
// the unreachable dead-code marker, and invoke_* catches it via setThrew.
reg("__cxa_rethrow",
api.GoModuleFunc(func(_ context.Context, m api.Module, _ []uint64) {
g_ehMu.Lock()
n := len(g_exceptionCaught)
if n == 0 {
g_ehMu.Unlock()
ehLog("RETHROW — no exception on stack!")
panic(fmt.Errorf("__cxa_rethrow: no exception on stack"))
}
ptr := g_exceptionCaught[n-1]
g_exceptionCaught = g_exceptionCaught[:n-1]
mem := m.Memory()
rethrown, _ := mem.ReadByte(ptr - 11)
if rethrown == 0 {
g_exceptionCaught = append(g_exceptionCaught, ptr)
mem.WriteByte(ptr-11, 1) // set_rethrown(true)
mem.WriteByte(ptr-12, 0) // set_caught(false)
g_uncaughtExceptionCount++
}
g_exceptionLast = ptr
g_ehMu.Unlock()
ehLog("RETHROW ptr=0x%x", ptr)
panic(newEHPanic("__cxa_rethrow: ptr=0x%x", ptr))
}),
nil, nil)
// __cxa_rethrow_primary_exception(ptr)
// JS: if (!ptr) return; exceptionCaught.push(info); info.set_rethrown(true); ___cxa_rethrow()
//
// Called by std::rethrow_exception(exception_ptr). The WASM code for
// std::rethrow_exception is:
// call __cxa_rethrow_primary_exception(ptr)
// call std::terminate() ← fallthrough if above returns
// unreachable
//
// We must NOT return — we panic to abort execution at this point.
// The panic propagates up through the interpreter to the enclosing invoke_*
// callGoModuleFunc, which converts it to a callErr. callInvoke then sees
// that g_exceptionLast changed (we set it below) and calls setThrew(1,0),
// causing the WASM invoke wrapper to branch to the catch block instead of
// falling through to std::terminate.
//
// Stack-balance note: the JS implementation does push(info)+rethrow(), where
// rethrow() immediately pops (since rethrown is already set true). Net effect
// on g_exceptionCaught is zero — push then pop cancel. We replicate this by
// not touching g_exceptionCaught at all, only setting g_exceptionLast and the
// in-memory rethrown flag, then panicking just as __cxa_rethrow does.
reg("__cxa_rethrow_primary_exception",
api.GoModuleFunc(func(_ context.Context, m api.Module, stack []uint64) {
ptr := api.DecodeU32(stack[0])
if ptr == 0 {
return
}
g_ehMu.Lock()
g_exceptionLast = ptr
g_ehMu.Unlock()
m.Memory().WriteByte(ptr-11, 1) // set_rethrown(true)
ehLog("RETHROW_PRIMARY ptr=0x%x — panicking to prevent std::terminate fallthrough", ptr)
// Panic to abort execution here — this will be caught by callGoModuleFunc
// and turned into a callErr, then callInvoke will set setThrew(1,0).
panic(newEHPanic("__cxa_rethrow_primary_exception: rethrow ptr=0x%x", ptr))
}),
[]api.ValueType{api.ValueTypeI32}, nil)
// __cxa_current_primary_exception() → ptr
// JS: if (!exceptionCaught.length) return 0; info = exceptionCaught[last]; increment; return ptr
reg("__cxa_current_primary_exception",
api.GoModuleFunc(func(ctx context.Context, _ api.Module, stack []uint64) {
g_ehMu.Lock()
n := len(g_exceptionCaught)
var ptr uint32
if n > 0 {
ptr = g_exceptionCaught[n-1]
}
g_ehMu.Unlock()
if ptr != 0 && rt.mod != nil {
if f := rt.mod.ExportedFunction("__cxa_increment_exception_refcount"); f != nil {
f.Call(ctx, uint64(ptr)) //nolint:errcheck
}
}
stack[0] = uint64(ptr)
}),
nil, oneI32)
// __cxa_uncaught_exceptions() → uncaughtExceptionCount
// JS: () => uncaughtExceptionCount
reg("__cxa_uncaught_exceptions",
api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) {
g_ehMu.Lock()
n := g_uncaughtExceptionCount
g_ehMu.Unlock()
if n < 0 {
n = 0
}
stack[0] = uint64(n)
}),
nil, oneI32)
// lix_host_get_config(buf_ptr, max_len) → len
reg("lix_host_get_config",
api.GoModuleFunc(func(_ context.Context, m api.Module, stack []uint64) {
buf := api.DecodeU32(stack[0])
maxLen := api.DecodeU32(stack[1])
cfg := rt.nixConfig
n := uint32(len(cfg))
if n > maxLen {
n = maxLen
}
if n > 0 {
m.Memory().WriteString(buf, cfg[:n])
}
stack[0] = api.EncodeI32(int32(n))
}),
[]api.ValueType{api.ValueTypeI32, api.ValueTypeI32},
[]api.ValueType{api.ValueTypeI32})
if _, err := b.Instantiate(ctx); err != nil {
return fmt.Errorf("env: %w", err)
}
return nil
}
// callInvoke is the common body for all invoke_* handlers.
//
// Mirrors the Emscripten JS invoke pattern:
//
// sp = emscripten_stack_get_current()
// try { table[index](args...) } catch(e) {
// _emscripten_stack_restore(sp); setThrew(1, 0)
// }
func (rt *Runtime) callInvoke(ctx context.Context, mod api.Module, invokeName string, stack []uint64) {
if rt.mod == nil {
return
}
var sp [2]uint64
if f := rt.mod.ExportedFunction("emscripten_stack_get_current"); f != nil {
f.CallWithStack(ctx, sp[:]) //nolint:errcheck
}
tableOffset := uint32(stack[0])
copy(stack, stack[1:])
// Snapshot exceptionLast before the call so we can detect new throws.
g_ehMu.Lock()
exceptionLastBefore := g_exceptionLast
g_ehMu.Unlock()
var callErr error
if hostName, ok := rt.hostTableEntries[tableOffset]; ok {
rt.Counters.InvokeHost.Add(1)
callErr = rt.dispatchHostTableEntry(ctx, mod, hostName, stack)
} else {
rt.Counters.InvokeWasm.Add(1)
copy(stack[1:], stack[0:len(stack)-1])
stack[0] = uint64(tableOffset)
if goFn, ok := rt.trampolineFns[invokeName]; ok {
callErr = callGoModuleFunc(ctx, mod, goFn, stack)
} else {
callErr = fmt.Errorf("_invoke trampoline missing %s", invokeName)
}
}
if callErr != nil {
g_ehMu.Lock()
last := g_exceptionLast
depth := len(g_exceptionCaught)
g_ehMu.Unlock()
ehLog("INVOKE_THREW %s slot=%d err=%v exceptionLast=0x%x catch_depth=%d",
invokeName, tableOffset, callErr, last, depth)
if last == exceptionLastBefore {
// g_exceptionLast didn't change after the call. Two cases:
//
// (a) EH panic: __cxa_rethrow / __resumeException / etc. — these
// set g_exceptionLast to the *same* value (the current
// exception being propagated) and then panic. This is normal
// EH propagation, NOT a fatal trap.
//
// (b) True fatal trap: abort, OOB, stack overflow, unreachable
// (not from EH) — g_exceptionLast is genuinely unchanged
// because no C++ exception was in flight.
//
// Distinguish them by the panic type: EH panics use ehPanic;
// wazero wraps true traps in its own error type.
var ep ehPanic
if errors.As(callErr, &ep) {
// Normal EH propagation — do not poison fatalErr.
ehLog("INVOKE_THREW eh-propagate (no new exception, last=0x%x) err=%v", last, callErr)
} else {
ehLog("INVOKE_THREW fatal (no new exception thrown, last=0x%x) — setting threw", last)
if rt.fatalErr == nil {
rt.fatalErr = callErr
}
}
// Either way fall through to the EH protocol: stackRestore + setThrew.
}
// C++ exception throw — run the EH protocol: restore stack, set threw.
if f := rt.mod.ExportedFunction("_emscripten_stack_restore"); f != nil {
f.CallWithStack(ctx, sp[:]) //nolint:errcheck
}
if f := rt.mod.ExportedFunction("setThrew"); f != nil {
sp[0] = 1
sp[1] = 0
f.CallWithStack(ctx, sp[:]) //nolint:errcheck
}
}
}
// dispatchHostTableEntry calls the host function identified by importName
// directly, without going through wazero's function-table machinery.
// importName is either a bare "env" import name, or "lix_store:<name>" for
// lix_store module imports that appear in the function table.
func (rt *Runtime) dispatchHostTableEntry(ctx context.Context, mod api.Module, importName string, stack []uint64) error {
if invokeVariantNeeded(importName) {
rt.Counters.InvokeTotal.Add(1)
rt.Counters.InvokeHost.Add(1)
rt.callInvoke(ctx, mod, importName, stack)
return nil
}
// lix_store module imports: dispatch through the store call handler.
if strings.HasPrefix(importName, "lix_store:") {
// lix_store.call(reqPtr i32, reqLen i32, respPtr i32, respMax i32) → i32
reqPtr := api.DecodeU32(stack[0])
reqLen := api.DecodeU32(stack[1])
respPtr := api.DecodeU32(stack[2])
respMax := api.DecodeU32(stack[3])
resp := rt.dispatchStoreCall(ctx, mod, reqPtr, reqLen)
if uint32(len(resp)) > respMax {
resp = resp[:respMax]
}
if !mod.Memory().Write(respPtr, resp) {
stack[0] = api.EncodeI32(-1)
return nil
}
stack[0] = api.EncodeI32(int32(len(resp)))
return nil
}
fn, ok := rt.envHandlers[importName]
if !ok {
return fmt.Errorf("no handler registered for env import %s", importName)
}
rt.counterFor(importName).Add(1)
if importName == "__cxa_throw" {
rt.Counters.CxaThrow.Add(1)
}
fn.Call(ctx, mod, stack)
return nil
}
// callGoModuleFunc calls a GoModuleFunction, converting any panic to an error.
// The interpreter signals WASM traps (unreachable, OOB, etc.) via panic.
func callGoModuleFunc(ctx context.Context, mod api.Module, fn api.GoModuleFunction, stack []uint64) (retErr error) {
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
retErr = err
} else {
retErr = fmt.Errorf("wasm panic: %v", r)
}
}
}()
fn.Call(ctx, mod, stack)
return nil
}
// resetEHState resets the Go-side EH globals and the WASM __THREW__ flag.
// Call this after a top-level WASM call returns an error to ensure the runtime
// is in a clean state for the next call. This is a safety net for cases where
// a C++ exception escapes without going through the normal invoke_* EH protocol
// (e.g. when nix_parse_expr itself propagates an exception to its caller).
func (rt *Runtime) resetEHState(ctx context.Context) {
if rt.mod != nil {
if f := rt.mod.ExportedFunction("setThrew"); f != nil {
f.Call(ctx, 0, 0) //nolint:errcheck
}
}
g_ehMu.Lock()
g_exceptionLast = 0
g_exceptionCaught = g_exceptionCaught[:0]
g_uncaughtExceptionCount = 0
g_ehMu.Unlock()
ehLog("resetEHState: EH globals cleared")
}
// wazeroHostModuleBuilder is satisfied by wazero.HostModuleBuilder.
var _ wazero.HostModuleBuilder = (wazero.HostModuleBuilder)(nil)
|