swjs_call_function_no_catch corrupts an Emscripten callee's WASM function table on the second invocation
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Cần làm rõ
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- javascript, swift, wasm
- Lĩnh vực
- web-dev
Hướng nghiên cứu
Chạy viz-repro.html được đính kèm và so sánh đường dẫn H bị lỗi với các trường hợp J và K chạy thành công. Sau đó kiểm tra Package/runtime.js xung quanh swjs_call_function_no_catch ở dòng 573, tập trung vào việc xử lý các đối số và giá trị trả về. Hoàn thành có nghĩa là xác định trạng thái bridge hoặc tương tác khiến lần gọi thứ hai gây ra trap, đồng thời ghi lại hoặc xác thực một bản sửa lỗi phù hợp.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
When Swift WASM (via JavaScriptKit) calls a JavaScript function that internally invokes an Emscripten-built WASM module (@viz-js/viz), the second such call on the same Emscripten instance traps inside the Emscripten callee with RuntimeError: table index is out of bounds. The trap fires inside the callee's WASM, but only when the caller is JavaScriptKit; the same JS function called the same number of times from plain JavaScript works perfectly. Yielding to the JS event loop between calls does not reliably help — the corruption is per-Emscripten-instance, not just timing-dependent. Allocating a fresh Emscripten instance for each Swift-originated call avoids the trap entirely.
This is reproducible with an 11-test differential matrix that isolates the trigger to the swjs_call_function_no_catch path specifically.
Environment
- JavaScriptKit: as bundled with Swift 6.3 / BridgeJS plugin (current)
@viz-js/viz: 3.26.0 (Graphviz 14.1.5, Emscripten-built)- Browser: Chrome (latest), macOS 15.4
runtime.jsreferenced below is BridgeJS-generatedPackage/runtime.js
Failing stack
RuntimeError: table index is out of bounds
at wasm://wasm/...:wasm-function[1006]:0x5742f
at wasm://wasm/...:wasm-function[160]:0x9cad
at wasm://wasm/...:wasm-function[1097]:0x619e2
at wasm://wasm/...:wasm-function[2197]:0xc45bc
at Object.ccall (viz.js:9)
at renderInput (viz.js:135)
at Viz.render (viz.js:308)
at Viz.renderString (viz.js:318)
at window.graphvizLayoutJSON (vgraph-v1.0.1.js:218)
at swjs_call_function_no_catch (Package/runtime.js:573)
The trap is inside viz-js's WASM linear memory / function table, but it only fires when the call frame above is swjs_call_function_no_catch. Identical JavaScript code, identical viz-js instance, identical input DOT — fails when called from Swift WASM, passes when called from JS.
Differential test matrix
A single HTML page (viz-repro.html, source linked below) runs eleven variants of "render a small graph" against the same viz-js instance, varying only how the call reaches it.
| Test | What it varies | Result |
|---|---|---|
| A | Two viz.renderString calls from plain JS, same DOT |
PASS |
| B | Two calls from JS, second DOT has shuffled attribute order | PASS |
| C | Two calls from JS, DOT contains fontname (no font in WASM) |
PASS |
| D | Two calls from JS, with a parallel instance() allocation in flight |
PASS |
| E | Two calls from JS in one synchronous turn (no microtask between) | PASS |
| F | Same as E, but routed through wrapper code that dispatches via window.graphvizLayoutJSON |
PASS |
| G | Full Swift WASM bundle initialised (BridgeJS exports loaded), then JS-only viz calls | PASS |
| H | Two Swift→JS→viz calls on the same instance, same DOT, same turn |
FAIL |
| I | Same as H + await new Promise(r => setTimeout(r, 0)) between calls |
UNRELIABLE — depends on whether a background instance() allocation happens to resolve in time |
| J | One Swift→viz call, then one JS-direct viz call on the same instance | PASS — confirms the viz instance still answers JS callers correctly |
| K | Two Swift→viz calls with 500 ms yield (ensures a fresh instance is swapped in) | PASS — confirms the bug is per-instance, not per-call-accumulating |
The matrix isolates the trigger to the second swjs_call_function_no_catch invocation that ends up calling the same Emscripten instance. JS callers cannot reproduce it under any input or timing variation; Swift callers reproduce it deterministically.
J is particularly informative: after a Swift→viz call, the same viz instance still serves a JS-direct call correctly. So the viz-js heap is not globally corrupted — only the Swift-bridge call path on that instance is broken. K confirms that allocating a fresh viz instance per Swift call avoids the trap entirely.
Hypothesis
I don't have enough JavaScriptKit internals knowledge to call the cause. What we observe is consistent with swjs_call_function_no_catch leaving residue in JS-side state that the JS function's next execution path through the Emscripten callee interacts with — possibly something around how the JSObject reference table is reused, how return values from JS-into-Emscripten are held, or how the underlying WebAssembly.Table imports are shared between modules. The fact that JS-direct calls don't reproduce, while two Swift bridge calls do, suggests it's specific to the bridge mechanics rather than the Emscripten callee's own state machine.
Reproducer
Self-contained ZIP attached: viz-repro.zip (1.9 MB).
To run:
unzip viz-repro.zip
cd viz-repro-bundle
python3 -m http.server 8000
Then open http://localhost:8000/viz-repro.html and click each lettered button. H reliably fails; A–G, J, K reliably pass; I is timing-dependent.
The ZIP includes the pre-built Swift WASM bundle (Package/VGraphWasm.wasm + runtime.js) so no Swift toolchain or build step is needed. @viz-js/viz and @bjorn3/browser_wasi_shim are loaded from jsDelivr.
I have not tested whether other Emscripten-built WASM modules (e.g. @hpcc-js/wasm) exhibit the same behaviour. Happy to test if it would help narrow scope.
Workaround
Maintain a pool of pre-allocated viz-js instances and consume a fresh one per Swift→viz call (refill async). Pool size N tolerates up to N Swift→viz calls per event-loop turn. This is shipping in our codebase now, but it's a heavy hammer — eagerly allocating 5+ Emscripten WASM instances at startup for a workaround that should not be necessary.
Ask
- Is this a known interaction pattern between
swjs_call_function_no_catchand Emscripten-built callees, or a fresh report? - If the call mechanism is leaving recoverable state in the Emscripten module, is there a way to flush / reset it from the Swift side without disposing the whole instance?
- Would you accept a PR adding an "after-call-cleanup hook" in JavaScriptKit's bridge, or is the right answer something Emscripten-side?
Happy to dig further into the JavaScriptKit runtime if you can point me at the most likely files. The smoking gun is somewhere in how swjs_call_function_no_catch (runtime.js:573) marshals arguments and return values when the called JS function calls into another WASM module.
- Ngôn ngữ chính
- Swift
- Star
- 986
- Fork
- 76
- Merge trung bình
- 21 giờ 11 phút
- Pull request đã merge (30 ngày)
- 4
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của swiftwasm/JavaScriptKit
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 55/100
swiftwasm/JavaScriptKit#793 ·
-
swiftwasm/JavaScriptKit#785 · 1 người được giao ·
-
[SwiftBuild] Preserve duplicate archive members when SwiftBuild expands static archives for linking Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
swiftwasm/JavaScriptKit#781 · 3 bình luận ·
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 25/100
swiftwasm/JavaScriptKit#773 ·
-
swiftwasm/JavaScriptKit#767 · 1 người được giao ·
Tất cả issue của swiftwasm/JavaScriptKit
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
manaflow-ai/cmux#13763 ·
-
type: feature request
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
googleapis/google-cloud-swift#1036 · 1 bình luận ·
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 78/100
wultra/mtoken-sdk-ios#253 ·
-
The correction-threshold rationale says 17 restraint cases collide; the current corpus has 16 Đang mởarea:dictation documentation P2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
uttrflow/uttrflow-swift#1180 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100