diff --git a/crates/cli-support/src/js.rs b/crates/cli-support/src/js.rs index 10f3c62a..72f8b875 100644 --- a/crates/cli-support/src/js.rs +++ b/crates/cli-support/src/js.rs @@ -220,6 +220,37 @@ impl<'a> Context<'a> { return ptr; }") }); + + for i in 0..8 { + let name = format!("__wbindgen_cb_arity{}", i); + bind(&name, &|me| { + me.expose_add_heap_object(); + me.function_table_needed = true; + let args = (0..i) + .map(|x| format!("arg{}", x)) + .collect::>() + .join(", "); + format!("function(a, b, c) {{ + const cb = function({0}) {{ + return this.f(this.a, this.b {1} {0}); + }}; + cb.a = b; + cb.b = c; + cb.f = wasm.__wbg_function_table.get(a); + let real = cb.bind(cb); + real.original = cb; + return addHeapObject(real); + }}", args, if i == 0 {""} else {","}) + }); + } + bind("__wbindgen_cb_drop", &|me| { + me.expose_drop_ref(); + String::from("function(i) { + let obj = getObject(i).original; + obj.a = obj.b = 0; + dropRef(i); + }") + }); } self.rewrite_imports(module_name); @@ -1438,6 +1469,10 @@ impl<'a, 'b> SubContext<'a, 'b> { self.cx.expose_get_object(); format!("getObject(arg{})", i) } + shared::TYPE_FUNC => { + self.cx.expose_get_object(); + format!("getObject(arg{})", i) + } shared::TYPE_STACK_FUNC0 | shared::TYPE_STACK_FUNC1 | shared::TYPE_STACK_FUNC2 | diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 3c0a1b76..c937a967 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -154,8 +154,9 @@ pub const TYPE_STACK_FUNC4: u32 = 28; pub const TYPE_STACK_FUNC5: u32 = 29; pub const TYPE_STACK_FUNC6: u32 = 30; pub const TYPE_STACK_FUNC7: u32 = 31; +pub const TYPE_FUNC: u32 = 32; -pub const TYPE_CUSTOM_START: u32 = 32; +pub const TYPE_CUSTOM_START: u32 = 40; pub const TYPE_CUSTOM_REF_FLAG: u32 = 1; pub fn name_to_descriptor(name: &str) -> u32 { diff --git a/src/closure.rs b/src/closure.rs new file mode 100644 index 00000000..83825cb3 --- /dev/null +++ b/src/closure.rs @@ -0,0 +1,241 @@ +use std::mem::ManuallyDrop; +use std::marker::{self, Unsize}; + +use {throw, JsValue}; +use convert::*; +use __rt::WasmRefCell; + +pub unsafe trait WasmShim<'a> { + #[doc(hidden)] + const DESCRIPTOR: Descriptor; + #[doc(hidden)] + type Wrapper; + #[doc(hidden)] + fn shim() -> u32; + #[doc(hidden)] + fn factory() -> unsafe extern fn(u32, u32, u32) -> u32; + #[doc(hidden)] + fn wrap(u: U) -> Self::Wrapper where U: Unsize + 'a; + #[doc(hidden)] + fn data(t: &Self::Wrapper) -> [u32; 2]; +} + +union RawPtr { + ptr: *const T, + data: [u32; 2] +} + +macro_rules! doit { + ($( + ($($var:ident)*) => $arity:ident + )*) => ($( + // Fn with no return + unsafe impl<'a, $($var: WasmAbi),*> WasmShim<'a> for Fn($($var),*) + 'a { + const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC; + type Wrapper = Box; + + fn shim() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn shim<$($var),*>( + a: u32, + b: u32, + $($var:$var),* + ) { + if a == 0 { + throw("closure has been destroyed already"); + } + (*RawPtr:: { data: [a, b] }.ptr)($($var),*) + } + shim::<$($var),*> as u32 + } + + fn factory() -> unsafe extern fn(u32, u32, u32) -> u32 { + super::$arity + } + + fn wrap(u: U) -> Self::Wrapper where U: Unsize + 'a { + Box::new(u) as Box + } + + fn data(t: &Self::Wrapper) -> [u32; 2] { + unsafe { + RawPtr:: { ptr: &**t }.data + } + } + } + + // Fn with a return + unsafe impl<'a, $($var: WasmAbi,)* R: WasmAbi> WasmShim<'a> for Fn($($var),*) -> R + 'a { + const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC; + type Wrapper = Box R + 'a>; + + fn shim() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn shim<$($var,)* R>( + a: u32, + b: u32, + $($var:$var),* + ) -> R { + if a == 0 { + throw("closure has been destroyed already"); + } + (*RawPtr:: R> { data: [a, b] }.ptr)($($var),*) + } + shim::<$($var,)* R> as u32 + } + + fn factory() -> unsafe extern fn(u32, u32, u32) -> u32 { + super::$arity + } + + fn wrap(u: U) -> Self::Wrapper where U: Unsize + 'a { + Box::new(u) as Box + } + + fn data(t: &Self::Wrapper) -> [u32; 2] { + unsafe { + RawPtr:: { ptr: &**t }.data + } + } + } + + // FnMut with no return + unsafe impl<'a, $($var: WasmAbi),*> WasmShim<'a> for FnMut($($var),*) + 'a { + const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC; + type Wrapper = Box>; + + fn shim() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn shim<$($var),*>( + a: u32, + b: u32, + $($var:$var),* + ) { + if a == 0 { + throw("closure has been destroyed already"); + } + let ptr: *const WasmRefCell = RawPtr { + data: [a, b], + }.ptr; + let mut ptr = (*ptr).borrow_mut(); + (&mut *ptr)($($var),*) + } + shim::<$($var),*> as u32 + } + + fn factory() -> unsafe extern fn(u32, u32, u32) -> u32 { + super::$arity + } + + fn wrap(u: U) -> Self::Wrapper where U: Unsize + 'a { + Box::new(WasmRefCell::new(u)) as Box<_> + } + + fn data(t: &Self::Wrapper) -> [u32; 2] { + unsafe { + RawPtr::> { ptr: &**t }.data + } + } + } + + // FnMut with a return + unsafe impl<'a, $($var: WasmAbi,)* R: WasmAbi> WasmShim<'a> for FnMut($($var),*) -> R + 'a { + const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC; + type Wrapper = Box R + 'a>>; + + fn shim() -> u32 { + #[allow(non_snake_case)] + unsafe extern fn shim<$($var,)* R>( + a: u32, + b: u32, + $($var:$var),* + ) -> R { + if a == 0 { + throw("closure has been destroyed already"); + } + let ptr: *const WasmRefCell R> = RawPtr { + data: [a, b], + }.ptr; + let mut ptr = (*ptr).borrow_mut(); + (&mut *ptr)($($var),*) + } + shim::<$($var,)* R> as u32 + } + + fn factory() -> unsafe extern fn(u32, u32, u32) -> u32 { + super::$arity + } + + fn wrap(u: U) -> Self::Wrapper where U: Unsize + 'a { + Box::new(WasmRefCell::new(u)) as Box<_> + } + + fn data(t: &Self::Wrapper) -> [u32; 2] { + unsafe { + RawPtr::> { ptr: &**t }.data + } + } + } + )*) +} + +doit! { + () => __wbindgen_cb_arity0 + (A) => __wbindgen_cb_arity1 + (A B) => __wbindgen_cb_arity2 + (A B C) => __wbindgen_cb_arity3 + (A B C D) => __wbindgen_cb_arity4 + (A B C D E) => __wbindgen_cb_arity5 + (A B C D E F) => __wbindgen_cb_arity6 + (A B C D E F G) => __wbindgen_cb_arity7 +} + +pub struct Closure<'a, T: WasmShim<'a> + ?Sized + 'a> { + _inner: T::Wrapper, + js: ManuallyDrop, + _marker: marker::PhantomData<&'a ()>, +} + +impl<'a, T> Closure<'a, T> + where T: WasmShim<'a> + ?Sized, +{ + pub fn new(t: U) -> Closure<'a, T> + where U: Unsize + 'a + { + Closure::wrap(T::wrap(t)) + } + + pub fn wrap(t: T::Wrapper) -> Closure<'a, T> { + unsafe { + let data = T::data(&t); + let js = T::factory()(T::shim(), data[0], data[1]); + Closure { + _inner: t, + js: ManuallyDrop::new(JsValue::from_abi(js, &mut GlobalStack::new())), + _marker: marker::PhantomData, + } + } + } +} + +impl<'a, T> ToRefWasmBoundary for Closure<'a, T> + where T: WasmShim<'a> + ?Sized, +{ + type Abi = u32; + const DESCRIPTOR: Descriptor = T::DESCRIPTOR; + + fn to_abi_ref(&self, extra: &mut Stack) -> u32 { + self.js.to_abi_ref(extra) + } +} + +impl<'a, T> Drop for Closure<'a, T> + where T: WasmShim<'a> + ?Sized, +{ + fn drop(&mut self) { + unsafe { + let idx = self.js.to_abi_ref(&mut GlobalStack::new()); + super::__wbindgen_cb_drop(idx); + } + } +} diff --git a/src/convert.rs b/src/convert.rs index 3e486234..360ac5a5 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -30,6 +30,8 @@ pub const DESCRIPTOR_STACK_FUNC5: Descriptor = Descriptor { __x: *b" 29", }; pub const DESCRIPTOR_STACK_FUNC6: Descriptor = Descriptor { __x: *b" 30", }; pub const DESCRIPTOR_STACK_FUNC7: Descriptor = Descriptor { __x: *b" 31", }; +pub const DESCRIPTOR_FUNC: Descriptor = Descriptor { __x: *b" 32", }; + pub trait WasmBoundary { type Abi: WasmAbi; const DESCRIPTOR: Descriptor; @@ -366,7 +368,7 @@ macro_rules! stack_closures { $($var: $var),* ) -> R { if a == 0 { - throw("stack closure has been destroyed already"); + throw("closure has been destroyed already"); } let f: &Fn($($var),*) -> R = mem::transmute((a, b)); f($($var),*) @@ -394,7 +396,7 @@ macro_rules! stack_closures { $($var: $var),* ) { if a == 0 { - throw("stack closure has been destroyed already"); + throw("closure has been destroyed already"); } let f: &Fn($($var),*) = mem::transmute((a, b)); f($($var),*) diff --git a/src/lib.rs b/src/lib.rs index 3b4c9bee..00ad8947 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! this crate and this crate also provides JS bindings through the `JsValue` //! interface. -#![feature(use_extern_macros, wasm_import_module, try_reserve)] +#![feature(use_extern_macros, wasm_import_module, try_reserve, unsize)] extern crate wasm_bindgen_macro; @@ -23,9 +23,11 @@ use convert::WasmBoundary; pub mod prelude { pub use wasm_bindgen_macro::wasm_bindgen; pub use JsValue; + pub use closure::Closure; } pub mod convert; +pub mod closure; /// Representation of an object owned by JS. /// @@ -230,6 +232,16 @@ extern { fn __wbindgen_is_symbol(idx: u32) -> u32; fn __wbindgen_string_get(idx: u32, len: *mut usize) -> *mut u8; fn __wbindgen_throw(a: *const u8, b: usize) -> !; + + fn __wbindgen_cb_arity0(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_arity1(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_arity2(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_arity3(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_arity4(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_arity5(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_arity6(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_arity7(a: u32, b: u32, c: u32) -> u32; + fn __wbindgen_cb_drop(idx: u32); } impl Clone for JsValue { @@ -341,13 +353,13 @@ pub mod __rt { /// guard accidental reentrancy, so this vendored version is intended solely /// to not panic in libstd. Instead when it "panics" it calls our `throw` /// function in this crate which raises an error in JS. - pub struct WasmRefCell { + pub struct WasmRefCell { borrow: Cell, value: UnsafeCell, } - impl WasmRefCell { - pub fn new(value: T) -> WasmRefCell { + impl WasmRefCell { + pub fn new(value: T) -> WasmRefCell where T: Sized { WasmRefCell { value: UnsafeCell::new(value), borrow: Cell::new(0), @@ -386,17 +398,17 @@ pub mod __rt { } } - pub fn into_inner(self) -> T { + pub fn into_inner(self) -> T where T: Sized { self.value.into_inner() } } - pub struct Ref<'b, T: 'b> { + pub struct Ref<'b, T: ?Sized + 'b> { value: &'b T, borrow: &'b Cell, } - impl<'b, T> Deref for Ref<'b, T> { + impl<'b, T: ?Sized> Deref for Ref<'b, T> { type Target = T; #[inline] @@ -405,18 +417,18 @@ pub mod __rt { } } - impl<'b, T> Drop for Ref<'b, T> { + impl<'b, T: ?Sized> Drop for Ref<'b, T> { fn drop(&mut self) { self.borrow.set(self.borrow.get() - 1); } } - pub struct RefMut<'b, T: 'b> { + pub struct RefMut<'b, T: ?Sized + 'b> { value: &'b mut T, borrow: &'b Cell, } - impl<'b, T> Deref for RefMut<'b, T> { + impl<'b, T: ?Sized> Deref for RefMut<'b, T> { type Target = T; #[inline] @@ -425,14 +437,14 @@ pub mod __rt { } } - impl<'b, T> DerefMut for RefMut<'b, T> { + impl<'b, T: ?Sized> DerefMut for RefMut<'b, T> { #[inline] fn deref_mut(&mut self) -> &mut T { self.value } } - impl<'b, T> Drop for RefMut<'b, T> { + impl<'b, T: ?Sized> Drop for RefMut<'b, T> { fn drop(&mut self) { self.borrow.set(0); } diff --git a/tests/closures.rs b/tests/closures.rs index 67bd867a..0fdddfbd 100644 --- a/tests/closures.rs +++ b/tests/closures.rs @@ -88,3 +88,238 @@ fn cannot_reuse() { .test(); } +#[test] +fn long_lived() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro, wasm_custom_section, wasm_import_module)] + + extern crate wasm_bindgen; + + use std::cell::Cell; + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "./test")] + extern { + fn call1(a: &Closure); + fn call2(a: &Closure u32>) -> u32; + } + + #[wasm_bindgen] + pub fn run() { + let hit = Cell::new(false); + let a = Closure::new(|| hit.set(true)); + assert!(!hit.get()); + call1(&a); + assert!(hit.get()); + + let mut hit = false; + { + let a = Closure::new(|x| { + hit = true; + x + 3 + }); + assert_eq!(call2(&a), 5); + } + assert!(hit); + } + "#) + .file("test.ts", r#" + import { run } from "./out"; + + export function call1(a: any) { + a(); + } + + export function call2(a: any) { + return a(2); + } + + export function test() { + run(); + } + "#) + .test(); +} + +#[test] +fn many_arity() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro, wasm_custom_section, wasm_import_module)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "./test")] + extern { + fn call1(a: &Closure); + fn call2(a: &Closure); + fn call3(a: &Closure); + fn call4(a: &Closure); + fn call5(a: &Closure); + fn call6(a: &Closure); + fn call7(a: &Closure); + fn call8(a: &Closure); + + #[wasm_bindgen(js_name = call1)] + fn stack1(a: &Fn()); + #[wasm_bindgen(js_name = call2)] + fn stack2(a: &Fn(u32)); + #[wasm_bindgen(js_name = call3)] + fn stack3(a: &Fn(u32, u32)); + #[wasm_bindgen(js_name = call4)] + fn stack4(a: &Fn(u32, u32, u32)); + #[wasm_bindgen(js_name = call5)] + fn stack5(a: &Fn(u32, u32, u32, u32)); + #[wasm_bindgen(js_name = call6)] + fn stack6(a: &Fn(u32, u32, u32, u32, u32)); + #[wasm_bindgen(js_name = call7)] + fn stack7(a: &Fn(u32, u32, u32, u32, u32, u32)); + #[wasm_bindgen(js_name = call8)] + fn stack8(a: &Fn(u32, u32, u32, u32, u32, u32, u32)); + } + + #[wasm_bindgen] + pub fn run() { + call1(&Closure::new(|| {})); + call2(&Closure::new(|a| assert_eq!(a, 1))); + call3(&Closure::new(|a, b| assert_eq!((a, b), (1, 2)))); + call4(&Closure::new(|a, b, c| assert_eq!((a, b, c), (1, 2, 3)))); + call5(&Closure::new(|a, b, c, d| { + assert_eq!((a, b, c, d), (1, 2, 3, 4)) + })); + call6(&Closure::new(|a, b, c, d, e| { + assert_eq!((a, b, c, d, e), (1, 2, 3, 4, 5)) + })); + call7(&Closure::new(|a, b, c, d, e, f| { + assert_eq!((a, b, c, d, e, f), (1, 2, 3, 4, 5, 6)) + })); + call8(&Closure::new(|a, b, c, d, e, f, g| { + assert_eq!((a, b, c, d, e, f, g), (1, 2, 3, 4, 5, 6, 7)) + })); + + stack1(&(|| {})); + stack2(&(|a| assert_eq!(a, 1))); + stack3(&(|a, b| assert_eq!((a, b), (1, 2)))); + stack4(&(|a, b, c| assert_eq!((a, b, c), (1, 2, 3)))); + stack5(&(|a, b, c, d| { + assert_eq!((a, b, c, d), (1, 2, 3, 4)) + })); + stack6(&(|a, b, c, d, e| { + assert_eq!((a, b, c, d, e), (1, 2, 3, 4, 5)) + })); + stack7(&(|a, b, c, d, e, f| { + assert_eq!((a, b, c, d, e, f), (1, 2, 3, 4, 5, 6)) + })); + stack8(&(|a, b, c, d, e, f, g| { + assert_eq!((a, b, c, d, e, f, g), (1, 2, 3, 4, 5, 6, 7)) + })); + } + "#) + .file("test.ts", r#" + import { run } from "./out"; + + export function call1(a: any) { a() } + export function call2(a: any) { a(1) } + export function call3(a: any) { a(1, 2) } + export function call4(a: any) { a(1, 2, 3) } + export function call5(a: any) { a(1, 2, 3, 4) } + export function call6(a: any) { a(1, 2, 3, 4, 5) } + export function call7(a: any) { a(1, 2, 3, 4, 5, 6) } + export function call8(a: any) { a(1, 2, 3, 4, 5, 6, 7) } + + export function test() { + run(); + } + "#) + .test(); +} + +#[test] +fn long_lived_dropping() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro, wasm_custom_section, wasm_import_module)] + + extern crate wasm_bindgen; + + use std::cell::Cell; + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "./test")] + extern { + fn cache(a: &Closure); + #[wasm_bindgen(catch)] + fn call() -> Result<(), JsValue>; + } + + #[wasm_bindgen] + pub fn run() { + let hit = Cell::new(false); + let a = Closure::new(|| hit.set(true)); + cache(&a); + assert!(!hit.get()); + assert!(call().is_ok()); + assert!(hit.get()); + drop(a); + assert!(call().is_err()); + } + "#) + .file("test.ts", r#" + import { run } from "./out"; + + let CACHE: any = null; + + export function cache(a: any) { CACHE = a; } + export function call() { CACHE() } + + export function test() { + run(); + } + "#) + .test(); +} + +#[test] +fn long_fnmut_recursive() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro, wasm_custom_section, wasm_import_module)] + + extern crate wasm_bindgen; + + use std::cell::Cell; + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "./test")] + extern { + fn cache(a: &Closure); + #[wasm_bindgen(catch)] + fn call() -> Result<(), JsValue>; + } + + #[wasm_bindgen] + pub fn run() { + let a = Closure::new(|| { + assert!(call().is_err()); + }); + cache(&a); + assert!(call().is_ok()); + } + "#) + .file("test.ts", r#" + import { run } from "./out"; + + let CACHE: any = null; + + export function cache(a: any) { CACHE = a; } + export function call() { CACHE() } + + export function test() { + run(); + } + "#) + .test(); +}