Skip to content

[WASM] Add a browser module #531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions vm/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ macro_rules! no_kwargs {
};
}

#[macro_export]
macro_rules! py_module {
( $ctx:expr, $module_name:expr, { $($name:expr => $value:expr),* $(,)* }) => {
{
Expand Down
23 changes: 23 additions & 0 deletions vm/src/pyobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,29 @@ impl PyFuncArgs {
}
None
}

pub fn get_optional_kwarg_with_type(
&self,
key: &str,
ty: PyObjectRef,
vm: &mut VirtualMachine,
) -> Result<Option<PyObjectRef>, PyObjectRef> {
match self.get_optional_kwarg(key) {
Some(kwarg) => {
if objtype::isinstance(&kwarg, &ty) {
Ok(Some(kwarg))
} else {
let expected_ty_name = vm.to_pystr(&ty)?;
let actual_ty_name = vm.to_pystr(&kwarg.typ())?;
Err(vm.new_type_error(format!(
"argument of type {} is required for named parameter `{}` (got: {})",
expected_ty_name, key, actual_ty_name
)))
}
}
None => Ok(None),
}
}
}

/// Rather than determining the type of a python object, this enum is more
Expand Down
12 changes: 12 additions & 0 deletions wasm/demo/snippets/fetch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from browser import fetch

def fetch_handler(res):
print(f"headers: {res['headers']}")

fetch(
"https://httpbin.org/get",
fetch_handler,
lambda err: print(f"error: {err}"),
response_format="json",
headers={"X-Header-Thing": "rustpython is neat!"},
)
138 changes: 138 additions & 0 deletions wasm/lib/src/browser_module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use crate::{convert, vm_class::AccessibleVM, wasm_builtins::window};
use futures::{future, Future};
use js_sys::Promise;
use rustpython_vm::obj::objstr;
use rustpython_vm::pyobject::{PyContext, PyFuncArgs, PyObjectRef, PyResult, TypeProtocol};
use rustpython_vm::VirtualMachine;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::{future_to_promise, JsFuture};

enum FetchResponseFormat {
Json,
Text,
ArrayBuffer,
}

impl FetchResponseFormat {
fn from_str(vm: &mut VirtualMachine, s: &str) -> Result<Self, PyObjectRef> {
match s {
"json" => Ok(FetchResponseFormat::Json),
"text" => Ok(FetchResponseFormat::Text),
"array_buffer" => Ok(FetchResponseFormat::ArrayBuffer),
_ => Err(vm.new_type_error("Unkown fetch response_format".into())),
}
}
fn get_response(&self, response: &web_sys::Response) -> Result<Promise, JsValue> {
match self {
FetchResponseFormat::Json => response.json(),
FetchResponseFormat::Text => response.text(),
FetchResponseFormat::ArrayBuffer => response.array_buffer(),
}
}
}

fn browser_fetch(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult {
arg_check!(
vm,
args,
required = [
(url, Some(vm.ctx.str_type())),
(handler, Some(vm.ctx.function_type()))
],
optional = [(reject_handler, Some(vm.ctx.function_type()))]
);
let response_format =
args.get_optional_kwarg_with_type("response_format", vm.ctx.str_type(), vm)?;
let method = args.get_optional_kwarg_with_type("method", vm.ctx.str_type(), vm)?;
let headers = args.get_optional_kwarg_with_type("headers", vm.ctx.dict_type(), vm)?;
let body = args.get_optional_kwarg("body");
let content_type = args.get_optional_kwarg_with_type("content_type", vm.ctx.str_type(), vm)?;

let response_format = match response_format {
Some(s) => FetchResponseFormat::from_str(vm, &objstr::get_value(&s))?,
None => FetchResponseFormat::Text,
};

let mut opts = web_sys::RequestInit::new();

match method {
Some(s) => opts.method(&objstr::get_value(&s)),
None => opts.method("GET"),
};

if let Some(body) = body {
opts.body(Some(&convert::py_to_js(vm, body)));
}

let request = web_sys::Request::new_with_str_and_init(&objstr::get_value(url), &opts)
.map_err(|err| convert::js_py_typeerror(vm, err))?;

if let Some(headers) = headers {
let h = request.headers();
for (key, value) in rustpython_vm::obj::objdict::get_key_value_pairs(&headers) {
let key = objstr::get_value(&vm.to_str(&key)?);
let value = objstr::get_value(&vm.to_str(&value)?);
h.set(&key, &value)
.map_err(|err| convert::js_py_typeerror(vm, err))?;
}
}

if let Some(content_type) = content_type {
request
.headers()
.set("Content-Type", &objstr::get_value(&content_type))
.map_err(|err| convert::js_py_typeerror(vm, err))?;
}

let window = window();
let request_prom = window.fetch_with_request(&request);

let handler = handler.clone();
let reject_handler = reject_handler.cloned();

let acc_vm = AccessibleVM::from_vm(vm);

let future = JsFuture::from(request_prom)
.and_then(move |val| {
let response = val
.dyn_into::<web_sys::Response>()
.expect("val to be of type Response");
response_format.get_response(&response)
})
.and_then(JsFuture::from)
.then(move |val| {
let vm = &mut acc_vm
.upgrade()
.expect("that the VM *not* be destroyed while promise is being resolved");
match val {
Ok(val) => {
let val = convert::js_to_py(vm, val);
let args = PyFuncArgs::new(vec![val], vec![]);
let _ = vm.invoke(handler, args);
}
Err(val) => {
if let Some(reject_handler) = reject_handler {
let val = convert::js_to_py(vm, val);
let args = PyFuncArgs::new(vec![val], vec![]);
let _ = vm.invoke(reject_handler, args);
}
}
}
future::ok(JsValue::UNDEFINED)
});
future_to_promise(future);

Ok(vm.get_none())
}

const BROWSER_NAME: &str = "browser";

pub fn mk_module(ctx: &PyContext) -> PyObjectRef {
py_module!(ctx, BROWSER_NAME, {
"fetch" => ctx.new_rustfunc(browser_fetch)
})
}

pub fn setup_browser_module(vm: &mut VirtualMachine) {
vm.stdlib_inits.insert(BROWSER_NAME.to_string(), mk_module);
}
3 changes: 3 additions & 0 deletions wasm/lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
pub mod browser_module;
pub mod convert;
pub mod vm_class;
pub mod wasm_builtins;

extern crate futures;
extern crate js_sys;
#[macro_use]
extern crate rustpython_vm;
extern crate wasm_bindgen;
extern crate wasm_bindgen_futures;
extern crate web_sys;

use js_sys::{Object, Reflect, TypeError};
Expand Down
13 changes: 7 additions & 6 deletions wasm/lib/src/vm_class.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::browser_module::setup_browser_module;
use crate::convert;
use crate::wasm_builtins::{self, setup_wasm_builtins};
use crate::wasm_builtins;
use js_sys::{SyntaxError, TypeError};
use rustpython_vm::{
compile,
Expand All @@ -17,12 +18,12 @@ pub(crate) struct StoredVirtualMachine {
}

impl StoredVirtualMachine {
fn new(id: String, inject_builtins: bool) -> StoredVirtualMachine {
fn new(id: String, inject_browser_module: 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);
if inject_browser_module {
setup_browser_module(&mut vm);
}
vm.wasm_id = Some(id);
StoredVirtualMachine { vm, scope }
Expand All @@ -42,12 +43,12 @@ pub struct VMStore;

#[wasm_bindgen(js_class = vmStore)]
impl VMStore {
pub fn init(id: String, inject_builtins: Option<bool>) -> WASMVirtualMachine {
pub fn init(id: String, inject_browser_module: Option<bool>) -> 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));
StoredVirtualMachine::new(id.clone(), inject_browser_module.unwrap_or(true));
vms.insert(id.clone(), Rc::new(RefCell::new(stored_vm)));
}
});
Expand Down
19 changes: 4 additions & 15 deletions wasm/lib/src/wasm_builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@
//!
//! 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 and some
//! others.

extern crate futures;
extern crate js_sys;
extern crate wasm_bindgen;
extern crate web_sys;
//! Implements functions listed here: https://docs.python.org/3/library/builtins.html.

use crate::convert;
use js_sys::Array;
use js_sys::{self, Array};
use rustpython_vm::obj::{objstr, objtype};
use rustpython_vm::pyobject::{IdProtocol, PyFuncArgs, PyObjectRef, PyResult, TypeProtocol};
use rustpython_vm::VirtualMachine;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{console, HtmlTextAreaElement};
use web_sys::{self, console, HtmlTextAreaElement};

fn window() -> web_sys::Window {
pub(crate) fn window() -> web_sys::Window {
web_sys::window().expect("Window to be available")
}

Expand Down Expand Up @@ -103,8 +97,3 @@ 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));
}