diff --git a/ports/webassembly/Makefile b/ports/webassembly/Makefile index e8f27d862b..2a5669392e 100644 --- a/ports/webassembly/Makefile +++ b/ports/webassembly/Makefile @@ -45,7 +45,34 @@ CFLAGS += -std=c99 -Wall -Werror -Wdouble-promotion -Wfloat-conversion CFLAGS += -Os -DNDEBUG CFLAGS += $(INC) +EXPORTED_FUNCTIONS_EXTRA += ,\ + _mp_js_do_exec,\ + _mp_js_do_import,\ + _mp_js_register_js_module,\ + _proxy_c_init,\ + _proxy_c_to_js_call,\ + _proxy_c_to_js_delete_attr,\ + _proxy_c_to_js_dir,\ + _proxy_c_to_js_get_array,\ + _proxy_c_to_js_get_dict,\ + _proxy_c_to_js_get_type,\ + _proxy_c_to_js_has_attr,\ + _proxy_c_to_js_lookup_attr,\ + _proxy_c_to_js_store_attr,\ + _proxy_convert_mp_to_js_obj_cside + +EXPORTED_RUNTIME_METHODS_EXTRA += ,\ + PATH,\ + PATH_FS,\ + UTF8ToString,\ + getValue,\ + lengthBytesUTF8,\ + setValue,\ + stringToUTF8 + JSFLAGS += -s EXPORTED_FUNCTIONS="\ + _free,\ + _malloc,\ _mp_js_init,\ _mp_js_init_repl,\ _mp_js_do_str,\ @@ -58,6 +85,7 @@ JSFLAGS += -s EXPORTED_RUNTIME_METHODS="\ FS$(EXPORTED_RUNTIME_METHODS_EXTRA)" JSFLAGS += --js-library library.js JSFLAGS += -s SUPPORT_LONGJMP=emscripten +JSFLAGS += -s MODULARIZE -s EXPORT_NAME=_createMicroPythonModule ################################################################################ # Source files and libraries. @@ -71,13 +99,21 @@ SRC_SHARED = $(addprefix shared/,\ ) SRC_C += \ + lexer_dedent.c \ main.c \ + modjs.c \ + modjsffi.c \ mphalport.c \ + objjsproxy.c \ + proxy_c.c \ # List of sources for qstr extraction. SRC_QSTR += $(SRC_C) $(SRC_SHARED) -SRC_JS ?= wrapper.js +SRC_JS += \ + api.js \ + objpyproxy.js \ + proxy_js.js \ OBJ += $(PY_O) OBJ += $(addprefix $(BUILD)/, $(SRC_SHARED:.c=.o)) @@ -86,23 +122,25 @@ OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) ################################################################################ # Main targets. -.PHONY: all min test +.PHONY: all min test test_min -all: $(BUILD)/micropython.js +all: $(BUILD)/micropython.mjs -$(BUILD)/micropython.js: $(OBJ) library.js $(SRC_JS) +$(BUILD)/micropython.mjs: $(OBJ) library.js $(SRC_JS) $(ECHO) "LINK $@" $(Q)emcc $(LDFLAGS) -o $@ $(OBJ) $(JSFLAGS) $(Q)cat $(SRC_JS) >> $@ -$(BUILD)/micropython.min.js: $(BUILD)/micropython.js +$(BUILD)/micropython.min.mjs: $(BUILD)/micropython.mjs $(TERSER) $< --compress --module -o $@ -min: $(BUILD)/micropython.min.js +min: $(BUILD)/micropython.min.mjs -test: $(BUILD)/micropython.js $(TOP)/tests/run-tests.py - $(eval DIRNAME=ports/$(notdir $(CURDIR))) - cd $(TOP)/tests && MICROPY_MICROPYTHON=../ports/webassembly/node_run.sh ./run-tests.py -j1 +test: $(BUILD)/micropython.mjs $(TOP)/tests/run-tests.py + cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py --target webassembly + +test_min: $(BUILD)/micropython.min.mjs $(TOP)/tests/run-tests.py + cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py --target webassembly ################################################################################ # Remaining make rules. diff --git a/ports/webassembly/api.js b/ports/webassembly/api.js new file mode 100644 index 0000000000..dfe7561768 --- /dev/null +++ b/ports/webassembly/api.js @@ -0,0 +1,146 @@ +/* + * 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. + */ + +// Options: +// - heapsize: size in bytes of the MicroPython GC heap. +// - url: location to load `micropython.mjs`. +// - stdin: function to return input characters. +// - stdout: function that takes one argument, and is passed lines of stdout +// output as they are produced. By default this is handled by Emscripten +// and in a browser goes to console, in node goes to process.stdout.write. +// - stderr: same behaviour as stdout but for error output. +// - linebuffer: whether to buffer line-by-line to stdout/stderr. +export async function loadMicroPython(options) { + const { heapsize, url, stdin, stdout, stderr, linebuffer } = Object.assign( + { heapsize: 1024 * 1024, linebuffer: true }, + options, + ); + const Module = {}; + Module.locateFile = (path, scriptDirectory) => + url || scriptDirectory + path; + Module._textDecoder = new TextDecoder(); + if (stdin !== undefined) { + Module.stdin = stdin; + } + if (stdout !== undefined) { + if (linebuffer) { + Module._stdoutBuffer = []; + Module.stdout = (c) => { + if (c === 10) { + stdout( + Module._textDecoder.decode( + new Uint8Array(Module._stdoutBuffer), + ), + ); + Module._stdoutBuffer = []; + } else { + Module._stdoutBuffer.push(c); + } + }; + } else { + Module.stdout = (c) => stdout(new Uint8Array([c])); + } + } + if (stderr !== undefined) { + if (linebuffer) { + Module._stderrBuffer = []; + Module.stderr = (c) => { + if (c === 10) { + stderr( + Module._textDecoder.decode( + new Uint8Array(Module._stderrBuffer), + ), + ); + Module._stderrBuffer = []; + } else { + Module._stderrBuffer.push(c); + } + }; + } else { + Module.stderr = (c) => stderr(new Uint8Array([c])); + } + } + const moduleLoaded = new Promise((r) => { + Module.postRun = r; + }); + _createMicroPythonModule(Module); + await moduleLoaded; + globalThis.Module = Module; + proxy_js_init(); + const pyimport = (name) => { + const value = Module._malloc(3 * 4); + Module.ccall( + "mp_js_do_import", + "null", + ["string", "pointer"], + [name, value], + ); + return proxy_convert_mp_to_js_obj_jsside_with_free(value); + }; + Module.ccall("mp_js_init", "null", ["number"], [heapsize]); + Module.ccall("proxy_c_init", "null", [], []); + return { + _module: Module, + PyProxy: PyProxy, + FS: Module.FS, + globals: { + __dict__: pyimport("__main__").__dict__, + get(key) { + return this.__dict__[key]; + }, + set(key, value) { + this.__dict__[key] = value; + }, + delete(key) { + delete this.__dict__[key]; + }, + }, + registerJsModule(name, module) { + const value = Module._malloc(3 * 4); + proxy_convert_js_to_mp_obj_jsside(module, value); + Module.ccall( + "mp_js_register_js_module", + "null", + ["string", "pointer"], + [name, value], + ); + Module._free(value); + }, + pyimport: pyimport, + runPython(code) { + const value = Module._malloc(3 * 4); + Module.ccall( + "mp_js_do_exec", + "number", + ["string", "pointer"], + [code, value], + ); + return proxy_convert_mp_to_js_obj_jsside_with_free(value); + }, + }; +} + +globalThis.loadMicroPython = loadMicroPython; diff --git a/ports/webassembly/lexer_dedent.c b/ports/webassembly/lexer_dedent.c new file mode 100644 index 0000000000..555caea896 --- /dev/null +++ b/ports/webassembly/lexer_dedent.c @@ -0,0 +1,105 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2023 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 "lexer_dedent.h" + +typedef struct _mp_reader_mem_dedent_t { + size_t free_len; // if >0 mem is freed on close by: m_free(beg, free_len) + const byte *beg; + const byte *cur; + const byte *end; + size_t dedent_prefix; +} mp_reader_mem_dedent_t; + +// Work out the amount of common whitespace among all non-empty lines. +static size_t dedent(const byte *text, size_t len) { + size_t min_prefix = -1; + size_t cur_prefix = 0; + bool start_of_line = true; + for (const byte *t = text; t < text + len; ++t) { + if (*t == '\n') { + start_of_line = true; + cur_prefix = 0; + } else if (start_of_line) { + if (unichar_isspace(*t)) { + ++cur_prefix; + } else { + if (cur_prefix < min_prefix) { + min_prefix = cur_prefix; + if (min_prefix == 0) { + return min_prefix; + } + } + start_of_line = false; + } + } + } + return min_prefix; +} + +static mp_uint_t mp_reader_mem_dedent_readbyte(void *data) { + mp_reader_mem_dedent_t *reader = (mp_reader_mem_dedent_t *)data; + if (reader->cur < reader->end) { + byte c = *reader->cur++; + if (c == '\n') { + for (size_t i = 0; i < reader->dedent_prefix; ++i) { + if (*reader->cur == '\n') { + break; + } + ++reader->cur; + } + } + return c; + } else { + return MP_READER_EOF; + } +} + +static void mp_reader_mem_dedent_close(void *data) { + mp_reader_mem_dedent_t *reader = (mp_reader_mem_dedent_t *)data; + if (reader->free_len > 0) { + m_del(char, (char *)reader->beg, reader->free_len); + } + m_del_obj(mp_reader_mem_dedent_t, reader); +} + +static void mp_reader_new_mem_dedent(mp_reader_t *reader, const byte *buf, size_t len, size_t free_len) { + mp_reader_mem_dedent_t *rm = m_new_obj(mp_reader_mem_dedent_t); + rm->free_len = free_len; + rm->beg = buf; + rm->cur = buf; + rm->end = buf + len; + rm->dedent_prefix = dedent(buf, len); + reader->data = rm; + reader->readbyte = mp_reader_mem_dedent_readbyte; + reader->close = mp_reader_mem_dedent_close; +} + +mp_lexer_t *mp_lexer_new_from_str_len_dedent(qstr src_name, const char *str, size_t len, size_t free_len) { + mp_reader_t reader; + mp_reader_new_mem_dedent(&reader, (const byte *)str, len, free_len); + return mp_lexer_new(src_name, reader); +} diff --git a/ports/webassembly/lexer_dedent.h b/ports/webassembly/lexer_dedent.h new file mode 100644 index 0000000000..a8cc2526b4 --- /dev/null +++ b/ports/webassembly/lexer_dedent.h @@ -0,0 +1,36 @@ +/* + * 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. + */ +#ifndef MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H +#define MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H + +#include "py/lexer.h" + +// This function creates a new "dedenting lexer" which automatically dedents the input +// source code if every non-empty line in that source starts with a common whitespace +// prefix. It does this dedenting inplace as the memory is read. +mp_lexer_t *mp_lexer_new_from_str_len_dedent(qstr src_name, const char *str, size_t len, size_t free_len); + +#endif // MICROPY_INCLUDED_WEBASSEMBLY_LEXER_DEDENT_H diff --git a/ports/webassembly/main.c b/ports/webassembly/main.c index ebde8ac700..c1c7a88840 100644 --- a/ports/webassembly/main.c +++ b/ports/webassembly/main.c @@ -40,7 +40,9 @@ #include "shared/runtime/pyexec.h" #include "emscripten.h" +#include "lexer_dedent.h" #include "library.h" +#include "proxy_c.h" #if MICROPY_ENABLE_COMPILER int do_str(const char *src, mp_parse_input_kind_t input_kind) { @@ -113,6 +115,60 @@ void mp_js_init_repl() { pyexec_event_repl_init(); } +void mp_js_register_js_module(const char *name, uint32_t *value) { + mp_obj_t module_name = MP_OBJ_NEW_QSTR(qstr_from_str(name)); + mp_obj_t module = proxy_convert_js_to_mp_obj_cside(value); + mp_map_t *mp_loaded_modules_map = &MP_STATE_VM(mp_loaded_modules_dict).map; + mp_map_lookup(mp_loaded_modules_map, module_name, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = module; +} + +void mp_js_do_import(const char *name, uint32_t *out) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_t ret = mp_import_name(qstr_from_str(name), mp_const_none, MP_OBJ_NEW_SMALL_INT(0)); + // Return the leaf of the import, eg for "a.b.c" return "c". + const char *m = name; + const char *n = name; + for (;; ++n) { + if (*n == '\0' || *n == '.') { + if (m != name) { + ret = mp_load_attr(ret, qstr_from_strn(m, n - m)); + } + m = n + 1; + if (*n == '\0') { + break; + } + } + } + nlr_pop(); + proxy_convert_mp_to_js_obj_cside(ret, out); + } else { + // uncaught exception + proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out); + } +} + +void mp_js_do_exec(const char *src, uint32_t *out) { + // Collect at the top-level, where there are no root pointers from stack/registers. + gc_collect_start(); + gc_collect_end(); + + mp_parse_input_kind_t input_kind = MP_PARSE_FILE_INPUT; + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_lexer_t *lex = mp_lexer_new_from_str_len_dedent(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0); + qstr source_name = lex->source_name; + mp_parse_tree_t parse_tree = mp_parse(lex, input_kind); + mp_obj_t module_fun = mp_compile(&parse_tree, source_name, false); + mp_obj_t ret = mp_call_function_0(module_fun); + nlr_pop(); + proxy_convert_mp_to_js_obj_cside(ret, out); + } else { + // uncaught exception + proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out); + } +} + #if MICROPY_GC_SPLIT_HEAP_AUTO // The largest new region that is available to become Python heap. diff --git a/ports/webassembly/modjs.c b/ports/webassembly/modjs.c new file mode 100644 index 0000000000..bed09086ab --- /dev/null +++ b/ports/webassembly/modjs.c @@ -0,0 +1,55 @@ +/* + * 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 "py/objmodule.h" +#include "py/runtime.h" +#include "proxy_c.h" + +#if MICROPY_PY_JS + +/******************************************************************************/ +// js module + +void mp_module_js_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + mp_obj_jsproxy_t global_this; + global_this.ref = 0; + mp_obj_jsproxy_attr(MP_OBJ_FROM_PTR(&global_this), attr, dest); +} + +static const mp_rom_map_elem_t mp_module_js_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_js) }, +}; +static MP_DEFINE_CONST_DICT(mp_module_js_globals, mp_module_js_globals_table); + +const mp_obj_module_t mp_module_js = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mp_module_js_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_js, mp_module_js); +MP_REGISTER_MODULE_DELEGATION(mp_module_js, mp_module_js_attr); + +#endif // MICROPY_PY_JS diff --git a/ports/webassembly/modjsffi.c b/ports/webassembly/modjsffi.c new file mode 100644 index 0000000000..d4e61e368f --- /dev/null +++ b/ports/webassembly/modjsffi.c @@ -0,0 +1,80 @@ +/* + * 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 "emscripten.h" +#include "py/objmodule.h" +#include "py/runtime.h" +#include "proxy_c.h" + +#if MICROPY_PY_JSFFI + +/******************************************************************************/ +// jsffi module + +EM_JS(void, proxy_convert_mp_to_js_then_js_to_mp_obj_jsside, (uint32_t * out), { + const ret = proxy_convert_mp_to_js_obj_jsside(out); + proxy_convert_js_to_mp_obj_jsside_force_double_proxy(ret, out); +}); + +static mp_obj_t mp_jsffi_create_proxy(mp_obj_t arg) { + uint32_t out[3]; + proxy_convert_mp_to_js_obj_cside(arg, out); + proxy_convert_mp_to_js_then_js_to_mp_obj_jsside(out); + return proxy_convert_js_to_mp_obj_cside(out); +} +static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_create_proxy_obj, mp_jsffi_create_proxy); + +EM_JS(void, proxy_convert_mp_to_js_then_js_to_js_then_js_to_mp_obj_jsside, (uint32_t * out), { + const ret = proxy_convert_mp_to_js_obj_jsside(out); + const js_obj = PyProxy.toJs(ret); + proxy_convert_js_to_mp_obj_jsside(js_obj, out); +}); + +static mp_obj_t mp_jsffi_to_js(mp_obj_t arg) { + uint32_t out[3]; + proxy_convert_mp_to_js_obj_cside(arg, out); + proxy_convert_mp_to_js_then_js_to_js_then_js_to_mp_obj_jsside(out); + return proxy_convert_js_to_mp_obj_cside(out); +} +static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_to_js_obj, mp_jsffi_to_js); + +static const mp_rom_map_elem_t mp_module_jsffi_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_jsffi) }, + + { MP_ROM_QSTR(MP_QSTR_JsProxy), MP_ROM_PTR(&mp_type_jsproxy) }, + { MP_ROM_QSTR(MP_QSTR_create_proxy), MP_ROM_PTR(&mp_jsffi_create_proxy_obj) }, + { MP_ROM_QSTR(MP_QSTR_to_js), MP_ROM_PTR(&mp_jsffi_to_js_obj) }, +}; +static MP_DEFINE_CONST_DICT(mp_module_jsffi_globals, mp_module_jsffi_globals_table); + +const mp_obj_module_t mp_module_jsffi = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mp_module_jsffi_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_jsffi, mp_module_jsffi); + +#endif // MICROPY_PY_JSFFI diff --git a/ports/webassembly/mpconfigport.h b/ports/webassembly/mpconfigport.h index abfbbca794..fc7ba2f82d 100644 --- a/ports/webassembly/mpconfigport.h +++ b/ports/webassembly/mpconfigport.h @@ -64,6 +64,14 @@ #define MICROPY_VFS_POSIX (MICROPY_VFS) #define MICROPY_PY_SYS_PLATFORM "webassembly" +#ifndef MICROPY_PY_JS +#define MICROPY_PY_JS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) +#endif + +#ifndef MICROPY_PY_JSFFI +#define MICROPY_PY_JSFFI (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) +#endif + #define MICROPY_EVENT_POLL_HOOK \ do { \ extern void mp_handle_pending(bool); \ diff --git a/ports/webassembly/objjsproxy.c b/ports/webassembly/objjsproxy.c new file mode 100644 index 0000000000..a28b791cf2 --- /dev/null +++ b/ports/webassembly/objjsproxy.c @@ -0,0 +1,330 @@ +/* + * 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" + +// *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); + if (attr in base) { + let value = base[attr]; + if (typeof value == "function") { + if (base !== globalThis) { + 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 * 4)); + } + const ret = Reflect.construct(f, as); + proxy_convert_js_to_mp_obj_jsside(ret, out); +}); + +EM_JS(int, js_get_len, (int f_ref), { + return proxy_js_ref[f_ref].length; +}); + +EM_JS(void, js_subscr_int, (int f_ref, int idx, uint32_t * out), { + const f = proxy_js_ref[f_ref]; + const ret = f[idx]; + proxy_convert_js_to_mp_obj_jsside(ret, out); +}); + +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); + } +} + +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[3]; + 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 (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; + int ref; + uint16_t cur; + uint16_t len; +} 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); + if (self->cur < self->len) { + uint32_t out[3]; + js_subscr_int(self->ref, self->cur, out); + self->cur += 1; + return proxy_convert_js_to_mp_obj_cside(out); + } else { + return MP_OBJ_STOP_ITERATION; + } +} + +static mp_obj_t jsproxy_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) { + assert(sizeof(jsproxy_it_t) <= sizeof(mp_obj_iter_buf_t)); + jsproxy_it_t *o = (jsproxy_it_t *)iter_buf; + o->base.type = &mp_type_polymorph_iter; + o->iternext = jsproxy_it_iternext; + o->ref = mp_obj_jsproxy_get_ref(o_in); + o->cur = 0; + o->len = js_get_len(o->ref); + return MP_OBJ_FROM_PTR(o); +} + +/******************************************************************************/ + +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(mp_obj_jsproxy_t, &mp_type_jsproxy); + o->ref = ref; + return MP_OBJ_FROM_PTR(o); +} diff --git a/ports/webassembly/objpyproxy.js b/ports/webassembly/objpyproxy.js new file mode 100644 index 0000000000..52670b66e9 --- /dev/null +++ b/ports/webassembly/objpyproxy.js @@ -0,0 +1,191 @@ +/* + * 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. + */ + +class PyProxy { + constructor(ref) { + this._ref = ref; + } + + // Convert js_obj -- which is possibly a PyProxy -- to a JavaScript object. + static toJs(js_obj) { + if (!(js_obj instanceof PyProxy)) { + return js_obj; + } + + const type = Module.ccall( + "proxy_c_to_js_get_type", + "number", + ["number"], + [js_obj._ref], + ); + + if (type === 1 || type === 2) { + // List or tuple. + const array_ref = Module._malloc(2 * 4); + const item = Module._malloc(3 * 4); + Module.ccall( + "proxy_c_to_js_get_array", + "null", + ["number", "pointer"], + [js_obj._ref, array_ref], + ); + const len = Module.getValue(array_ref, "i32"); + const items_ptr = Module.getValue(array_ref + 4, "i32"); + const js_array = []; + for (let i = 0; i < len; ++i) { + Module.ccall( + "proxy_convert_mp_to_js_obj_cside", + "null", + ["pointer", "pointer"], + [Module.getValue(items_ptr + i * 4, "i32"), item], + ); + const js_item = proxy_convert_mp_to_js_obj_jsside(item); + js_array.push(PyProxy.toJs(js_item)); + } + Module._free(array_ref); + Module._free(item); + return js_array; + } + + if (type === 3) { + // Dict. + const map_ref = Module._malloc(2 * 4); + const item = Module._malloc(3 * 4); + Module.ccall( + "proxy_c_to_js_get_dict", + "null", + ["number", "pointer"], + [js_obj._ref, map_ref], + ); + const alloc = Module.getValue(map_ref, "i32"); + const table_ptr = Module.getValue(map_ref + 4, "i32"); + const js_dict = {}; + for (let i = 0; i < alloc; ++i) { + const mp_key = Module.getValue(table_ptr + i * 8, "i32"); + if (mp_key > 8) { + // Convert key to JS object. + Module.ccall( + "proxy_convert_mp_to_js_obj_cside", + "null", + ["pointer", "pointer"], + [mp_key, item], + ); + const js_key = proxy_convert_mp_to_js_obj_jsside(item); + + // Convert value to JS object. + const mp_value = Module.getValue( + table_ptr + i * 8 + 4, + "i32", + ); + Module.ccall( + "proxy_convert_mp_to_js_obj_cside", + "null", + ["pointer", "pointer"], + [mp_value, item], + ); + const js_value = proxy_convert_mp_to_js_obj_jsside(item); + + // Populate JS dict. + js_dict[js_key] = PyProxy.toJs(js_value); + } + } + Module._free(map_ref); + Module._free(item); + return js_dict; + } + + // Cannot convert to JS, leave as a PyProxy. + return js_obj; + } +} + +// This handler's goal is to allow minimal introspection +// of Python references from the JS world/utilities. +const py_proxy_handler = { + isExtensible() { + return true; + }, + ownKeys(target) { + const value = Module._malloc(3 * 4); + Module.ccall( + "proxy_c_to_js_dir", + "null", + ["number", "pointer"], + [target._ref, value], + ); + const dir = proxy_convert_mp_to_js_obj_jsside_with_free(value); + return PyProxy.toJs(dir).filter((attr) => !attr.startsWith("__")); + }, + getOwnPropertyDescriptor(target, prop) { + return { + value: target[prop], + enumerable: true, + writable: true, + configurable: true, + }; + }, + has(target, prop) { + return Module.ccall( + "proxy_c_to_js_has_attr", + "number", + ["number", "string"], + [target._ref, prop], + ); + }, + get(target, prop) { + if (prop === "_ref") { + return target._ref; + } + const value = Module._malloc(3 * 4); + Module.ccall( + "proxy_c_to_js_lookup_attr", + "number", + ["number", "string", "pointer"], + [target._ref, prop, value], + ); + return proxy_convert_mp_to_js_obj_jsside_with_free(value); + }, + set(target, prop, value) { + const value_conv = Module._malloc(3 * 4); + proxy_convert_js_to_mp_obj_jsside(value, value_conv); + const ret = Module.ccall( + "proxy_c_to_js_store_attr", + "number", + ["number", "string", "number"], + [target._ref, prop, value_conv], + ); + Module._free(value_conv); + return ret; + }, + deleteProperty(target, prop) { + return Module.ccall( + "proxy_c_to_js_delete_attr", + "number", + ["number", "string"], + [target._ref, prop], + ); + }, +}; diff --git a/ports/webassembly/proxy_c.c b/ports/webassembly/proxy_c.c new file mode 100644 index 0000000000..809dd44dde --- /dev/null +++ b/ports/webassembly/proxy_c.c @@ -0,0 +1,281 @@ +/* + * 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 "py/builtin.h" +#include "py/runtime.h" +#include "proxy_c.h" + +// These constants should match the constants in proxy_js.js. + +enum { + PROXY_KIND_MP_EXCEPTION = -1, + PROXY_KIND_MP_NULL = 0, + PROXY_KIND_MP_NONE = 1, + PROXY_KIND_MP_BOOL = 2, + PROXY_KIND_MP_INT = 3, + PROXY_KIND_MP_FLOAT = 4, + PROXY_KIND_MP_STR = 5, + PROXY_KIND_MP_CALLABLE = 6, + PROXY_KIND_MP_OBJECT = 7, + PROXY_KIND_MP_JSPROXY = 8, +}; + +enum { + PROXY_KIND_JS_NULL = 1, + PROXY_KIND_JS_BOOLEAN = 2, + PROXY_KIND_JS_INTEGER = 3, + PROXY_KIND_JS_DOUBLE = 4, + PROXY_KIND_JS_STRING = 5, + PROXY_KIND_JS_OBJECT = 6, + PROXY_KIND_JS_PYPROXY = 7, +}; + +void proxy_c_init(void) { + MP_STATE_PORT(proxy_c_ref) = mp_obj_new_list(0, NULL); + mp_obj_list_append(MP_STATE_PORT(proxy_c_ref), MP_OBJ_NULL); +} + +MP_REGISTER_ROOT_POINTER(mp_obj_t proxy_c_ref); + +static inline mp_obj_t proxy_c_get_obj(uint32_t c_ref) { + return ((mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref)))->items[c_ref]; +} + +mp_obj_t proxy_convert_js_to_mp_obj_cside(uint32_t *value) { + if (value[0] == PROXY_KIND_JS_NULL) { + return mp_const_none; + } else if (value[0] == PROXY_KIND_JS_BOOLEAN) { + return mp_obj_new_bool(value[1]); + } else if (value[0] == PROXY_KIND_JS_INTEGER) { + return mp_obj_new_int(value[1]); + } else if (value[0] == PROXY_KIND_JS_DOUBLE) { + return mp_obj_new_float_from_d(*(double *)&value[1]); + } else if (value[0] == PROXY_KIND_JS_STRING) { + mp_obj_t s = mp_obj_new_str((void *)value[2], value[1]); + free((void *)value[2]); + return s; + } else if (value[0] == PROXY_KIND_JS_PYPROXY) { + return proxy_c_get_obj(value[1]); + } else { + // PROXY_KIND_JS_OBJECT + return mp_obj_new_jsproxy(value[1]); + } +} + +void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out) { + uint32_t kind; + if (obj == MP_OBJ_NULL) { + kind = PROXY_KIND_MP_NULL; + } else if (obj == mp_const_none) { + kind = PROXY_KIND_MP_NONE; + } else if (mp_obj_is_bool(obj)) { + kind = PROXY_KIND_MP_BOOL; + out[1] = mp_obj_is_true(obj); + } else if (mp_obj_is_int(obj)) { + kind = PROXY_KIND_MP_INT; + out[1] = mp_obj_get_int_truncated(obj); // TODO support big int + } else if (mp_obj_is_float(obj)) { + kind = PROXY_KIND_MP_FLOAT; + *(double *)&out[1] = mp_obj_get_float(obj); + } else if (mp_obj_is_str(obj)) { + kind = PROXY_KIND_MP_STR; + size_t len; + const char *str = mp_obj_str_get_data(obj, &len); + out[1] = len; + out[2] = (uintptr_t)str; + } else if (mp_obj_is_jsproxy(obj)) { + kind = PROXY_KIND_MP_JSPROXY; + out[1] = mp_obj_jsproxy_get_ref(obj); + } else { + if (mp_obj_is_callable(obj)) { + kind = PROXY_KIND_MP_CALLABLE; + } else { + kind = PROXY_KIND_MP_OBJECT; + } + size_t id = ((mp_obj_list_t *)MP_OBJ_TO_PTR(MP_STATE_PORT(proxy_c_ref)))->len; + mp_obj_list_append(MP_STATE_PORT(proxy_c_ref), obj); + out[1] = id; + } + out[0] = kind; +} + +void proxy_convert_mp_to_js_exc_cside(void *exc, uint32_t *out) { + out[0] = PROXY_KIND_MP_EXCEPTION; + vstr_t vstr; + mp_print_t print; + vstr_init_print(&vstr, 64, &print); + vstr_add_str(&vstr, qstr_str(mp_obj_get_type(MP_OBJ_FROM_PTR(exc))->name)); + vstr_add_char(&vstr, '\x04'); + mp_obj_print_exception(&print, MP_OBJ_FROM_PTR(exc)); + char *s = malloc(vstr_len(&vstr) + 1); + memcpy(s, vstr_str(&vstr), vstr_len(&vstr)); + out[1] = vstr_len(&vstr); + out[2] = (uintptr_t)s; + vstr_clear(&vstr); +} + +void proxy_c_to_js_call(uint32_t c_ref, uint32_t n_args, uint32_t *args_value, uint32_t *out) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_t args[4] = { mp_const_none, mp_const_none, mp_const_none, mp_const_none }; + for (size_t i = 0; i < n_args; ++i) { + args[i] = proxy_convert_js_to_mp_obj_cside(args_value + i * 3); + } + mp_obj_t obj = proxy_c_get_obj(c_ref); + mp_obj_t member = mp_call_function_n_kw(obj, n_args, 0, args); + nlr_pop(); + proxy_convert_mp_to_js_obj_cside(member, out); + } else { + // uncaught exception + proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out); + } +} + +void proxy_c_to_js_dir(uint32_t c_ref, uint32_t *out) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_t obj = proxy_c_get_obj(c_ref); + mp_obj_t dir; + if (mp_obj_is_dict_or_ordereddict(obj)) { + mp_map_t *map = mp_obj_dict_get_map(obj); + dir = mp_obj_new_list(0, NULL); + for (size_t i = 0; i < map->alloc; i++) { + if (mp_map_slot_is_filled(map, i)) { + mp_obj_list_append(dir, map->table[i].key); + } + } + } else { + mp_obj_t args[1] = { obj }; + dir = mp_builtin_dir_obj.fun.var(1, args); + } + nlr_pop(); + return proxy_convert_mp_to_js_obj_cside(dir, out); + } else { + // uncaught exception + return proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out); + } +} + +bool proxy_c_to_js_has_attr(uint32_t c_ref, const char *attr_in) { + mp_obj_t obj = proxy_c_get_obj(c_ref); + qstr attr = qstr_from_str(attr_in); + if (mp_obj_is_dict_or_ordereddict(obj)) { + mp_map_t *map = mp_obj_dict_get_map(obj); + mp_map_elem_t *elem = mp_map_lookup(map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP); + return elem != NULL; + } else { + mp_obj_t dest[2]; + mp_load_method_protected(obj, attr, dest, true); + if (dest[0] != MP_OBJ_NULL) { + return true; + } + } + return false; +} + +void proxy_c_to_js_lookup_attr(uint32_t c_ref, const char *attr_in, uint32_t *out) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_t obj = proxy_c_get_obj(c_ref); + qstr attr = qstr_from_str(attr_in); + mp_obj_t member; + if (mp_obj_is_dict_or_ordereddict(obj)) { + member = mp_obj_dict_get(obj, MP_OBJ_NEW_QSTR(attr)); + } else { + member = mp_load_attr(obj, attr); + } + nlr_pop(); + return proxy_convert_mp_to_js_obj_cside(member, out); + } else { + // uncaught exception + return proxy_convert_mp_to_js_exc_cside(nlr.ret_val, out); + } +} + +static bool proxy_c_to_js_store_helper(uint32_t c_ref, const char *attr_in, mp_obj_t value) { + mp_obj_t obj = proxy_c_get_obj(c_ref); + qstr attr = qstr_from_str(attr_in); + + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + if (mp_obj_is_dict_or_ordereddict(obj)) { + if (value == MP_OBJ_NULL) { + mp_obj_dict_delete(obj, MP_OBJ_NEW_QSTR(attr)); + } else { + mp_obj_dict_store(obj, MP_OBJ_NEW_QSTR(attr), value); + } + } else { + mp_store_attr(obj, attr, value); + } + nlr_pop(); + return true; + } else { + // uncaught exception + return false; + } +} + +bool proxy_c_to_js_store_attr(uint32_t c_ref, const char *attr_in, uint32_t *value_in) { + mp_obj_t value = proxy_convert_js_to_mp_obj_cside(value_in); + return proxy_c_to_js_store_helper(c_ref, attr_in, value); +} + +bool proxy_c_to_js_delete_attr(uint32_t c_ref, const char *attr_in) { + return proxy_c_to_js_store_helper(c_ref, attr_in, MP_OBJ_NULL); +} + +uint32_t proxy_c_to_js_get_type(uint32_t c_ref) { + mp_obj_t obj = proxy_c_get_obj(c_ref); + const mp_obj_type_t *type = mp_obj_get_type(obj); + if (type == &mp_type_tuple) { + return 1; + } else if (type == &mp_type_list) { + return 2; + } else if (type == &mp_type_dict) { + return 3; + } else { + return 4; + } +} + +void proxy_c_to_js_get_array(uint32_t c_ref, uint32_t *out) { + mp_obj_t obj = proxy_c_get_obj(c_ref); + size_t len; + mp_obj_t *items; + mp_obj_get_array(obj, &len, &items); + out[0] = len; + out[1] = (uintptr_t)items; +} + +void proxy_c_to_js_get_dict(uint32_t c_ref, uint32_t *out) { + mp_obj_t obj = proxy_c_get_obj(c_ref); + mp_map_t *map = mp_obj_dict_get_map(obj); + out[0] = map->alloc; + out[1] = (uintptr_t)map->table; +} diff --git a/ports/webassembly/proxy_c.h b/ports/webassembly/proxy_c.h new file mode 100644 index 0000000000..3e68d25049 --- /dev/null +++ b/ports/webassembly/proxy_c.h @@ -0,0 +1,58 @@ +/* + * 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. + */ +#ifndef MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H +#define MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H + +#include "py/obj.h" + +// proxy value number of items +#define PVN (3) + +typedef struct _mp_obj_jsproxy_t { + mp_obj_base_t base; + int ref; +} mp_obj_jsproxy_t; + +extern const mp_obj_type_t mp_type_jsproxy; + +void proxy_c_init(void); +mp_obj_t proxy_convert_js_to_mp_obj_cside(uint32_t *value); +void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out); +void proxy_convert_mp_to_js_exc_cside(void *exc, uint32_t *out); + +mp_obj_t mp_obj_new_jsproxy(int ref); +void mp_obj_jsproxy_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest); + +static inline bool mp_obj_is_jsproxy(mp_obj_t o) { + return mp_obj_get_type(o) == &mp_type_jsproxy; +} + +static inline int mp_obj_jsproxy_get_ref(mp_obj_t o) { + mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(o); + return self->ref; +} + +#endif // MICROPY_INCLUDED_WEBASSEMBLY_PROXY_C_H diff --git a/ports/webassembly/proxy_js.js b/ports/webassembly/proxy_js.js new file mode 100644 index 0000000000..1835bdfdfa --- /dev/null +++ b/ports/webassembly/proxy_js.js @@ -0,0 +1,222 @@ +/* + * 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. + */ + +// These constants should match the constants in proxy_c.c. + +const PROXY_KIND_MP_EXCEPTION = -1; +const PROXY_KIND_MP_NULL = 0; +const PROXY_KIND_MP_NONE = 1; +const PROXY_KIND_MP_BOOL = 2; +const PROXY_KIND_MP_INT = 3; +const PROXY_KIND_MP_FLOAT = 4; +const PROXY_KIND_MP_STR = 5; +const PROXY_KIND_MP_CALLABLE = 6; +const PROXY_KIND_MP_OBJECT = 7; +const PROXY_KIND_MP_JSPROXY = 8; + +const PROXY_KIND_JS_NULL = 1; +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; + +class PythonError extends Error { + constructor(exc_type, exc_details) { + super(exc_details); + this.name = "PythonError"; + this.type = exc_type; + } +} + +function proxy_js_init() { + globalThis.proxy_js_ref = [globalThis]; +} + +function proxy_call_python(target, argumentsList) { + let args = 0; + + // Strip trailing "undefined" arguments. + while ( + argumentsList.length > 0 && + argumentsList[argumentsList.length - 1] === undefined + ) { + argumentsList.pop(); + } + + if (argumentsList.length > 0) { + // TODO use stackAlloc/stackRestore? + args = Module._malloc(argumentsList.length * 3 * 4); + for (const i in argumentsList) { + proxy_convert_js_to_mp_obj_jsside( + argumentsList[i], + args + i * 3 * 4, + ); + } + } + const value = Module._malloc(3 * 4); + Module.ccall( + "proxy_c_to_js_call", + "null", + ["number", "number", "number", "pointer"], + [target, argumentsList.length, args, value], + ); + if (argumentsList.length > 0) { + Module._free(args); + } + return proxy_convert_mp_to_js_obj_jsside_with_free(value); +} + +function proxy_convert_js_to_mp_obj_jsside(js_obj, out) { + let kind; + if (js_obj === null) { + kind = PROXY_KIND_JS_NULL; + } else if (typeof js_obj === "boolean") { + kind = PROXY_KIND_JS_BOOLEAN; + Module.setValue(out + 4, js_obj, "i32"); + } else if (typeof js_obj === "number") { + if (Number.isInteger(js_obj)) { + kind = PROXY_KIND_JS_INTEGER; + Module.setValue(out + 4, js_obj, "i32"); + } else { + kind = PROXY_KIND_JS_DOUBLE; + // double must be stored to an address that's a multiple of 8 + const temp = (out + 4) & ~7; + Module.setValue(temp, js_obj, "double"); + const double_lo = Module.getValue(temp, "i32"); + const double_hi = Module.getValue(temp + 4, "i32"); + Module.setValue(out + 4, double_lo, "i32"); + Module.setValue(out + 8, double_hi, "i32"); + } + } else if (typeof js_obj === "string") { + kind = PROXY_KIND_JS_STRING; + const len = Module.lengthBytesUTF8(js_obj); + const buf = Module._malloc(len + 1); + Module.stringToUTF8(js_obj, buf, len + 1); + Module.setValue(out + 4, len, "i32"); + Module.setValue(out + 8, buf, "i32"); + } else if (js_obj instanceof PyProxy) { + kind = PROXY_KIND_JS_PYPROXY; + Module.setValue(out + 4, js_obj._ref, "i32"); + } else { + kind = PROXY_KIND_JS_OBJECT; + const id = proxy_js_ref.length; + proxy_js_ref[id] = js_obj; + Module.setValue(out + 4, id, "i32"); + } + Module.setValue(out + 0, kind, "i32"); +} + +function proxy_convert_js_to_mp_obj_jsside_force_double_proxy(js_obj, out) { + if (js_obj instanceof PyProxy) { + const kind = PROXY_KIND_JS_OBJECT; + const id = proxy_js_ref.length; + proxy_js_ref[id] = 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); + } +} + +function proxy_convert_mp_to_js_obj_jsside(value) { + const kind = Module.getValue(value, "i32"); + let obj; + if (kind === PROXY_KIND_MP_EXCEPTION) { + // Exception + const str_len = Module.getValue(value + 4, "i32"); + const str_ptr = Module.getValue(value + 8, "i32"); + const str = Module.UTF8ToString(str_ptr, str_len); + Module._free(str_ptr); + const str_split = str.split("\x04"); + throw new PythonError(str_split[0], str_split[1]); + } + if (kind === PROXY_KIND_MP_NULL) { + // MP_OBJ_NULL + throw new Error("NULL object"); + } + if (kind === PROXY_KIND_MP_NONE) { + // None + obj = null; + } else if (kind === PROXY_KIND_MP_BOOL) { + // bool + obj = Module.getValue(value + 4, "i32") ? true : false; + } else if (kind === PROXY_KIND_MP_INT) { + // int + obj = Module.getValue(value + 4, "i32"); + } else if (kind === PROXY_KIND_MP_FLOAT) { + // float + // double must be loaded from an address that's a multiple of 8 + const temp = (value + 4) & ~7; + const double_lo = Module.getValue(value + 4, "i32"); + const double_hi = Module.getValue(value + 8, "i32"); + Module.setValue(temp, double_lo, "i32"); + Module.setValue(temp + 4, double_hi, "i32"); + obj = Module.getValue(temp, "double"); + } else if (kind === PROXY_KIND_MP_STR) { + // str + const str_len = Module.getValue(value + 4, "i32"); + const str_ptr = Module.getValue(value + 8, "i32"); + obj = Module.UTF8ToString(str_ptr, str_len); + } else if (kind === PROXY_KIND_MP_JSPROXY) { + // js proxy + const id = Module.getValue(value + 4, "i32"); + obj = proxy_js_ref[id]; + } else { + // obj + const id = Module.getValue(value + 4, "i32"); + if (kind === PROXY_KIND_MP_CALLABLE) { + obj = (...args) => { + return proxy_call_python(id, args); + }; + } else { + // PROXY_KIND_MP_OBJECT + const target = new PyProxy(id); + obj = new Proxy(target, py_proxy_handler); + } + } + return obj; +} + +function proxy_convert_mp_to_js_obj_jsside_with_free(value) { + const ret = proxy_convert_mp_to_js_obj_jsside(value); + Module._free(value); + return ret; +} + +function python_index_semantics(target, index_in) { + let index = index_in; + if (typeof index === "number") { + if (index < 0) { + index += target.length; + } + if (index < 0 || index >= target.length) { + throw new PythonError("IndexError", "index out of range"); + } + } + return index; +}