Skip to content
Prev Previous commit
Next Next commit
Add a WASM browser module with fetch() available
  • Loading branch information
coolreader18 committed Feb 23, 2019
commit fd184a1e68f2d2c91498c111c34fd32eb8595a60
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
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(|prom| JsFuture::from(prom))
.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);
}
1 change: 1 addition & 0 deletions wasm/lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod browser_module;
pub mod convert;
pub mod vm_class;
pub mod wasm_builtins;
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
143 changes: 5 additions & 138 deletions wasm/lib/src/wasm_builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +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.
//! Implements functions listed here: https://docs.python.org/3/library/builtins.html.

extern crate futures;
extern crate js_sys;
extern crate wasm_bindgen;
extern crate wasm_bindgen_futures;
extern crate web_sys;

use crate::{convert, vm_class::AccessibleVM};
use futures::{future, Future};
use js_sys::{Array, Promise};
use crate::convert;
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 wasm_bindgen_futures::{future_to_promise, JsFuture};
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 @@ -106,127 +97,3 @@ pub fn builtin_print_console(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyRes
console::log(&arr);
Ok(vm.get_none())
}

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 builtin_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(|prom| JsFuture::from(prom))
.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())
}

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));
ctx.set_attr(scope, "fetch", ctx.new_rustfunc(builtin_fetch));
}