webassembly/proxy_js: Reuse JsProxy ref if object matches.
Some checks failed
JavaScript code lint and formatting with Biome / eslint (push) Has been cancelled
Check code formatting / code-formatting (push) Has been cancelled
Check spelling with codespell / codespell (push) Has been cancelled
Build docs / build (push) Has been cancelled
Check examples / embedding (push) Has been cancelled
Package mpremote / build (push) Has been cancelled
.mpy file format and tools / test (push) Has been cancelled
Build ports metadata / build (push) Has been cancelled
alif port / build_alif (alif_ae3_build) (push) Has been cancelled
cc3200 port / build (push) Has been cancelled
esp32 port / build_idf (esp32_build_cmod_spiram_s2) (push) Has been cancelled
esp32 port / build_idf (esp32_build_s3_c3) (push) Has been cancelled
esp8266 port / build (push) Has been cancelled
mimxrt port / build (push) Has been cancelled
nrf port / build (push) Has been cancelled
powerpc port / build (push) Has been cancelled
qemu port / build_and_test_arm (bigendian) (push) Has been cancelled
qemu port / build_and_test_arm (sabrelite) (push) Has been cancelled
qemu port / build_and_test_arm (thumb) (push) Has been cancelled
qemu port / build_and_test_rv32 (push) Has been cancelled
renesas-ra port / build_renesas_ra_board (push) Has been cancelled
rp2 port / build (push) Has been cancelled
samd port / build (push) Has been cancelled
stm32 port / build_stm32 (stm32_misc_build) (push) Has been cancelled
stm32 port / build_stm32 (stm32_nucleo_build) (push) Has been cancelled
stm32 port / build_stm32 (stm32_pyb_build) (push) Has been cancelled
unix port / minimal (push) Has been cancelled
unix port / reproducible (push) Has been cancelled
unix port / standard (push) Has been cancelled
unix port / standard_v2 (push) Has been cancelled
unix port / coverage (push) Has been cancelled
unix port / coverage_32bit (push) Has been cancelled
unix port / nanbox (push) Has been cancelled
unix port / longlong (push) Has been cancelled
unix port / float (push) Has been cancelled
unix port / gil_enabled (push) Has been cancelled
unix port / stackless_clang (push) Has been cancelled
unix port / float_clang (push) Has been cancelled
unix port / settrace_stackless (push) Has been cancelled
unix port / macos (push) Has been cancelled
unix port / qemu_mips (push) Has been cancelled
unix port / qemu_arm (push) Has been cancelled
unix port / qemu_riscv64 (push) Has been cancelled
unix port / sanitize_address (push) Has been cancelled
unix port / sanitize_undefined (push) Has been cancelled
webassembly port / build (push) Has been cancelled
windows port / build-vs (Debug, x64, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Debug, x64, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Debug, x86, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Debug, x86, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, x64, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, x64, dev, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, x64, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, x64, standard, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, x64, standard, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, x64, standard, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, x86, dev, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, x86, dev, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, x86, dev, 2022, [17, 18)) (push) Has been cancelled
windows port / build-vs (Release, x86, standard, 2017, [15, 16)) (push) Has been cancelled
windows port / build-vs (Release, x86, standard, 2019, [16, 17)) (push) Has been cancelled
windows port / build-vs (Release, x86, standard, 2022, [17, 18)) (push) Has been cancelled
windows port / build-mingw (i686, mingw32, dev) (push) Has been cancelled
windows port / build-mingw (i686, mingw32, standard) (push) Has been cancelled
windows port / build-mingw (x86_64, mingw64, dev) (push) Has been cancelled
windows port / build-mingw (x86_64, mingw64, standard) (push) Has been cancelled
windows port / cross-build-on-linux (push) Has been cancelled
zephyr port / build (push) Has been cancelled
Python code lint and formatting with ruff / ruff (push) Has been cancelled

This reduces memory use by reusing objects, and improves identity/equality
relationships of JavaScript objects on the Python side.

In 77bd8fe5b8 PyProxy's were reused when the
same Python object was proxied across to JavaScript.  This commit does the
same thing but for JsProxy's going from JS to Python.  If an existing
JsProxy reference exists for the JS object about to be proxied across, then
it's reused.

