webassembly: Improve identity and fix bug with lost JsProxy refs.

Commit ffa98cb014 improved equality for
`JsProxy` objects so that, eg, `js.Object == js.Object` is true.

As mentioned in #17758, a further optimisation is to make identity work in
that case, eg `js.Object is js.Object` should be true (on the Python side).

This commit implements that, by keeping track of all `JsProxy` Python
objects and reusing them where possible: where the underlying JS ref is
equal, ie they point to the same JS object.  That reduces memory churn and
gives better identity behaviour of JS objects proxied over to Python.

As part of this, a bug is fixed where JS objects can be freed while there's
still a `JsProxy` referring to that JS object.  A test is added for that
exact scenario, and the test now passes.

Signed-off-by: Damien George <damien@micropython.org>
This commit is contained in:
Damien George
2025-09-01 13:56:58 +10:00
parent 9eddbb32f3
commit a319ddefa6
9 changed files with 155 additions and 52 deletions

View File

@@ -48,8 +48,9 @@ const PROXY_KIND_JS_BOOLEAN = 2;
const PROXY_KIND_JS_INTEGER = 3;
const PROXY_KIND_JS_DOUBLE = 4;
const PROXY_KIND_JS_STRING = 5;
const PROXY_KIND_JS_OBJECT = 6;
const PROXY_KIND_JS_PYPROXY = 7;
const PROXY_KIND_JS_OBJECT_EXISTING = 6;
const PROXY_KIND_JS_OBJECT = 7;
const PROXY_KIND_JS_PYPROXY = 8;
class PythonError extends Error {
constructor(exc_type, exc_details) {
@@ -63,6 +64,7 @@ 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_ref_map.set(globalThis, 0);
globalThis.proxy_js_map = new Map();
globalThis.proxy_js_existing = [undefined];
globalThis.pyProxyFinalizationRegistry = new FinalizationRegistry(
@@ -99,12 +101,6 @@ function proxy_js_check_existing(c_ref) {
// 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) {
@@ -175,7 +171,7 @@ function proxy_call_python(target, argumentsList) {
return ret;
}
function proxy_convert_js_to_mp_obj_jsside(js_obj, out) {
function proxy_convert_js_to_mp_obj_jsside_helper(js_obj, out, allow_pyproxy) {
let kind;
if (js_obj === undefined) {
kind = PROXY_KIND_JS_UNDEFINED;
@@ -206,33 +202,35 @@ function proxy_convert_js_to_mp_obj_jsside(js_obj, out) {
Module.setValue(out + 4, len, "i32");
Module.setValue(out + 8, buf, "i32");
} else if (
js_obj instanceof PyProxy ||
(typeof js_obj === "function" && "_ref" in js_obj) ||
js_obj instanceof PyProxyThenable
allow_pyproxy &&
(js_obj instanceof PyProxy ||
(typeof js_obj === "function" && "_ref" in js_obj) ||
js_obj instanceof PyProxyThenable)
) {
kind = PROXY_KIND_JS_PYPROXY;
Module.setValue(out + 4, js_obj._ref, "i32");
} else {
kind = PROXY_KIND_JS_OBJECT;
const id = proxy_js_add_obj(js_obj);
let id;
// 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) {
kind = PROXY_KIND_JS_OBJECT_EXISTING;
id = existing_ref;
} else {
kind = PROXY_KIND_JS_OBJECT;
id = proxy_js_add_obj(js_obj);
}
Module.setValue(out + 4, id, "i32");
}
Module.setValue(out + 0, kind, "i32");
}
function proxy_convert_js_to_mp_obj_jsside(js_obj, out) {
proxy_convert_js_to_mp_obj_jsside_helper(js_obj, out, true);
}
function proxy_convert_js_to_mp_obj_jsside_force_double_proxy(js_obj, out) {
if (
js_obj instanceof PyProxy ||
(typeof js_obj === "function" && "_ref" in js_obj) ||
js_obj instanceof PyProxyThenable
) {
const kind = PROXY_KIND_JS_OBJECT;
const id = proxy_js_add_obj(js_obj);
Module.setValue(out + 4, id, "i32");
Module.setValue(out + 0, kind, "i32");
} else {
proxy_convert_js_to_mp_obj_jsside(js_obj, out);
}
proxy_convert_js_to_mp_obj_jsside_helper(js_obj, out, false);
}
function proxy_convert_mp_to_js_obj_jsside(value) {