diff --git a/Cargo.lock b/Cargo.lock index 9f5ffa3972..dfab5148a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,15 @@ dependencies = [ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "constant_time_eq" version = "0.1.3" @@ -207,7 +216,7 @@ name = "docopt" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)", @@ -292,6 +301,11 @@ name = "fuchsia-zircon-sys" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "futures" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "generic-array" version = "0.9.0" @@ -399,7 +413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "lazy_static" -version = "1.0.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -728,7 +742,7 @@ dependencies = [ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "caseless 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lexical 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "num-bigint 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -750,10 +764,13 @@ name = "rustpython_wasm" version = "0.1.0" dependencies = [ "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", "js-sys 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "rustpython_parser 0.0.1", "rustpython_vm 0.1.0", "wasm-bindgen 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "web-sys 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -855,7 +872,7 @@ name = "string_cache" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "new_debug_unreachable 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", "precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -970,7 +987,7 @@ name = "thread_local" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1044,7 +1061,7 @@ name = "wasm-bindgen-backend" version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", "quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1052,6 +1069,16 @@ dependencies = [ "wasm-bindgen-shared 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.29" @@ -1196,6 +1223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18" "checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6c5dd2c094474ec60a6acaf31780af270275e3153bafff2db5995b715295762e" "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" "checksum diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3c2b69f912779fbb121ceb775d74d51e915af17aaebc38d28a592843a2dd0a3a" "checksum digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "00a49051fef47a72c9623101b19bd71924a45cca838826caae3eaa4d00772603" @@ -1211,6 +1239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fixedbitset 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b" "checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" "checksum heck 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea04fa3ead4e05e51a7c806fc07271fdbde4e246a6c6d1efd52e72230b771b82" "checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e" @@ -1221,7 +1250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum lalrpop 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ba451f7bd819b7afc99d4cf4bdcd5a4861e64955ba9680ac70df3a50625ad6cf" "checksum lalrpop-snap 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)" = "60013fd6be14317d43f47658b1440956a9ca48a9ed0257e0e0a59aac13e43a1f" "checksum lalrpop-util 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)" = "60c6c48ba857cd700673ce88907cadcdd7e2cd7783ed02378537c5ffd4f6460c" -"checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" +"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" "checksum lexical 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e4fac65df7e751b57bb3a334c346239cb4ce2601907d698726ceeb82a54ba4ef" "checksum lexical-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "025babf624c0c2b4bed1373efd684d5d0b2eecd61138d26ec3eec77bf0f2e33d" "checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" @@ -1298,6 +1327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum wasm-bindgen 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "91f95b8f30407b9ca0c2de157281d3828bbed1fc1f55bea6eb54f40c52ec75ec" "checksum wasm-bindgen-backend 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "ab7c242ebcb45bae45340986c48d1853eb2c1c52ff551f7724951b62a2c51429" +"checksum wasm-bindgen-futures 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "d1784e7401a90119b2a4e8ec9c8d37c3594c3e3bb9ba24533ee1969eebaf0485" "checksum wasm-bindgen-macro 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "6e353f83716dec9a3597b5719ef88cb6c9e461ec16528f38aa023d3224b4e569" "checksum wasm-bindgen-macro-support 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "3cc90b65fe69c3dd5a09684517dc79f42b847baa2d479c234d125e0a629d9b0a" "checksum wasm-bindgen-shared 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "a71a37df4f5845025f96f279d20bbe5b19cbcb77f5410a3a90c6c544d889a162" diff --git a/vm/src/builtins.rs b/vm/src/builtins.rs index 8dc455ccd1..a14925add0 100644 --- a/vm/src/builtins.rs +++ b/vm/src/builtins.rs @@ -18,6 +18,8 @@ use crate::pyobject::{ AttributeProtocol, IdProtocol, PyContext, PyFuncArgs, PyObject, PyObjectPayload, PyObjectRef, PyResult, Scope, TypeProtocol, }; + +#[cfg(not(target_arch = "wasm32"))] use crate::stdlib::io::io_open; use crate::vm::VirtualMachine; @@ -702,7 +704,7 @@ fn builtin_sum(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { // builtin___import__ pub fn make_module(ctx: &PyContext) -> PyObjectRef { - py_module!(ctx, "__builtins__", { + let py_mod = py_module!(ctx, "__builtins__", { //set __name__ fixes: https://github.com/RustPython/RustPython/issues/146 "__name__" => ctx.new_str(String::from("__main__")), @@ -747,7 +749,6 @@ pub fn make_module(ctx: &PyContext) -> PyObjectRef { "min" => ctx.new_rustfunc(builtin_min), "object" => ctx.object(), "oct" => ctx.new_rustfunc(builtin_oct), - "open" => ctx.new_rustfunc(io_open), "ord" => ctx.new_rustfunc(builtin_ord), "next" => ctx.new_rustfunc(builtin_next), "pow" => ctx.new_rustfunc(builtin_pow), @@ -789,7 +790,12 @@ pub fn make_module(ctx: &PyContext) -> PyObjectRef { "StopIteration" => ctx.exceptions.stop_iteration.clone(), "ZeroDivisionError" => ctx.exceptions.zero_division_error.clone(), "KeyError" => ctx.exceptions.key_error.clone(), - }) + }); + + #[cfg(not(target_arch = "wasm32"))] + ctx.set_attr(&py_mod, "open", ctx.new_rustfunc(io_open)); + + py_mod } pub fn builtin_build_class_(vm: &mut VirtualMachine, mut args: PyFuncArgs) -> PyResult { diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 874f3e2a2c..f785e294d7 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -28,7 +28,7 @@ extern crate rustpython_parser; // This is above everything else so that the defined macros are available everywhere #[macro_use] -mod macros; +pub mod macros; mod builtins; pub mod bytecode; diff --git a/vm/src/macros.rs b/vm/src/macros.rs index 3d6268e3cc..de94fc4415 100644 --- a/vm/src/macros.rs +++ b/vm/src/macros.rs @@ -1,15 +1,18 @@ // count number of tokens given as arguments. // see: https://danielkeep.github.io/tlborm/book/blk-counting.html +#[macro_export] macro_rules! replace_expr { ($_t:tt $sub:expr) => { $sub }; } +#[macro_export] macro_rules! count_tts { ($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*}; } +#[macro_export] macro_rules! type_check { ($vm:ident, $args:ident, $arg_count:ident, $arg_name:ident, $arg_type:expr) => { // None indicates that we have no type requirement (i.e. we accept any type) @@ -31,6 +34,7 @@ macro_rules! type_check { }; } +#[macro_export] macro_rules! arg_check { ( $vm: ident, $args:ident ) => { // Zero-arg case @@ -94,6 +98,7 @@ macro_rules! arg_check { }; } +#[macro_export] macro_rules! no_kwargs { ( $vm: ident, $args:ident ) => { // Zero-arg case diff --git a/vm/src/stdlib/mod.rs b/vm/src/stdlib/mod.rs index 5b0ef01f58..a0dcd9bff6 100644 --- a/vm/src/stdlib/mod.rs +++ b/vm/src/stdlib/mod.rs @@ -1,10 +1,8 @@ mod ast; mod dis; -pub mod io; mod json; mod keyword; mod math; -mod os; mod pystruct; mod random; mod re; @@ -15,6 +13,11 @@ mod types; mod weakref; use std::collections::HashMap; +#[cfg(not(target_arch = "wasm32"))] +pub mod io; +#[cfg(not(target_arch = "wasm32"))] +mod os; + use crate::pyobject::{PyContext, PyObjectRef}; pub type StdlibInitFunc = fn(&PyContext) -> PyObjectRef; @@ -23,11 +26,9 @@ pub fn get_module_inits() -> HashMap { let mut modules = HashMap::new(); modules.insert("ast".to_string(), ast::mk_module as StdlibInitFunc); modules.insert("dis".to_string(), dis::mk_module as StdlibInitFunc); - modules.insert("io".to_string(), io::mk_module as StdlibInitFunc); modules.insert("json".to_string(), json::mk_module as StdlibInitFunc); modules.insert("keyword".to_string(), keyword::mk_module as StdlibInitFunc); modules.insert("math".to_string(), math::mk_module as StdlibInitFunc); - modules.insert("os".to_string(), os::mk_module as StdlibInitFunc); modules.insert("re".to_string(), re::mk_module as StdlibInitFunc); modules.insert("random".to_string(), random::mk_module as StdlibInitFunc); modules.insert("string".to_string(), string::mk_module as StdlibInitFunc); @@ -39,5 +40,13 @@ pub fn get_module_inits() -> HashMap { ); modules.insert("types".to_string(), types::mk_module as StdlibInitFunc); modules.insert("_weakref".to_string(), weakref::mk_module as StdlibInitFunc); + + // disable some modules on WASM + #[cfg(not(target_arch = "wasm32"))] + { + modules.insert("io".to_string(), io::mk_module as StdlibInitFunc); + modules.insert("os".to_string(), os::mk_module as StdlibInitFunc); + } + modules } diff --git a/vm/src/vm.rs b/vm/src/vm.rs index f6d8c37c99..149a5313cf 100644 --- a/vm/src/vm.rs +++ b/vm/src/vm.rs @@ -39,6 +39,7 @@ pub struct VirtualMachine { pub stdlib_inits: HashMap, pub ctx: PyContext, pub current_frame: Option, + pub wasm_id: Option, } impl VirtualMachine { @@ -61,6 +62,7 @@ impl VirtualMachine { stdlib_inits, ctx, current_frame: None, + wasm_id: None, } } diff --git a/wasm/lib/Cargo.toml b/wasm/lib/Cargo.toml index ac5da7450a..103d069d9d 100644 --- a/wasm/lib/Cargo.toml +++ b/wasm/lib/Cargo.toml @@ -11,12 +11,25 @@ edition = "2018" crate-type = ["cdylib", "rlib"] [dependencies] -rustpython_parser = {path = "../../parser"} -rustpython_vm = {path = "../../vm"} +rustpython_parser = { path = "../../parser" } +rustpython_vm = { path = "../../vm" } cfg-if = "0.1.2" wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.3" js-sys = "0.3" +futures = "0.1" +console_error_panic_hook = "0.1" [dependencies.web-sys] version = "0.3" -features = [ "console", "Document", "Element", "HtmlTextAreaElement", "Window" ] +features = [ + "console", + "Document", + "Element", + "HtmlTextAreaElement", + "Window", + "Headers", + "Request", + "RequestInit", + "Response" +] diff --git a/wasm/lib/src/convert.rs b/wasm/lib/src/convert.rs new file mode 100644 index 0000000000..df63c959e6 --- /dev/null +++ b/wasm/lib/src/convert.rs @@ -0,0 +1,192 @@ +use crate::vm_class::{AccessibleVM, WASMVirtualMachine}; +use js_sys::{Array, ArrayBuffer, Object, Reflect, Uint8Array}; +use rustpython_vm::obj::{objbytes, objtype}; +use rustpython_vm::pyobject::{self, PyFuncArgs, PyObjectRef, PyResult}; +use rustpython_vm::VirtualMachine; +use wasm_bindgen::{closure::Closure, prelude::*, JsCast}; + +pub fn py_str_err(vm: &mut VirtualMachine, py_err: &PyObjectRef) -> String { + vm.to_pystr(&py_err) + .unwrap_or_else(|_| "Error, and error getting error message".into()) +} + +pub fn js_py_typeerror(vm: &mut VirtualMachine, js_err: JsValue) -> PyObjectRef { + let msg = js_err.unchecked_into::().to_string(); + vm.new_type_error(msg.into()) +} + +pub fn py_to_js(vm: &mut VirtualMachine, py_obj: PyObjectRef) -> JsValue { + if let Some(ref wasm_id) = vm.wasm_id { + if objtype::isinstance(&py_obj, &vm.ctx.function_type()) { + let wasm_vm = WASMVirtualMachine { + id: wasm_id.clone(), + }; + let mut py_obj = Some(py_obj); + let closure = + move |args: Option, kwargs: Option| -> Result { + let py_obj = match wasm_vm.assert_valid() { + Ok(_) => py_obj.clone().expect("py_obj to be valid if VM is valid"), + Err(err) => { + py_obj = None; + return Err(err); + } + }; + let acc_vm = AccessibleVM::from(wasm_vm.clone()); + let vm = &mut acc_vm + .upgrade() + .expect("acc. VM to be invalid when WASM vm is valid"); + let mut py_func_args = rustpython_vm::pyobject::PyFuncArgs::default(); + if let Some(ref args) = args { + for arg in args.values() { + py_func_args.args.push(js_to_py(vm, arg?)); + } + } + if let Some(ref kwargs) = kwargs { + for pair in object_entries(kwargs) { + let (key, val) = pair?; + py_func_args + .kwargs + .push((js_sys::JsString::from(key).into(), js_to_py(vm, val))); + } + } + let result = vm.invoke(py_obj.clone(), py_func_args); + pyresult_to_jsresult(vm, result) + }; + let closure = Closure::wrap(Box::new(closure) + as Box, Option) -> Result>); + let func = closure.as_ref().clone(); + + // TODO: Come up with a way of managing closure handles + closure.forget(); + + return func; + } + } + if objtype::isinstance(&py_obj, &vm.ctx.bytes_type()) + || objtype::isinstance(&py_obj, &vm.ctx.bytearray_type()) + { + let bytes = objbytes::get_value(&py_obj); + let arr = Uint8Array::new_with_length(bytes.len() as u32); + for (i, byte) in bytes.iter().enumerate() { + Reflect::set(&arr, &(i as u32).into(), &(*byte).into()) + .expect("setting Uint8Array value failed"); + } + arr.into() + } else { + let dumps = rustpython_vm::import::import( + vm, + std::path::PathBuf::default(), + "json", + &Some("dumps".into()), + ) + .expect("Couldn't get json.dumps function"); + match vm.invoke(dumps, pyobject::PyFuncArgs::new(vec![py_obj], vec![])) { + Ok(value) => { + let json = vm.to_pystr(&value).unwrap(); + js_sys::JSON::parse(&json).unwrap_or(JsValue::UNDEFINED) + } + Err(_) => JsValue::UNDEFINED, + } + } +} + +pub fn object_entries(obj: &Object) -> impl Iterator> { + Object::entries(obj).values().into_iter().map(|pair| { + pair.map(|pair| { + let key = Reflect::get(&pair, &"0".into()).unwrap(); + let val = Reflect::get(&pair, &"1".into()).unwrap(); + (key, val) + }) + }) +} + +pub fn pyresult_to_jsresult(vm: &mut VirtualMachine, result: PyResult) -> Result { + result + .map(|value| py_to_js(vm, value)) + .map_err(|err| py_str_err(vm, &err).into()) +} + +pub fn js_to_py(vm: &mut VirtualMachine, js_val: JsValue) -> PyObjectRef { + if js_val.is_object() { + if Array::is_array(&js_val) { + let js_arr: Array = js_val.into(); + let elems = js_arr + .values() + .into_iter() + .map(|val| js_to_py(vm, val.expect("Iteration over array failed"))) + .collect(); + vm.ctx.new_list(elems) + } else if ArrayBuffer::is_view(&js_val) || js_val.is_instance_of::() { + // unchecked_ref because if it's not an ArrayByffer it could either be a TypedArray + // or a DataView, but they all have a `buffer` property + let u8_array = js_sys::Uint8Array::new( + &js_val + .dyn_ref::() + .cloned() + .unwrap_or_else(|| js_val.unchecked_ref::().buffer()), + ); + let mut vec = Vec::with_capacity(u8_array.length() as usize); + // TODO: use Uint8Array::copy_to once updating js_sys doesn't break everything + u8_array.for_each(&mut |byte, _, _| vec.push(byte)); + vm.ctx.new_bytes(vec) + } else { + let dict = vm.new_dict(); + for pair in object_entries(&Object::from(js_val)) { + let (key, val) = pair.expect("iteration over object to not fail"); + let py_val = js_to_py(vm, val); + vm.ctx + .set_item(&dict, &String::from(js_sys::JsString::from(key)), py_val); + } + dict + } + } else if js_val.is_function() { + let func = js_sys::Function::from(js_val); + vm.ctx.new_rustfunc( + move |vm: &mut VirtualMachine, args: PyFuncArgs| -> PyResult { + let func = func.clone(); + let this = Object::new(); + for (k, v) in args.kwargs { + Reflect::set(&this, &k.into(), &py_to_js(vm, v)) + .expect("property to be settable"); + } + let js_args = Array::new(); + for v in args.args { + js_args.push(&py_to_js(vm, v)); + } + func.apply(&this, &js_args) + .map(|val| js_to_py(vm, val)) + .map_err(|err| js_to_py(vm, err)) + }, + ) + } else if let Some(err) = js_val.dyn_ref::() { + let exc_type = match String::from(err.name()).as_str() { + "TypeError" => &vm.ctx.exceptions.type_error, + "ReferenceError" => &vm.ctx.exceptions.name_error, + "SyntaxError" => &vm.ctx.exceptions.syntax_error, + _ => &vm.ctx.exceptions.exception_type, + } + .clone(); + vm.new_exception(exc_type, err.message().into()) + } else if js_val.is_undefined() { + // Because `JSON.stringify(undefined)` returns undefined + vm.get_none() + } else { + let loads = rustpython_vm::import::import( + vm, + std::path::PathBuf::default(), + "json", + &Some("loads".into()), + ) + .expect("json.loads function to be available"); + + let json = match js_sys::JSON::stringify(&js_val) { + Ok(json) => String::from(json), + Err(_) => return vm.get_none(), + }; + let py_json = vm.new_str(json); + + vm.invoke(loads, pyobject::PyFuncArgs::new(vec![py_json], vec![])) + // can safely unwrap because we know it's valid JSON + .unwrap() + } +} diff --git a/wasm/lib/src/lib.rs b/wasm/lib/src/lib.rs index 4595c882ea..2c69866e90 100644 --- a/wasm/lib/src/lib.rs +++ b/wasm/lib/src/lib.rs @@ -1,142 +1,30 @@ -mod wasm_builtins; +pub mod convert; +pub mod vm_class; +pub mod wasm_builtins; +extern crate futures; extern crate js_sys; extern crate rustpython_vm; extern crate wasm_bindgen; extern crate web_sys; -use js_sys::{Array, Object, Reflect, TypeError}; -use rustpython_vm::compile; -use rustpython_vm::pyobject::{self, PyFuncArgs, PyObjectRef, PyResult}; -use rustpython_vm::VirtualMachine; -use std::error::Error; -use wasm_bindgen::{prelude::*, JsCast}; +use js_sys::{Object, Reflect, TypeError}; +use wasm_bindgen::prelude::*; -// Hack to comment out wasm-bindgen's typescript definitions -#[wasm_bindgen(typescript_custom_section)] -const TS_CMT_START: &'static str = "/*"; - -fn py_str_err(vm: &mut VirtualMachine, py_err: &PyObjectRef) -> String { - vm.to_pystr(&py_err) - .unwrap_or_else(|_| "Error, and error getting error message".into()) -} +pub use crate::vm_class::*; -fn py_to_js(vm: &mut VirtualMachine, py_obj: PyObjectRef) -> JsValue { - let dumps = rustpython_vm::import::import( - vm, - std::path::PathBuf::default(), - "json", - &Some("dumps".into()), - ) - .expect("Couldn't get json.dumps function"); - match vm.invoke(dumps, pyobject::PyFuncArgs::new(vec![py_obj], vec![])) { - Ok(value) => { - let json = vm.to_pystr(&value).unwrap(); - js_sys::JSON::parse(&json).unwrap_or(JsValue::UNDEFINED) - } - Err(_) => JsValue::UNDEFINED, - } -} +const PY_EVAL_VM_ID: &str = "__py_eval_vm"; -fn js_to_py(vm: &mut VirtualMachine, js_val: JsValue) -> PyObjectRef { - if js_val.is_object() { - if Array::is_array(&js_val) { - let js_arr: Array = js_val.into(); - let elems = js_arr - .values() - .into_iter() - .map(|val| js_to_py(vm, val.expect("Iteration over array failed"))) - .collect(); - vm.ctx.new_list(elems) - } else { - let dict = vm.new_dict(); - for pair in Object::entries(&Object::from(js_val)).values() { - let pair = pair.expect("Iteration over object failed"); - let key = Reflect::get(&pair, &"0".into()).unwrap(); - let val = Reflect::get(&pair, &"1".into()).unwrap(); - let py_val = js_to_py(vm, val); - vm.ctx - .set_item(&dict, &String::from(js_sys::JsString::from(key)), py_val); - } - dict - } - } else if js_val.is_function() { - let func = js_sys::Function::from(js_val); - vm.ctx.new_rustfunc( - move |vm: &mut VirtualMachine, args: PyFuncArgs| -> PyResult { - let func = func.clone(); - let this = Object::new(); - for (k, v) in args.kwargs { - Reflect::set(&this, &k.into(), &py_to_js(vm, v)) - .expect("Couldn't set this property"); - } - let js_args = Array::new(); - for v in args.args { - js_args.push(&py_to_js(vm, v)); - } - func.apply(&this, &js_args) - .map(|val| js_to_py(vm, val)) - .map_err(|err| js_to_py(vm, err)) - }, - ) - } else if let Some(err) = js_val.dyn_ref::() { - let exc_type = match String::from(err.name()).as_str() { - "TypeError" => &vm.ctx.exceptions.type_error, - "ReferenceError" => &vm.ctx.exceptions.name_error, - "SyntaxError" => &vm.ctx.exceptions.syntax_error, - _ => &vm.ctx.exceptions.exception_type, - } - .clone(); - vm.new_exception(exc_type, err.message().into()) - } else if js_val.is_undefined() { - // Because `JSON.stringify(undefined)` returns undefined - vm.get_none() - } else { - let loads = rustpython_vm::import::import( - vm, - std::path::PathBuf::default(), - "json", - &Some("loads".into()), - ) - .expect("Couldn't get json.loads function"); - - let json = match js_sys::JSON::stringify(&js_val) { - Ok(json) => String::from(json), - Err(_) => return vm.get_none(), - }; - let py_json = vm.new_str(json); - - vm.invoke(loads, pyobject::PyFuncArgs::new(vec![py_json], vec![])) - // can safely unwrap because we know it's valid JSON - .unwrap() - } -} +extern crate console_error_panic_hook; -fn base_scope(vm: &mut VirtualMachine) -> PyObjectRef { - let builtins = vm.get_builtin_scope(); - vm.context().new_scope(Some(builtins)) +#[wasm_bindgen(start)] +pub fn setup_console_error() { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); } -fn eval(vm: &mut VirtualMachine, source: &str, vars: PyObjectRef) -> PyResult { - // HACK: if the code doesn't end with newline it crashes. - let mut source = source.to_string(); - if !source.ends_with('\n') { - source.push('\n'); - } - - let code_obj = compile::compile( - &source, - &compile::Mode::Exec, - "".to_string(), - vm.ctx.code_type(), - ) - .map_err(|err| { - let syntax_error = vm.context().exceptions.syntax_error.clone(); - vm.new_exception(syntax_error, err.description().to_string()) - })?; - - vm.run_code_obj(code_obj, vars) -} +// Hack to comment out wasm-bindgen's generated typescript definitons +#[wasm_bindgen(typescript_custom_section)] +const TS_CMT_START: &'static str = "/*"; #[wasm_bindgen(js_name = pyEval)] /// Evaluate Python code @@ -154,7 +42,7 @@ fn eval(vm: &mut VirtualMachine, source: &str, vars: PyObjectRef) -> PyResult { /// receive the Python kwargs as the `this` argument. /// - `stdout?`: `(out: string) => void`: A function to replace the native print /// function, by default `console.log`. -pub fn eval_py(source: &str, options: Option) -> Result { +pub fn eval_py(source: String, options: Option) -> Result { let options = options.unwrap_or_else(Object::new); let js_vars = { let prop = Reflect::get(&options, &"vars".into())?; @@ -174,64 +62,16 @@ pub fn eval_py(source: &str, options: Option) -> Result PyResult> = match stdout { - Some(val) => { - if let Some(selector) = val.as_string() { - Box::new( - move |vm: &mut VirtualMachine, args: PyFuncArgs| -> PyResult { - wasm_builtins::builtin_print_html(vm, args, &selector) - }, - ) - } else if val.is_function() { - let func = js_sys::Function::from(val); - Box::new( - move |vm: &mut VirtualMachine, args: PyFuncArgs| -> PyResult { - func.call1( - &JsValue::UNDEFINED, - &wasm_builtins::format_print_args(vm, args)?.into(), - ) - .map_err(|err| js_to_py(vm, err))?; - Ok(vm.get_none()) - }, - ) - } else { - return Err(TypeError::new("stdout must be a function or a css selector").into()); - } - } - None => Box::new(wasm_builtins::builtin_print_console), - }; - - vm.ctx.set_attr( - &vm.builtins, - "print", - vm.ctx.new_rustfunc_from_box(print_fn), - ); + let vm = VMStore::init(PY_EVAL_VM_ID.into(), Some(true)); - let vars = base_scope(&mut vm); + vm.set_stdout(stdout.unwrap_or(JsValue::UNDEFINED))?; - let injections = vm.new_dict(); - - if let Some(js_vars) = js_vars.clone() { - for pair in Object::entries(&js_vars).values() { - let pair = pair?; - let key = Reflect::get(&pair, &"0".into()).unwrap(); - let val = Reflect::get(&pair, &"1".into()).unwrap(); - let py_val = js_to_py(&mut vm, val); - vm.ctx.set_item( - &injections, - &String::from(js_sys::JsString::from(key)), - py_val, - ); - } + if let Some(js_vars) = js_vars { + vm.add_to_scope("js_vars".into(), js_vars.into())?; } - vm.ctx.set_item(&vars, "js_vars", injections); - - eval(&mut vm, source, vars) - .map(|value| py_to_js(&mut vm, value)) - .map_err(|err| py_str_err(&mut vm, &err).into()) + vm.exec(source) } #[wasm_bindgen(typescript_custom_section)] diff --git a/wasm/lib/src/vm_class.rs b/wasm/lib/src/vm_class.rs new file mode 100644 index 0000000000..5af38ef39b --- /dev/null +++ b/wasm/lib/src/vm_class.rs @@ -0,0 +1,309 @@ +use crate::convert; +use crate::wasm_builtins::{self, setup_wasm_builtins}; +use js_sys::{SyntaxError, TypeError}; +use rustpython_vm::{ + compile, + pyobject::{PyFuncArgs, PyObjectRef, PyRef, PyResult}, + VirtualMachine, +}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::{Rc, Weak}; +use wasm_bindgen::prelude::*; + +pub(crate) struct StoredVirtualMachine { + pub vm: VirtualMachine, + pub scope: PyObjectRef, +} + +impl StoredVirtualMachine { + fn new(id: String, inject_builtins: bool) -> StoredVirtualMachine { + let mut vm = VirtualMachine::new(); + let builtin = vm.get_builtin_scope(); + let scope = vm.context().new_scope(Some(builtin)); + if inject_builtins { + setup_wasm_builtins(&mut vm, &scope); + } + vm.wasm_id = Some(id); + StoredVirtualMachine { vm, scope } + } +} + +// It's fine that it's thread local, since WASM doesn't even have threads yet. thread_local! probably +// gets compiled down to a normal-ish static varible, like Atomic* types: +// https://rustwasm.github.io/2018/10/24/multithreading-rust-and-wasm.html#atomic-instructions +thread_local! { + static STORED_VMS: PyRef>> = Rc::default(); + static ACTIVE_VMS: PyRef> = Rc::default(); +} + +#[wasm_bindgen(js_name = vmStore)] +pub struct VMStore; + +#[wasm_bindgen(js_class = vmStore)] +impl VMStore { + pub fn init(id: String, inject_builtins: Option) -> WASMVirtualMachine { + STORED_VMS.with(|cell| { + let mut vms = cell.borrow_mut(); + if !vms.contains_key(&id) { + let stored_vm = + StoredVirtualMachine::new(id.clone(), inject_builtins.unwrap_or(true)); + vms.insert(id.clone(), Rc::new(RefCell::new(stored_vm))); + } + }); + WASMVirtualMachine { id } + } + + pub(crate) fn _get(id: String) -> Option { + STORED_VMS.with(|cell| { + let vms = cell.borrow(); + if vms.contains_key(&id) { + Some(WASMVirtualMachine { id }) + } else { + None + } + }) + } + + pub fn get(id: String) -> JsValue { + match Self::_get(id) { + Some(wasm_vm) => wasm_vm.into(), + None => JsValue::UNDEFINED, + } + } + + pub fn destroy(id: String) { + STORED_VMS.with(|cell| { + use std::collections::hash_map::Entry; + match cell.borrow_mut().entry(id) { + Entry::Occupied(o) => { + let (_k, stored_vm) = o.remove_entry(); + // for f in stored_vm.drop_handlers.iter() { + // f(); + // } + // deallocate the VM + drop(stored_vm); + } + Entry::Vacant(_v) => {} + } + }); + } + + pub fn ids() -> Vec { + STORED_VMS.with(|cell| cell.borrow().keys().map(|k| k.into()).collect()) + } +} + +#[derive(Clone)] +pub struct AccessibleVM { + weak: Weak>, + id: String, +} + +impl AccessibleVM { + pub fn from_id(id: String) -> AccessibleVM { + let weak = STORED_VMS + .with(|cell| Rc::downgrade(cell.borrow().get(&id).expect("WASM VM to be valid"))); + AccessibleVM { weak, id } + } + + pub fn from_vm(vm: &VirtualMachine) -> AccessibleVM { + AccessibleVM::from_id( + vm.wasm_id + .clone() + .expect("VM passed to from_vm to have wasm_id be Some()"), + ) + } + + pub fn upgrade(&self) -> Option { + let vm_cell = self.weak.upgrade()?; + let top_level = match vm_cell.try_borrow_mut() { + Ok(mut vm) => { + ACTIVE_VMS.with(|cell| { + cell.borrow_mut().insert(self.id.clone(), &mut vm.vm); + }); + true + } + Err(_) => false, + }; + Some(ACTIVE_VMS.with(|cell| { + let vms = cell.borrow(); + let ptr = vms.get(&self.id).expect("id to be in ACTIVE_VMS"); + let vm = unsafe { &mut **ptr }; + AccessibleVMPtr { + id: self.id.clone(), + top_level, + inner: vm, + } + })) + } +} + +impl From for AccessibleVM { + fn from(vm: WASMVirtualMachine) -> AccessibleVM { + AccessibleVM::from_id(vm.id) + } +} +impl From<&WASMVirtualMachine> for AccessibleVM { + fn from(vm: &WASMVirtualMachine) -> AccessibleVM { + AccessibleVM::from_id(vm.id.clone()) + } +} + +pub struct AccessibleVMPtr<'a> { + id: String, + top_level: bool, + inner: &'a mut VirtualMachine, +} + +impl std::ops::Deref for AccessibleVMPtr<'_> { + type Target = VirtualMachine; + fn deref(&self) -> &VirtualMachine { + &self.inner + } +} +impl std::ops::DerefMut for AccessibleVMPtr<'_> { + fn deref_mut(&mut self) -> &mut VirtualMachine { + &mut self.inner + } +} + +impl Drop for AccessibleVMPtr<'_> { + fn drop(&mut self) { + if self.top_level { + // remove the (now invalid) pointer from the map + ACTIVE_VMS.with(|cell| cell.borrow_mut().remove(&self.id)); + } + } +} + +#[wasm_bindgen(js_name = VirtualMachine)] +#[derive(Clone)] +pub struct WASMVirtualMachine { + pub(crate) id: String, +} + +#[wasm_bindgen(js_class = VirtualMachine)] +impl WASMVirtualMachine { + pub(crate) fn with_unchecked(&self, f: F) -> R + where + F: FnOnce(&mut StoredVirtualMachine) -> R, + { + let stored_vm = STORED_VMS.with(|cell| { + let mut vms = cell.borrow_mut(); + vms.get_mut(&self.id).unwrap().clone() + }); + let mut stored_vm = stored_vm.borrow_mut(); + f(&mut stored_vm) + } + + pub(crate) fn with(&self, f: F) -> Result + where + F: FnOnce(&mut StoredVirtualMachine) -> R, + { + self.assert_valid()?; + Ok(self.with_unchecked(f)) + } + + pub fn valid(&self) -> bool { + STORED_VMS.with(|cell| cell.borrow().contains_key(&self.id)) + } + + pub fn assert_valid(&self) -> Result<(), JsValue> { + if self.valid() { + Ok(()) + } else { + Err(TypeError::new( + "Invalid VirtualMachine, this VM was destroyed while this reference was still held", + ) + .into()) + } + } + + pub fn destroy(&self) -> Result<(), JsValue> { + self.assert_valid()?; + VMStore::destroy(self.id.clone()); + Ok(()) + } + + #[wasm_bindgen(js_name = addToScope)] + pub fn add_to_scope(&self, name: String, value: JsValue) -> Result<(), JsValue> { + self.with( + move |StoredVirtualMachine { + ref mut vm, + ref mut scope, + }| { + let value = convert::js_to_py(vm, value); + vm.ctx.set_attr(scope, &name, value); + }, + ) + } + + #[wasm_bindgen(js_name = setStdout)] + pub fn set_stdout(&self, stdout: JsValue) -> Result<(), JsValue> { + self.with( + move |StoredVirtualMachine { + ref mut vm, + ref mut scope, + }| { + let print_fn: Box PyResult> = + if let Some(selector) = stdout.as_string() { + Box::new( + move |vm: &mut VirtualMachine, args: PyFuncArgs| -> PyResult { + wasm_builtins::builtin_print_html(vm, args, &selector) + }, + ) + } else if stdout.is_function() { + let func = js_sys::Function::from(stdout); + Box::new( + move |vm: &mut VirtualMachine, args: PyFuncArgs| -> PyResult { + func.call1( + &JsValue::UNDEFINED, + &wasm_builtins::format_print_args(vm, args)?.into(), + ) + .map_err(|err| convert::js_to_py(vm, err))?; + Ok(vm.get_none()) + }, + ) + } else if stdout.is_undefined() || stdout.is_null() { + Box::new(wasm_builtins::builtin_print_console) + } else { + return Err(TypeError::new( + "stdout must be null, a function or a css selector", + ) + .into()); + }; + vm.ctx + .set_attr(scope, "print", vm.ctx.new_rustfunc_from_box(print_fn)); + Ok(()) + }, + )? + } + + fn run(&self, mut source: String, mode: compile::Mode) -> Result { + self.assert_valid()?; + self.with_unchecked( + |StoredVirtualMachine { + ref mut vm, + ref mut scope, + }| { + source.push('\n'); + let code = + compile::compile(&source, &mode, "".to_string(), vm.ctx.code_type()) + .map_err(|err| { + SyntaxError::new(&format!("Error parsing Python code: {}", err)) + })?; + let result = vm.run_code_obj(code, scope.clone()); + convert::pyresult_to_jsresult(vm, result) + }, + ) + } + + pub fn exec(&self, source: String) -> Result { + self.run(source, compile::Mode::Exec) + } + + pub fn eval(&self, source: String) -> Result { + self.run(source, compile::Mode::Eval) + } +} diff --git a/wasm/lib/src/wasm_builtins.rs b/wasm/lib/src/wasm_builtins.rs index 025bf27083..211eedb415 100644 --- a/wasm/lib/src/wasm_builtins.rs +++ b/wasm/lib/src/wasm_builtins.rs @@ -2,24 +2,30 @@ //! //! This is required because some feature like I/O works differently in the browser comparing to //! desktop. -//! Implements functions listed here: https://docs.python.org/3/library/builtins.html -//! +//! Implements functions listed here: https://docs.python.org/3/library/builtins.html and some +//! others. + +extern crate futures; extern crate js_sys; extern crate wasm_bindgen; extern crate web_sys; -use crate::js_to_py; +use crate::convert; use js_sys::Array; use rustpython_vm::obj::{objstr, objtype}; use rustpython_vm::pyobject::{IdProtocol, PyFuncArgs, PyObjectRef, PyResult, TypeProtocol}; use rustpython_vm::VirtualMachine; -use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{console, window, HtmlTextAreaElement}; +use wasm_bindgen::{prelude::*, JsCast}; +use web_sys::{console, HtmlTextAreaElement}; + +fn window() -> web_sys::Window { + web_sys::window().expect("Window to be available") +} // The HTML id of the textarea element that act as our STDOUT pub fn print_to_html(text: &str, selector: &str) -> Result<(), JsValue> { - let document = window().unwrap().document().unwrap(); + let document = window().document().expect("Document to be available"); let element = document .query_selector(selector)? .ok_or_else(|| js_sys::TypeError::new("Couldn't get element"))?; @@ -85,7 +91,7 @@ pub fn format_print_args(vm: &mut VirtualMachine, args: PyFuncArgs) -> Result PyResult { let output = format_print_args(vm, args)?; - print_to_html(&output, selector).map_err(|err| js_to_py(vm, err))?; + print_to_html(&output, selector).map_err(|err| convert::js_to_py(vm, err))?; Ok(vm.get_none()) } @@ -97,3 +103,8 @@ pub fn builtin_print_console(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyRes console::log(&arr); Ok(vm.get_none()) } + +pub fn setup_wasm_builtins(vm: &mut VirtualMachine, scope: &PyObjectRef) { + let ctx = vm.context(); + ctx.set_attr(scope, "print", ctx.new_rustfunc(builtin_print_console)); +}