diff --git a/README.md b/README.md index e472bd9f6a..b11c664e89 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,11 @@ You can build the WebAssembly WASI file with: cargo build --release --target wasm32-wasip1 --features="freeze-stdlib" ``` -> Note: we use the `freeze-stdlib` to include the standard library inside the binary. You also have to run once `rustup target add wasm32-wasip1`. +> [!NOTE] +> we use the `freeze-stdlib` to include the standard library inside the binary. You also have to run once `rustup target add wasm32-wasip1`. + +> [!IMPORTANT] +> Both `wasip1` and `wasip2` are supported, but `p2` requires nightly to build. ### JIT (Just in time) compiler diff --git a/wasm/demo/package.json b/wasm/demo/package.json index 487f46fbc1..bfd0fc0912 100644 --- a/wasm/demo/package.json +++ b/wasm/demo/package.json @@ -4,7 +4,7 @@ "description": "Bindings to the RustPython library for WebAssembly", "main": "index.js", "dependencies": { - "@codemirror/lang-python": "^6.1.6", + "@codemirror/lang-python": "^6.2.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.3.0", "codemirror": "^6.0.1", diff --git a/wasm/demo/src/style.css b/wasm/demo/src/style.css index f9892745ac..53f066cb52 100644 --- a/wasm/demo/src/style.css +++ b/wasm/demo/src/style.css @@ -17,7 +17,11 @@ textarea { #run-btn { width: 6em; height: 2em; - font-size: 24px; + font-size: 20px; + border-radius: 8px; + margin: 8px; + background-color: #00913a; + color: white; } #error { diff --git a/wasm/lib/Cargo.toml b/wasm/lib/Cargo.toml index 0e35292a2c..c28a689ebf 100644 --- a/wasm/lib/Cargo.toml +++ b/wasm/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpython_wasm" -description = "A Python-3 (CPython >= 3.5.0) Interpreter written in Rust, compiled to WASM" +description = "A Python-3 (CPython >= 3.13) Interpreter written in Rust, compiled to WASM" version.workspace = true authors.workspace = true edition.workspace = true @@ -39,11 +39,12 @@ web-sys = { version = "0.3", features = [ "console", "Document", "Element", - "Window", "Headers", + "HtmlElement", "Request", "RequestInit", - "Response" + "Response", + "Window", ] } [package.metadata.wasm-pack.profile.release] diff --git a/wasm/lib/Lib/browser.py b/wasm/lib/Lib/browser.py deleted file mode 100644 index 515fe2e673..0000000000 --- a/wasm/lib/Lib/browser.py +++ /dev/null @@ -1,76 +0,0 @@ -from _browser import ( - fetch, - request_animation_frame, - cancel_animation_frame, - Document, - Element, - load_module, -) - -from _js import JSValue, Promise -from _window import window - -__all__ = [ - "jsstr", - "jsclosure", - "jsclosure_once", - "jsfloat", - "NULL", - "UNDEFINED", - "alert", - "confirm", - "prompt", - "fetch", - "request_animation_frame", - "cancel_animation_frame", - "Document", - "Element", - "load_module", - "JSValue", - "Promise", -] - - -jsstr = window.new_from_str -jsclosure = window.new_closure -jsclosure_once = window.new_closure_once -_jsfloat = window.new_from_float - -UNDEFINED = window.undefined() -NULL = window.null() - - -def jsfloat(n): - return _jsfloat(float(n)) - - -_alert = window.get_prop("alert") - - -def alert(msg): - if type(msg) != str: - raise TypeError("msg must be a string") - _alert.call(jsstr(msg)) - - -_confirm = window.get_prop("confirm") - - -def confirm(msg): - if type(msg) != str: - raise TypeError("msg must be a string") - return _confirm.call(jsstr(msg)).as_bool() - - -_prompt = window.get_prop("prompt") - - -def prompt(msg, default_val=None): - if type(msg) != str: - raise TypeError("msg must be a string") - if default_val is not None and type(default_val) != str: - raise TypeError("default_val must be a string") - - return _prompt.call( - jsstr(msg), jsstr(default_val) if default_val else UNDEFINED - ).as_str() diff --git a/wasm/lib/Lib/browser/__init__.py b/wasm/lib/Lib/browser/__init__.py new file mode 100644 index 0000000000..9751155116 --- /dev/null +++ b/wasm/lib/Lib/browser/__init__.py @@ -0,0 +1,34 @@ +from _browser import ( + Document, + Element, + load_module, +) + +from _js import JSValue, Promise +from .window import alert, atob, btoa, confirm, prompt, request_animation_frame, cancel_animation_frame + +from .util import jsstr, jsclosure, jsclosure_once, jsfloat, NULL, UNDEFINED + +__all__ = [ + "jsstr", + "jsclosure", + "jsclosure_once", + "jsfloat", + "NULL", + "UNDEFINED", + "alert", + "atob", + "btoa", + "confirm", + "prompt", + "fetch", + "request_animation_frame", + "cancel_animation_frame", + "Document", + "Element", + "load_module", + "JSValue", + "Promise", +] + + diff --git a/wasm/lib/Lib/browser/util.py b/wasm/lib/Lib/browser/util.py new file mode 100644 index 0000000000..1bb21d7c4c --- /dev/null +++ b/wasm/lib/Lib/browser/util.py @@ -0,0 +1,11 @@ +from _browser import window # type: ignore + +jsstr = window.new_from_str +jsclosure = window.new_closure +jsclosure_once = window.new_closure_once + +def jsfloat(n): + return window.new_from_float(float(n)) + +UNDEFINED = window.undefined() +NULL = window.null() diff --git a/wasm/lib/Lib/browser/window.py b/wasm/lib/Lib/browser/window.py new file mode 100644 index 0000000000..31b3f67bb1 --- /dev/null +++ b/wasm/lib/Lib/browser/window.py @@ -0,0 +1,99 @@ +from browser import window as Window # type: ignore +from .util import jsint, jsstr, UNDEFINED + +__all__ = [ + "Window", + "alert", + "atob", + "btoa", + "cancel_animation_frame", + "close", + "confirm", + "fetch", + "focus", + "print", + "prompt", + "request_animation_frame", + "resize_by", + "resize_to", +] + +_alert = Window.get_prop("alert") + +def alert(msg = None): + if msg is None: + return _alert.call() + if type(msg) != str: + raise TypeError("msg must be a string") + _alert.call(jsstr(msg)) + +_atob = Window.get_prop("atob") + +def atob(data): + if type(data) != str: + raise TypeError("data must be a string") + return _atob.call(jsstr(data)).as_str() + +_btoa = Window.get_prop("btoa") +def btoa(data): + if type(data) != str: + raise TypeError("data must be a string") + return _btoa.call(jsstr(data)).as_str() + + +from _browser import cancel_animation_frame + +_close = Window.get_prop("close") +def close(): + return _close.call() + +_confirm = Window.get_prop("confirm") +def confirm(msg): + if type(msg) != str: + raise TypeError("msg must be a string") + return _confirm.call(jsstr(msg)).as_bool() + +from _browser import fetch + +_focus = Window.get_prop("focus") +def focus(): + return _focus.call() + +_print = Window.get_prop("print") +def print(): + return _print.call() + +_prompt = Window.get_prop("prompt") +def prompt(msg, default_val=None): + if type(msg) != str: + raise TypeError("msg must be a string") + if default_val is not None and type(default_val) != str: + raise TypeError("default_val must be a string") + + return _prompt.call( + jsstr(msg), jsstr(default_val) if default_val else UNDEFINED + ).as_str() + +from _browser import request_animation_frame + +_resize_by = Window.get_prop("resizeBy") + +def resize_by(x, y): + if type(x) != int: + raise TypeError("x must be an int") + if type(y) != int: + raise TypeError("y must be an int") + _resize_by.call(jsint(x), jsint(y)) + +_resize_to = Window.get_prop("resizeTo") + +def resize_to(x, y): + if type(x) != int: + raise TypeError("x must be an int") + if type(y) != int: + raise TypeError("y must be an int") + _resize_to.call(jsint(x), jsint(y)) + +_stop = Window.get_prop("stop") +def stop(): + return _stop.call() diff --git a/wasm/lib/src/browser_module.rs b/wasm/lib/src/browser_module.rs index f8d1b2ebc3..089d572aa6 100644 --- a/wasm/lib/src/browser_module.rs +++ b/wasm/lib/src/browser_module.rs @@ -1,18 +1,17 @@ use rustpython_vm::VirtualMachine; pub(crate) use _browser::make_module; +use crate::wasm_builtins::window; +use rustpython_vm::PyRef; +use rustpython_vm::builtins::PyModule; +use rustpython_vm::PyPayload; #[pymodule] mod _browser { use crate::{convert, js_module::PyPromise, vm_class::weak_vm, wasm_builtins::window}; use js_sys::Promise; use rustpython_vm::{ - PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyDictRef, PyStrRef}, - class::PyClassImpl, - convert::ToPyObject, - function::{ArgCallable, OptionalArg}, - import::import_source, + builtins::{PyDictRef, PyStrRef}, class::PyClassImpl, convert::ToPyObject, function::{ArgCallable, OptionalArg}, import::import_source, types::Constructor, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine }; use wasm_bindgen::{JsCast, prelude::*}; use wasm_bindgen_futures::JsFuture; @@ -168,6 +167,26 @@ mod _browser { #[pyclass] impl Document { + #[pygetset] + fn body(&self, vm: &VirtualMachine) -> PyResult { + let body = self + .doc + .body() + .map(|elem| HTMLElement { elem }) + .to_pyobject(vm); + Ok(body) + } + + #[pymethod] + fn get_element_by_id(&self, id: PyStrRef, vm: &VirtualMachine) -> PyResult { + let elem = self + .doc + .get_element_by_id(id.as_str()) + .map(|elem| Element { elem }) + .to_pyobject(vm); + Ok(elem) + } + #[pymethod] fn query(&self, query: PyStrRef, vm: &VirtualMachine) -> PyResult { let elem = self @@ -178,6 +197,19 @@ mod _browser { .to_pyobject(vm); Ok(elem) } + + #[pygetset] + fn title(&self, vm: &VirtualMachine) -> PyResult { + let title = self + .doc + .title(); + Ok(vm.ctx.new_str(title).into()) + } + + #[pygetset(setter)] + fn set_title(&self, title: PyStrRef) { + self.doc.set_title(title.as_str()); + } } #[pyattr] @@ -221,6 +253,27 @@ mod _browser { } } + #[pyattr] + #[pyclass(module = "browser", name)] + #[derive(Debug, PyPayload)] + struct HTMLElement { + elem: web_sys::HtmlElement, + } + + #[pyclass] + impl HTMLElement { + #[pygetset] + fn title(&self, vm: &VirtualMachine) -> PyResult { + let title = self.elem.title(); + Ok(vm.ctx.new_str(title).into()) + } + + #[pygetset(setter)] + fn set_title(&self, title: PyStrRef) { + self.elem.set_title(title.as_str()); + } + } + #[pyfunction] fn load_module(module: PyStrRef, path: PyStrRef, vm: &VirtualMachine) -> PyResult { let weak_vm = weak_vm(vm); @@ -257,7 +310,17 @@ mod _browser { } } +fn init_browser_module(vm: &VirtualMachine) -> PyRef { + let module = make_module(vm); + + extend_module!(vm, &module, { + "window" => crate::js_module::PyJsValue::new(crate::wasm_builtins::window()).into_ref(&vm.ctx), + }); + + module +} + pub fn setup_browser_module(vm: &mut VirtualMachine) { - vm.add_native_module("_browser".to_owned(), Box::new(make_module)); + vm.add_native_module("_browser".to_owned(), Box::new(init_browser_module)); vm.add_frozen(py_freeze!(dir = "Lib")); } diff --git a/wasm/lib/src/vm_class.rs b/wasm/lib/src/vm_class.rs index bbd895c989..d4185af300 100644 --- a/wasm/lib/src/vm_class.rs +++ b/wasm/lib/src/vm_class.rs @@ -25,19 +25,6 @@ pub(crate) struct StoredVirtualMachine { held_objects: RefCell>, } -#[pymodule] -mod _window {} - -fn init_window_module(vm: &VirtualMachine) -> PyRef { - let module = _window::make_module(vm); - - extend_module!(vm, &module, { - "window" => js_module::PyJsValue::new(wasm_builtins::window()).into_ref(&vm.ctx), - }); - - module -} - impl StoredVirtualMachine { fn new(id: String, inject_browser_module: bool) -> StoredVirtualMachine { let mut scope = None; @@ -54,7 +41,6 @@ impl StoredVirtualMachine { js_module::setup_js_module(vm); if inject_browser_module { - vm.add_native_module("_window".to_owned(), Box::new(init_window_module)); setup_browser_module(vm); }