diff --git a/changelog.d/8621-web-textfield-callback-strings.md b/changelog.d/8621-web-textfield-callback-strings.md new file mode 100644 index 0000000000..d4f976add0 --- /dev/null +++ b/changelog.d/8621-web-textfield-callback-strings.md @@ -0,0 +1,6 @@ +### Fixed + +- Web `TextField`, `SecureField`, and `TextArea` callbacks now receive their + entered text as strings instead of numeric `NaN` values. The WASM closure + bridge converts browser values directly to its BigInt ABI without an + intermediate NaN-boxed JavaScript number. (#8584) diff --git a/crates/perry-codegen-wasm/src/lib.rs b/crates/perry-codegen-wasm/src/lib.rs index f10738cd11..7f02b0fa10 100644 --- a/crates/perry-codegen-wasm/src/lib.rs +++ b/crates/perry-codegen-wasm/src/lib.rs @@ -101,3 +101,25 @@ fn html_escape(s: &str) -> String { .replace('>', ">") .replace('"', """) } + +#[cfg(test)] +mod tests { + use super::WASM_RUNTIME_JS; + + #[test] + fn text_input_callbacks_pass_plain_js_strings_to_wasm_closures() { + assert_eq!( + WASM_RUNTIME_JS + .matches("callWasmClosure(el._perryCallback, el.value)") + .count(), + 3, + "TextField, SecureField, and TextArea must pass their DOM strings directly" + ); + assert!( + !WASM_RUNTIME_JS + .lines() + .any(|line| { line.contains("callWasmClosure") && line.contains("fromJsValue") }), + "callWasmClosure owns JS-value conversion; callers must not pre-box arguments" + ); + } +} diff --git a/crates/perry-codegen-wasm/src/wasm_runtime.js b/crates/perry-codegen-wasm/src/wasm_runtime.js index 8da1518437..6acd37265d 100644 --- a/crates/perry-codegen-wasm/src/wasm_runtime.js +++ b/crates/perry-codegen-wasm/src/wasm_runtime.js @@ -2603,6 +2603,8 @@ function uiGet(h) { return uiHandles.get(h); } // Helper: call a WASM closure — accepts either a raw NaN-boxed f64 handle, // or a JS closure object ({funcIdx, captures}) from toJsValue conversion. +// Extra args must be plain JS values: this function owns their one conversion +// to i64 bits. Pre-boxing a string as f64 lets JS canonicalize its NaN payload. // WASM functions use i64 (BigInt) params/returns. function callWasmClosure(closureVal, ...extraArgs) { let closure; @@ -2766,7 +2768,7 @@ function perry_ui_textfield_create(placeholder, callback) { el.placeholder = placeholder || ""; el._perryCallback = callback; el.addEventListener("input", () => { - if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, fromJsValue(el.value)); + if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.value); }); return uiAlloc(el); } @@ -2775,7 +2777,7 @@ function perry_ui_securefield_create(placeholder, callback) { el.placeholder = placeholder || ""; el._perryCallback = callback; el.addEventListener("input", () => { - if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, fromJsValue(el.value)); + if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.value); }); return uiAlloc(el); } @@ -2935,7 +2937,7 @@ function perry_ui_textarea_create(placeholder, callback) { el.placeholder = placeholder || ""; el._perryCallback = callback; el.addEventListener("input", () => { - if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, fromJsValue(el.value)); + if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.value); }); return uiAlloc(el); } @@ -3164,7 +3166,7 @@ function perry_ui_state_get(h) { return uiStateGet(h); } function perry_ui_state_set(h, v) { uiStateSet(h, v); } function perry_ui_state_on_change(stateH, callback) { const s = uiStates.get(stateH); - if (s) s.subscribers.push((val) => callWasmClosure(callback, fromJsValue(val))); + if (s) s.subscribers.push((val) => callWasmClosure(callback, val)); } function perry_ui_state_bind_text(stateH, widgetH) { const el = uiGet(widgetH), s = uiStates.get(stateH); @@ -3585,13 +3587,13 @@ function perry_ui_clipboard_write(text) { try { navigator.clipboard.writeText(te // ---------- Dialog ---------- function perry_ui_open_file_dialog(callback) { const input = document.createElement("input"); input.type = "file"; - input.addEventListener("change", () => { if (input.files.length) callWasmClosure(callback, fromJsValue(input.files[0].name)); }); + input.addEventListener("change", () => { if (input.files.length) callWasmClosure(callback, input.files[0].name); }); input.click(); } function perry_ui_open_folder_dialog(callback) { perry_ui_open_file_dialog(callback); } function perry_ui_save_file_dialog(callback, defaultName) { const name = prompt("Save as:", defaultName || "file.txt"); - if (name) callWasmClosure(callback, fromJsValue(name)); + if (name) callWasmClosure(callback, name); } function perry_ui_alert(title, message, buttons, callback) { alert((title || "") + "\n" + (message || "")); @@ -3889,7 +3891,7 @@ function _perry_media_flush_state(handle) { const e = _perry_media_get(handle); if (!e || e.onStateChange === undefined || e.onStateChange === null) return; try { - callWasmClosure(e.onStateChange, fromJsValue(e.state)); + callWasmClosure(e.onStateChange, e.state); } catch (err) { console.warn("perry/media onStateChange threw:", err); } }