/* * This file is part of the MicroPython project, http://micropython.org/ * * The MIT License (MIT) * * Copyright (c) 2023-2024 Damien P. George * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include #include #include "emscripten.h" #include "py/objmodule.h" #include "py/runtime.h" #include "proxy_c.h" EM_JS(bool, has_attr, (int jsref, const char *str), { const base = proxy_js_ref[jsref]; const attr = UTF8ToString(str); if (attr in base) { return true; } else { return false; } }); // *FORMAT-OFF* EM_JS(bool, lookup_attr, (int jsref, const char *str, uint32_t * out), { const base = proxy_js_ref[jsref]; const attr = UTF8ToString(str); // Attempt to lookup the requested attribute from the base object: // - If the value is not `undefined` then the attribute exists with that value. // - Otherwise if the value is `undefined` and the `in` operator returns true, then // that attribute does exist and is intended to have a value of `undefined`. // - Otherwise, the attribute does not exist. let value = base[attr]; if (value !== undefined || attr in base) { if (typeof value === "function") { if (base !== globalThis) { if ("_ref" in value) { // This is a proxy of a Python function, it doesn't need // binding. And not binding it means if it's passed back // to Python then it can be extracted from the proxy as a // true Python function. } else { // A function that is not a Python function. Bind it. value = value.bind(base); } } } proxy_convert_js_to_mp_obj_jsside(value, out); return true; } else { return false; } }); // *FORMAT-ON* EM_JS(void, store_attr, (int jsref, const char *attr_ptr, uint32_t * value_ref), { const attr = UTF8ToString(attr_ptr); const value = proxy_convert_mp_to_js_obj_jsside(value_ref); proxy_js_ref[jsref][attr] = value; }); EM_JS(void, call0, (int f_ref, uint32_t * out), { // Because of JavaScript "this" semantics, we must extract the target function // to a variable before calling it, so "this" is bound to the correct value. // // In detail: // In JavaScript, proxy_js_ref[f_ref] acts like a function call // proxy_js_ref.at(f_ref), and "this" will be bound to proxy_js_ref if // there is a chain of calls, such as proxy_js_ref.at(f_ref)(). // But proxy_js_ref is not "this" in the context of the call, so we // must extract the function to an independent variable and then call // that variable, so that "this" is correct (it will be "undefined"). const f = proxy_js_ref[f_ref]; const ret = f(); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(int, call1, (int f_ref, uint32_t * a0, uint32_t * out), { const a0_js = proxy_convert_mp_to_js_obj_jsside(a0); const f = proxy_js_ref[f_ref]; const ret = f(a0_js); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(int, call2, (int f_ref, uint32_t * a0, uint32_t * a1, uint32_t * out), { const a0_js = proxy_convert_mp_to_js_obj_jsside(a0); const a1_js = proxy_convert_mp_to_js_obj_jsside(a1); const f = proxy_js_ref[f_ref]; const ret = f(a0_js, a1_js); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(int, calln, (int f_ref, uint32_t n_args, uint32_t * value, uint32_t * out), { const f = proxy_js_ref[f_ref]; const a = []; for (let i = 0; i < n_args; ++i) { const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4); a.push(v); } const ret = f(... a); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(void, call0_kwarg, (int f_ref, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), { const f = proxy_js_ref[f_ref]; const a = {}; for (let i = 0; i < n_kw; ++i) { const k = UTF8ToString(getValue(key + i * 4, "i32")); const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4); a[k] = v; } const ret = f(a); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(void, call1_kwarg, (int f_ref, uint32_t * arg0, uint32_t n_kw, uint32_t * key, uint32_t * value, uint32_t * out), { const f = proxy_js_ref[f_ref]; const a0 = proxy_convert_mp_to_js_obj_jsside(arg0); const a = {}; for (let i = 0; i < n_kw; ++i) { const k = UTF8ToString(getValue(key + i * 4, "i32")); const v = proxy_convert_mp_to_js_obj_jsside(value + i * 3 * 4); a[k] = v; } const ret = f(a0, a); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(void, js_reflect_construct, (int f_ref, uint32_t n_args, uint32_t * args, uint32_t * out), { const f = proxy_js_ref[f_ref]; const as = []; for (let i = 0; i < n_args; ++i) { as.push(proxy_convert_mp_to_js_obj_jsside(args + i * 3 * 4)); } const ret = Reflect.construct(f, as); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(void, js_get_iter, (int f_ref, uint32_t * out), { const f = proxy_js_ref[f_ref]; const ret = f[Symbol.iterator](); proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(bool, js_iter_next, (int f_ref, uint32_t * out), { const f = proxy_js_ref[f_ref]; const ret = f.next(); if (ret.done) { return false; } else { proxy_convert_js_to_mp_obj_jsside(ret.value, out); return true; } }); EM_JS(void, js_subscr_load, (int f_ref, uint32_t * index_ref, uint32_t * out), { const target = proxy_js_ref[f_ref]; const index = python_index_semantics(target, proxy_convert_mp_to_js_obj_jsside(index_ref)); const ret = target[index]; proxy_convert_js_to_mp_obj_jsside(ret, out); }); EM_JS(void, js_subscr_store, (int f_ref, uint32_t * idx, uint32_t * value), { const f = proxy_js_ref[f_ref]; f[proxy_convert_mp_to_js_obj_jsside(idx)] = proxy_convert_mp_to_js_obj_jsside(value); }); static void jsproxy_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in); mp_printf(print, "", self->ref); } static mp_obj_t jsproxy_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in); if (n_kw == 0) { mp_arg_check_num(n_args, n_kw, 0, MP_OBJ_FUN_ARGS_MAX, false); } else { mp_arg_check_num(n_args, n_kw, 0, 1, true); uint32_t key[n_kw]; uint32_t value[PVN * n_kw]; for (int i = 0; i < n_kw; ++i) { key[i] = (uintptr_t)mp_obj_str_get_str(args[n_args + i * 2]); proxy_convert_mp_to_js_obj_cside(args[n_args + i * 2 + 1], &value[i * PVN]); } uint32_t out[3]; if (n_args == 0) { call0_kwarg(self->ref, n_kw, key, value, out); } else { // n_args == 1 uint32_t arg0[PVN]; proxy_convert_mp_to_js_obj_cside(args[0], arg0); call1_kwarg(self->ref, arg0, n_kw, key, value, out); } return proxy_convert_js_to_mp_obj_cside(out); } if (n_args == 0) { uint32_t out[3]; call0(self->ref, out); return proxy_convert_js_to_mp_obj_cside(out); } else if (n_args == 1) { uint32_t arg0[PVN]; uint32_t out[PVN]; proxy_convert_mp_to_js_obj_cside(args[0], arg0); call1(self->ref, arg0, out); return proxy_convert_js_to_mp_obj_cside(out); } else if (n_args == 2) { uint32_t arg0[PVN]; proxy_convert_mp_to_js_obj_cside(args[0], arg0); uint32_t arg1[PVN]; proxy_convert_mp_to_js_obj_cside(args[1], arg1); uint32_t out[3]; call2(self->ref, arg0, arg1, out); return proxy_convert_js_to_mp_obj_cside(out); } else { uint32_t value[PVN * n_args]; for (int i = 0; i < n_args; ++i) { proxy_convert_mp_to_js_obj_cside(args[i], &value[i * PVN]); } uint32_t out[3]; calln(self->ref, n_args, value, out); return proxy_convert_js_to_mp_obj_cside(out); } } EM_JS(void, proxy_js_free_obj, (int js_ref), { if (js_ref >= PROXY_JS_REF_NUM_STATIC) { proxy_js_ref[js_ref] = undefined; if (js_ref < proxy_js_ref_next) { proxy_js_ref_next = js_ref; } } }); static mp_obj_t jsproxy___del__(mp_obj_t self_in) { mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in); proxy_js_free_obj(self->ref); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(jsproxy___del___obj, jsproxy___del__); static mp_obj_t jsproxy_reflect_construct(size_t n_args, const mp_obj_t *args) { int arg0 = mp_obj_jsproxy_get_ref(args[0]); n_args -= 1; args += 1; uint32_t args_conv[n_args]; for (unsigned int i = 0; i < n_args; ++i) { proxy_convert_mp_to_js_obj_cside(args[i], &args_conv[i * PVN]); } uint32_t out[PVN]; js_reflect_construct(arg0, n_args, args_conv, out); return proxy_convert_js_to_mp_obj_cside(out); } static MP_DEFINE_CONST_FUN_OBJ_VAR(jsproxy_reflect_construct_obj, 1, jsproxy_reflect_construct); static mp_obj_t jsproxy_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) { mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in); if (value == MP_OBJ_SENTINEL) { // Load subscript. uint32_t idx[PVN], out[PVN]; proxy_convert_mp_to_js_obj_cside(index, idx); js_subscr_load(self->ref, idx, out); return proxy_convert_js_to_mp_obj_cside(out); } else if (value == MP_OBJ_NULL) { // Delete subscript. return MP_OBJ_NULL; // not supported } else { // Store subscript. uint32_t idx[PVN], val[PVN]; proxy_convert_mp_to_js_obj_cside(index, idx); proxy_convert_mp_to_js_obj_cside(value, val); js_subscr_store(self->ref, idx, val); return mp_const_none; } } void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in); if (dest[0] == MP_OBJ_NULL) { // Load attribute. uint32_t out[PVN]; if (attr == MP_QSTR___del__) { // For finaliser. dest[0] = MP_OBJ_FROM_PTR(&jsproxy___del___obj); dest[1] = self_in; } else if (lookup_attr(self->ref, qstr_str(attr), out)) { dest[0] = proxy_convert_js_to_mp_obj_cside(out); } else if (attr == MP_QSTR_new) { // Special case to handle construction of JS objects. // JS objects don't have a ".new" attribute, doing "Obj.new" is a Pyodide idiom for "new Obj". // It translates to the JavaScript "Reflect.construct(Obj, Array(...args))". dest[0] = MP_OBJ_FROM_PTR(&jsproxy_reflect_construct_obj); dest[1] = self_in; } } else if (dest[1] == MP_OBJ_NULL) { // Delete attribute. } else { // Store attribute. uint32_t value[PVN]; proxy_convert_mp_to_js_obj_cside(dest[1], value); store_attr(self->ref, qstr_str(attr), value); dest[0] = MP_OBJ_NULL; } } /******************************************************************************/ // jsproxy iterator typedef struct _jsproxy_it_t { mp_obj_base_t base; mp_fun_1_t iternext; mp_obj_jsproxy_t *iter; } jsproxy_it_t; static mp_obj_t jsproxy_it_iternext(mp_obj_t self_in) { jsproxy_it_t *self = MP_OBJ_TO_PTR(self_in); uint32_t out[3]; if (js_iter_next(self->iter->ref, out)) { return proxy_convert_js_to_mp_obj_cside(out); } else { return MP_OBJ_STOP_ITERATION; } } static mp_obj_t jsproxy_new_it(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) { assert(sizeof(jsproxy_it_t) <= sizeof(mp_obj_iter_buf_t)); mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in); jsproxy_it_t *o = (jsproxy_it_t *)iter_buf; o->base.type = &mp_type_polymorph_iter; o->iternext = jsproxy_it_iternext; uint32_t out[3]; js_get_iter(self->ref, out); o->iter = proxy_convert_js_to_mp_obj_cside(out); return MP_OBJ_FROM_PTR(o); } /******************************************************************************/ // jsproxy generator enum { JSOBJ_GEN_STATE_WAITING, JSOBJ_GEN_STATE_COMPLETED, JSOBJ_GEN_STATE_EXHAUSTED, }; typedef struct _jsproxy_gen_t { mp_obj_base_t base; mp_obj_t thenable; int state; } jsproxy_gen_t; mp_vm_return_kind_t jsproxy_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, mp_obj_t *ret_val) { jsproxy_gen_t *self = MP_OBJ_TO_PTR(self_in); if (throw_value) { *ret_val = throw_value; return MP_VM_RETURN_EXCEPTION; } switch (self->state) { case JSOBJ_GEN_STATE_WAITING: self->state = JSOBJ_GEN_STATE_COMPLETED; *ret_val = self->thenable; return MP_VM_RETURN_YIELD; case JSOBJ_GEN_STATE_COMPLETED: self->state = JSOBJ_GEN_STATE_EXHAUSTED; *ret_val = send_value; return MP_VM_RETURN_NORMAL; case JSOBJ_GEN_STATE_EXHAUSTED: default: // Trying to resume an already stopped generator. // This is an optimised "raise StopIteration(None)". *ret_val = mp_const_none; return MP_VM_RETURN_NORMAL; } } static mp_obj_t jsproxy_gen_resume_and_raise(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, bool raise_stop_iteration) { mp_obj_t ret; switch (jsproxy_gen_resume(self_in, send_value, throw_value, &ret)) { case MP_VM_RETURN_NORMAL: default: // A normal return is a StopIteration, either raise it or return // MP_OBJ_STOP_ITERATION as an optimisation. if (ret == mp_const_none) { ret = MP_OBJ_NULL; } if (raise_stop_iteration) { mp_raise_StopIteration(ret); } else { return mp_make_stop_iteration(ret); } case MP_VM_RETURN_YIELD: return ret; case MP_VM_RETURN_EXCEPTION: nlr_raise(ret); } } static mp_obj_t jsproxy_gen_instance_iternext(mp_obj_t self_in) { return jsproxy_gen_resume_and_raise(self_in, mp_const_none, MP_OBJ_NULL, false); } static mp_obj_t jsproxy_gen_instance_send(mp_obj_t self_in, mp_obj_t send_value) { return jsproxy_gen_resume_and_raise(self_in, send_value, MP_OBJ_NULL, true); } static MP_DEFINE_CONST_FUN_OBJ_2(jsproxy_gen_instance_send_obj, jsproxy_gen_instance_send); static mp_obj_t jsproxy_gen_instance_throw(size_t n_args, const mp_obj_t *args) { // The signature of this function is: throw(type[, value[, traceback]]) // CPython will pass all given arguments through the call chain and process them // at the point they are used (native generators will handle them differently to // user-defined generators with a throw() method). To save passing multiple // values, MicroPython instead does partial processing here to reduce it down to // one argument and passes that through: // - if only args[1] is given, or args[2] is given but is None, args[1] is // passed through (in the standard case it is an exception class or instance) // - if args[2] is given and not None it is passed through (in the standard // case it would be an exception instance and args[1] its corresponding class) // - args[3] is always ignored mp_obj_t exc = args[1]; if (n_args > 2 && args[2] != mp_const_none) { exc = args[2]; } return jsproxy_gen_resume_and_raise(args[0], mp_const_none, exc, true); } static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(jsproxy_gen_instance_throw_obj, 2, 4, jsproxy_gen_instance_throw); static mp_obj_t jsproxy_gen_instance_close(mp_obj_t self_in) { mp_obj_t ret; switch (jsproxy_gen_resume(self_in, mp_const_none, MP_OBJ_FROM_PTR(&mp_const_GeneratorExit_obj), &ret)) { case MP_VM_RETURN_YIELD: mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("generator ignored GeneratorExit")); // Swallow GeneratorExit (== successful close), and re-raise any other case MP_VM_RETURN_EXCEPTION: // ret should always be an instance of an exception class if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(ret)), MP_OBJ_FROM_PTR(&mp_type_GeneratorExit))) { return mp_const_none; } nlr_raise(ret); default: // The only choice left is MP_VM_RETURN_NORMAL which is successful close return mp_const_none; } } static MP_DEFINE_CONST_FUN_OBJ_1(jsproxy_gen_instance_close_obj, jsproxy_gen_instance_close); static const mp_rom_map_elem_t jsproxy_gen_instance_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&jsproxy_gen_instance_close_obj) }, { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&jsproxy_gen_instance_send_obj) }, { MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&jsproxy_gen_instance_throw_obj) }, }; static MP_DEFINE_CONST_DICT(jsproxy_gen_instance_locals_dict, jsproxy_gen_instance_locals_dict_table); MP_DEFINE_CONST_OBJ_TYPE( mp_type_jsproxy_gen, MP_QSTR_generator, MP_TYPE_FLAG_ITER_IS_ITERNEXT, iter, jsproxy_gen_instance_iternext, locals_dict, &jsproxy_gen_instance_locals_dict ); static mp_obj_t jsproxy_new_gen(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) { assert(sizeof(jsproxy_gen_t) <= sizeof(mp_obj_iter_buf_t)); jsproxy_gen_t *o = (jsproxy_gen_t *)iter_buf; o->base.type = &mp_type_jsproxy_gen; o->thenable = self_in; o->state = JSOBJ_GEN_STATE_WAITING; return MP_OBJ_FROM_PTR(o); } /******************************************************************************/ #if MICROPY_PY_ASYNCIO extern mp_obj_t mp_asyncio_context; #endif static mp_obj_t jsproxy_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) { mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in); if (has_attr(self->ref, "then")) { #if MICROPY_PY_ASYNCIO // When asyncio is running and the caller here is a task, wrap the JavaScript // thenable in a ThenableEvent, and get the task to wait on that event. This // decouples the task from the thenable and allows cancelling the task. if (mp_asyncio_context != MP_OBJ_NULL) { mp_obj_t cur_task = mp_obj_dict_get(mp_asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_cur_task)); if (cur_task != mp_const_none) { mp_obj_t thenable_event_class = mp_obj_dict_get(mp_asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_ThenableEvent)); mp_obj_t thenable_event = mp_call_function_1(thenable_event_class, self_in); mp_obj_t dest[2]; mp_load_method(thenable_event, MP_QSTR_wait, dest); mp_obj_t wait_gen = mp_call_method_n_kw(0, 0, dest); return mp_getiter(wait_gen, iter_buf); } } #endif return jsproxy_new_gen(self_in, iter_buf); } else { return jsproxy_new_it(self_in, iter_buf); } } MP_DEFINE_CONST_OBJ_TYPE( mp_type_jsproxy, MP_QSTR_JsProxy, MP_TYPE_FLAG_ITER_IS_GETITER, print, jsproxy_print, call, jsproxy_call, attr, mp_obj_jsproxy_attr, subscr, jsproxy_subscr, iter, jsproxy_getiter ); mp_obj_t mp_obj_new_jsproxy(int ref) { mp_obj_jsproxy_t *o = mp_obj_malloc_with_finaliser(mp_obj_jsproxy_t, &mp_type_jsproxy); o->ref = ref; return MP_OBJ_FROM_PTR(o); }