Skip to content

Commit 45aa65b

Browse files
committed
webassembly/objjsproxy: Fix binding of self to JavaScript methods.
Fixes a bug in the binding of self/this to JavaScript methods. The new semantics match Pyodide's behaviour, at least for the included tests. Signed-off-by: Damien George <[email protected]>
1 parent 9b61bb9 commit 45aa65b

File tree

5 files changed

+138
-37
lines changed

5 files changed

+138
-37
lines changed

ports/webassembly/modjs.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@
3434
// js module
3535

3636
void mp_module_js_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
37-
mp_obj_jsproxy_t global_this;
38-
global_this.ref = MP_OBJ_JSPROXY_REF_GLOBAL_THIS;
39-
mp_obj_jsproxy_attr(MP_OBJ_FROM_PTR(&global_this), attr, dest);
37+
mp_obj_jsproxy_global_this_attr(attr, dest);
4038
}
4139

4240
static const mp_rom_map_elem_t mp_module_js_globals_table[] = {

ports/webassembly/objjsproxy.c

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ EM_JS(bool, has_attr, (int jsref, const char *str), {
4343
});
4444

4545
// *FORMAT-OFF*
46-
EM_JS(bool, lookup_attr, (int jsref, const char *str, uint32_t * out), {
46+
EM_JS(int, lookup_attr, (int jsref, const char *str, uint32_t * out), {
4747
const base = proxy_js_ref[jsref];
4848
const attr = UTF8ToString(str);
4949

@@ -54,23 +54,17 @@ EM_JS(bool, lookup_attr, (int jsref, const char *str, uint32_t * out), {
5454
// - Otherwise, the attribute does not exist.
5555
let value = base[attr];
5656
if (value !== undefined || attr in base) {
57-
if (typeof value === "function") {
58-
if (base !== globalThis) {
59-
if ("_ref" in value) {
60-
// This is a proxy of a Python function, it doesn't need
61-
// binding. And not binding it means if it's passed back
62-
// to Python then it can be extracted from the proxy as a
63-
// true Python function.
64-
} else {
65-
// A function that is not a Python function. Bind it.
66-
value = value.bind(base);
67-
}
68-
}
69-
}
7057
proxy_convert_js_to_mp_obj_jsside(value, out);
71-
return true;
58+
if (typeof value === "function" && !("_ref" in value)) {
59+
// Attribute found and it's a JavaScript function.
60+
return 2;
61+
} else {
62+
// Attribute found.
63+
return 1;
64+
}
7265
} else {
73-
return false;
66+
// Attribute not found.
67+
return 0;
7468
}
7569
});
7670
// *FORMAT-ON*
@@ -98,45 +92,65 @@ EM_JS(void, call0, (int f_ref, uint32_t * out), {
9892
proxy_convert_js_to_mp_obj_jsside(ret, out);
9993
});
10094

101-
EM_JS(int, call1, (int f_ref, uint32_t * a0, uint32_t * out), {
95+
EM_JS(int, call1, (int f_ref, bool via_call, uint32_t * a0, uint32_t * out), {
10296
const a0_js = proxy_convert_mp_to_js_obj_jsside(a0);
10397
const f = proxy_js_ref[f_ref];
104-
const ret = f(a0_js);
98+
let ret;
99+
if (via_call) {
100+
ret = f.call(a0_js);
101+
} else {
102+
ret = f(a0_js);
103+
}
105104
proxy_convert_js_to_mp_obj_jsside(ret, out);
106105
});
107106

108-
EM_JS(int, call2, (int f_ref, uint32_t * a0, uint32_t * a1, uint32_t * out), {
107+
EM_JS(int, call2, (int f_ref, bool via_call, uint32_t * a0, uint32_t * a1, uint32_t * out), {
109108
const a0_js = proxy_convert_mp_to_js_obj_jsside(a0);
110109
const a1_js = proxy_convert_mp_to_js_obj_jsside(a1);
111110
const f = proxy_js_ref[f_ref];
112-
const ret = f(a0_js, a1_js);
111+
let ret;
112+
if (via_call) {
113+
ret = f.call(a0_js, a1_js);
114+
} else {
115+
ret = f(a0_js, a1_js);
116+
}
113117
proxy_convert_js_to_mp_obj_jsside(ret, out);
114118
});
115119

116-
EM_JS(int, calln, (int f_ref, uint32_t n_args, uint32_t * value, uint32_t * out), {
120+
EM_JS(int, calln, (int f_ref, bool via_call, uint32_t n_args, uint32_t * value, uint32_t * out), {
117121
const f = proxy_js_ref[f_ref];
118122
const a = [];
119123
for (let i = 0; i < n_args; ++i) {
120124
const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
121125
a.push(v);
122126
}
123-
const ret = f(... a);
127+
let ret;
128+
if (via_call) {
129+
ret = f.call(... a);
130+
} else {
131+
ret = f(... a);
132+
}
124133
proxy_convert_js_to_mp_obj_jsside(ret, out);
125134
});
126135

127-
EM_JS(void, call0_kwarg, (int f_ref, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), {
136+
EM_JS(void, call0_kwarg, (int f_ref, bool via_call, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), {
128137
const f = proxy_js_ref[f_ref];
129138
const a = {};
130139
for (let i = 0; i < n_kw; ++i) {
131140
const k = UTF8ToString(getValue(key + i * 4, "i32"));
132141
const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
133142
a[k] = v;
134143
}
135-
const ret = f(a);
144+
let ret;
145+
if (via_call) {
146+
ret = f.call(a);
147+
} else {
148+
ret = f(a);
149+
}
136150
proxy_convert_js_to_mp_obj_jsside(ret, out);
137151
});
138152

139-
EM_JS(void, call1_kwarg, (int f_ref, uint32_t * arg0, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), {
153+
EM_JS(void, call1_kwarg, (int f_ref, bool via_call, uint32_t * arg0, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), {
140154
const f = proxy_js_ref[f_ref];
141155
const a0 = proxy_convert_mp_to_js_obj_jsside(arg0);
142156
const a = {};
@@ -145,7 +159,12 @@ EM_JS(void, call1_kwarg, (int f_ref, uint32_t * arg0, uint32_t n_kw, uint32_t *
145159
const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4);
146160
a[k] = v;
147161
}
148-
const ret = f(a0, a);
162+
let ret;
163+
if (via_call) {
164+
ret = f.call(a0, a);
165+
} else {
166+
ret = f(a0, a);
167+
}
149168
proxy_convert_js_to_mp_obj_jsside(ret, out);
150169
});
151170

@@ -208,12 +227,12 @@ static mp_obj_t jsproxy_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const
208227
}
209228
uint32_t out[3];
210229
if (n_args == 0) {
211-
call0_kwarg(self->ref, n_kw, key, value, out);
230+
call0_kwarg(self->ref, self->bind_to_self, n_kw, key, value, out);
212231
} else {
213232
// n_args == 1
214233
uint32_t arg0[PVN];
215234
proxy_convert_mp_to_js_obj_cside(args[0], arg0);
216-
call1_kwarg(self->ref, arg0, n_kw, key, value, out);
235+
call1_kwarg(self->ref, self->bind_to_self, arg0, n_kw, key, value, out);
217236
}
218237
return proxy_convert_js_to_mp_obj_cside(out);
219238
}
@@ -226,23 +245,23 @@ static mp_obj_t jsproxy_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const
226245
uint32_t arg0[PVN];
227246
uint32_t out[PVN];
228247
proxy_convert_mp_to_js_obj_cside(args[0], arg0);
229-
call1(self->ref, arg0, out);
248+
call1(self->ref, self->bind_to_self, arg0, out);
230249
return proxy_convert_js_to_mp_obj_cside(out);
231250
} else if (n_args == 2) {
232251
uint32_t arg0[PVN];
233252
proxy_convert_mp_to_js_obj_cside(args[0], arg0);
234253
uint32_t arg1[PVN];
235254
proxy_convert_mp_to_js_obj_cside(args[1], arg1);
236255
uint32_t out[3];
237-
call2(self->ref, arg0, arg1, out);
256+
call2(self->ref, self->bind_to_self, arg0, arg1, out);
238257
return proxy_convert_js_to_mp_obj_cside(out);
239258
} else {
240259
uint32_t value[PVN * n_args];
241260
for (int i = 0; i < n_args; ++i) {
242261
proxy_convert_mp_to_js_obj_cside(args[i], &value[i * PVN]);
243262
}
244263
uint32_t out[3];
245-
calln(self->ref, n_args, value, out);
264+
calln(self->ref, self->bind_to_self, n_args, value, out);
246265
return proxy_convert_js_to_mp_obj_cside(out);
247266
}
248267
}
@@ -298,17 +317,26 @@ static mp_obj_t jsproxy_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value)
298317
}
299318
}
300319

301-
void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
320+
static void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
302321
mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
303322
if (dest[0] == MP_OBJ_NULL) {
304323
// Load attribute.
324+
int lookup_ret;
305325
uint32_t out[PVN];
306326
if (attr == MP_QSTR___del__) {
307327
// For finaliser.
308328
dest[0] = MP_OBJ_FROM_PTR(&jsproxy___del___obj);
309329
dest[1] = self_in;
310-
} else if (lookup_attr(self->ref, qstr_str(attr), out)) {
330+
} else if ((lookup_ret = lookup_attr(self->ref, qstr_str(attr), out)) != 0) {
311331
dest[0] = proxy_convert_js_to_mp_obj_cside(out);
332+
if (lookup_ret == 2) {
333+
// The loaded attribute is a JavaScript method, which should be called
334+
// with f.call(self, ...). Indicate this via the bind_to_self member.
335+
// This will either be called immediately (due to the mp_load_method
336+
// optimisation) or turned into a bound_method and called later.
337+
dest[1] = self_in;
338+
((mp_obj_jsproxy_t *)dest[0])->bind_to_self = true;
339+
}
312340
} else if (attr == MP_QSTR_new) {
313341
// Special case to handle construction of JS objects.
314342
// JS objects don't have a ".new" attribute, doing "Obj.new" is a Pyodide idiom for "new Obj".
@@ -546,5 +574,25 @@ MP_DEFINE_CONST_OBJ_TYPE(
546574
mp_obj_t mp_obj_new_jsproxy(int ref) {
547575
mp_obj_jsproxy_t *o = mp_obj_malloc_with_finaliser(mp_obj_jsproxy_t, &mp_type_jsproxy);
548576
o->ref = ref;
577+
o->bind_to_self = false;
549578
return MP_OBJ_FROM_PTR(o);
550579
}
580+
581+
// Load/delete/store an attribute from/to the JavaScript globalThis entity.
582+
void mp_obj_jsproxy_global_this_attr(qstr attr, mp_obj_t *dest) {
583+
if (dest[0] == MP_OBJ_NULL) {
584+
// Load attribute.
585+
uint32_t out[PVN];
586+
if (lookup_attr(MP_OBJ_JSPROXY_REF_GLOBAL_THIS, qstr_str(attr), out)) {
587+
dest[0] = proxy_convert_js_to_mp_obj_cside(out);
588+
}
589+
} else if (dest[1] == MP_OBJ_NULL) {
590+
// Delete attribute.
591+
} else {
592+
// Store attribute.
593+
uint32_t value[PVN];
594+
proxy_convert_mp_to_js_obj_cside(dest[1], value);
595+
store_attr(MP_OBJ_JSPROXY_REF_GLOBAL_THIS, qstr_str(attr), value);
596+
dest[0] = MP_OBJ_NULL;
597+
}
598+
}

ports/webassembly/proxy_c.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
typedef struct _mp_obj_jsproxy_t {
3939
mp_obj_base_t base;
4040
int ref;
41+
bool bind_to_self;
4142
} mp_obj_jsproxy_t;
4243

4344
extern const mp_obj_type_t mp_type_jsproxy;
@@ -52,7 +53,7 @@ void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out);
5253
void proxy_convert_mp_to_js_exc_cside(void *exc, uint32_t *out);
5354

5455
mp_obj_t mp_obj_new_jsproxy(int ref);
55-
void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
56+
void mp_obj_jsproxy_global_this_attr(qstr attr, mp_obj_t *dest);
5657

5758
static inline bool mp_obj_is_jsproxy(mp_obj_t o) {
5859
return mp_obj_get_type(o) == &mp_type_jsproxy;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Test how JavaScript binds self/this when methods are called from Python.
2+
3+
const mp = await (await import(process.argv[2])).loadMicroPython();
4+
5+
// Test accessing and calling JavaScript methods from Python.
6+
mp.runPython(`
7+
import js
8+
9+
# Get the push method to call later on.
10+
push = js.Array.prototype.push
11+
12+
# Create initial array.
13+
ar = js.Array(1, 2)
14+
js.console.log(ar)
15+
16+
# Add an element using a method (should implicitly supply "ar" as context).
17+
print(ar.push(3))
18+
js.console.log(ar)
19+
20+
# Add an element using prototype function, need to explicitly provide "ar" as context.
21+
print(push.call(ar, 4))
22+
js.console.log(ar)
23+
24+
# Add an element using a method with call and explicit context.
25+
print(ar.push.call(ar, 5))
26+
js.console.log(ar)
27+
28+
# Add an element using a different instances method with call and explicit context.
29+
print(js.Array().push.call(ar, 6))
30+
js.console.log(ar)
31+
`);
32+
33+
// Test assigning Python functions to JavaScript objects, and using them like a method.
34+
mp.runPython(`
35+
import js
36+
37+
a = js.Object()
38+
a.meth1 = lambda *x: print("meth1", x)
39+
a.meth1(1, 2)
40+
41+
js.Object.prototype.meth2 = lambda *x: print("meth2", x)
42+
a.meth2(3, 4)
43+
`);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[ 1, 2 ]
2+
3
3+
[ 1, 2, 3 ]
4+
4
5+
[ 1, 2, 3, 4 ]
6+
5
7+
[ 1, 2, 3, 4, 5 ]
8+
6
9+
[ 1, 2, 3, 4, 5, 6 ]
10+
meth1 (1, 2)
11+
meth2 (3, 4)

0 commit comments

Comments
 (0)