This helps reduce the number of alive objects (memory use), and, more
importantly, improves equality relationships of JavaScript objects on the
Python side.  Eg we now get, on the Python side:

    import js

    print(js.Object == js.Object)

that prints True.  Previously it was False.

Note that this change does not make identity work with `is`, for example
`js.Object is js.Object` is actually False.  With more work that could be
made True but for now we leave that as-is.

The behaviour with this commit matches Pyodide semantics.

Signed-off-by: Damien George <damien@micropython.org>
This commit is contained in:
Damien George
2025-07-21 23:41:16 +10:00
parent 813f0c1cb9
commit ffa98cb014
5 changed files with 32 additions and 5 deletions

View File

@@ -285,6 +285,7 @@ static mp_obj_t jsproxy_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t r
EM_JS(void, proxy_js_free_obj, (int js_ref), {
if (js_ref >= PROXY_JS_REF_NUM_STATIC) {
proxy_js_ref_map.delete(proxy_js_ref[js_ref]);
proxy_js_ref[js_ref] = undefined;
if (js_ref < proxy_js_ref_next) {
proxy_js_ref_next = js_ref;

View File

@@ -62,6 +62,7 @@ class PythonError extends Error {
function proxy_js_init() {
globalThis.proxy_js_ref = [globalThis, undefined];
globalThis.proxy_js_ref_next = PROXY_JS_REF_NUM_STATIC;
globalThis.proxy_js_ref_map = new Map();
globalThis.proxy_js_map = new Map();
globalThis.proxy_js_existing = [undefined];
globalThis.pyProxyFinalizationRegistry = new FinalizationRegistry(
@@ -95,8 +96,15 @@ function proxy_js_check_existing(c_ref) {
return globalThis.proxy_js_existing.length - 1;
}
// js_obj cannot be undefined
// The `js_obj` argument cannot be `undefined`.
// Returns an integer reference to the given `js_obj`.
function proxy_js_add_obj(js_obj) {
// See if there is an existing JsProxy reference, and use that if there is.
const existing_ref = proxy_js_ref_map.get(js_obj);
if (existing_ref !== undefined) {
return existing_ref;
}
// Search for the first free slot in proxy_js_ref.
while (proxy_js_ref_next < proxy_js_ref.length) {
if (proxy_js_ref[proxy_js_ref_next] === undefined) {
@@ -104,6 +112,7 @@ function proxy_js_add_obj(js_obj) {
const id = proxy_js_ref_next;
++proxy_js_ref_next;
proxy_js_ref[id] = js_obj;
proxy_js_ref_map.set(js_obj, id);
return id;
}
++proxy_js_ref_next;
@@ -113,6 +122,7 @@ function proxy_js_add_obj(js_obj) {
const id = proxy_js_ref.length;
proxy_js_ref[id] = js_obj;
proxy_js_ref_next = proxy_js_ref.length;
proxy_js_ref_map.set(js_obj, id);
return id;
}

View File

@@ -23,4 +23,13 @@ js.eventTarget.addEventListener("event", callback)
js.eventTarget.dispatchEvent(js.event)
js.eventTarget.removeEventListener("event", callback)
js.eventTarget.dispatchEvent(js.event)
print("Object equality")
print(js.Object == js.Object)
print(js.Object.assign == js.Object.assign)
print("Array equality")
print(js.Array == js.Array)
print(js.Array.prototype == js.Array.prototype)
print(js.Array.prototype.push == js.Array.prototype.push)
`);

View File

@@ -1,3 +1,10 @@
PyProxy { _ref: 3 } PyProxy { _ref: 3 }
true
callback <JsProxy 7>
callback <JsProxy 5>
Object equality
True
True
Array equality
True
True
True

View File

@@ -2,16 +2,16 @@
1
<JsProxy 2>
py 1
<JsProxy 5>
<JsProxy 4>
py 2
2
resolved 123
3
= TEST 2 ==========
1
<JsProxy 6>
<JsProxy 5>
py 1
<JsProxy 9>
<JsProxy 6>
py 2
2
setTimeout resolved