diff --git a/components/script/dom/bindings/refcounted.rs b/components/script/dom/bindings/refcounted.rs index ea4c47b1148..41cc60b0210 100644 --- a/components/script/dom/bindings/refcounted.rs +++ b/components/script/dom/bindings/refcounted.rs @@ -26,6 +26,7 @@ use core::nonzero::NonZero; use dom::bindings::js::Root; use dom::bindings::reflector::{Reflectable, Reflector}; use dom::bindings::trace::trace_reflector; +use dom::promise::Promise; use js::jsapi::JSTracer; use libc; use std::cell::RefCell; @@ -34,6 +35,7 @@ use std::collections::hash_map::HashMap; use std::hash::Hash; use std::marker::PhantomData; use std::os; +use std::rc::Rc; use std::sync::{Arc, Weak}; @@ -58,6 +60,63 @@ impl TrustedReference { } } +/// A safe wrapper around a DOM Promise object that can be shared among threads for use +/// in asynchronous operations. The underlying DOM object is guaranteed to live at least +/// as long as the last outstanding `TrustedPromise` instance. These values cannot be cloned, +/// only created from existing Rc values. +pub struct TrustedPromise { + dom_object: *const Promise, + owner_thread: *const libc::c_void, +} + +unsafe impl Send for TrustedPromise {} + +impl TrustedPromise { + /// Create a new `TrustedPromise` instance from an existing DOM object. The object will + /// be prevented from being GCed for the duration of the resulting `TrustedPromise` object's + /// lifetime. + #[allow(unrooted_must_root)] + pub fn new(promise: Rc) -> TrustedPromise { + LIVE_REFERENCES.with(|ref r| { + let r = r.borrow(); + let live_references = r.as_ref().unwrap(); + let ptr = &*promise as *const Promise; + live_references.addref_promise(promise); + TrustedPromise { + dom_object: ptr, + owner_thread: (&*live_references) as *const _ as *const libc::c_void, + } + }) + } + + /// Obtain a usable DOM Promise from a pinned `TrustedPromise` value. Fails if used on + /// a different thread than the original value from which this `TrustedPromise` was + /// obtained. + #[allow(unrooted_must_root)] + pub fn root(self) -> Rc { + LIVE_REFERENCES.with(|ref r| { + let r = r.borrow(); + let live_references = r.as_ref().unwrap(); + assert!(self.owner_thread == (&*live_references) as *const _ as *const libc::c_void); + // Borrow-check error requires the redundant `let promise = ...; promise` here. + let promise = match live_references.promise_table.borrow_mut().entry(self.dom_object) { + Occupied(mut entry) => { + let promise = { + let promises = entry.get_mut(); + promises.pop().expect("rooted promise list unexpectedly empty") + }; + if entry.get().is_empty() { + entry.remove(); + } + promise + } + Vacant(_) => unreachable!(), + }; + promise + }) + } +} + /// A safe wrapper around a raw pointer to a DOM object that can be /// shared among threads for use in asynchronous operations. The underlying /// DOM object is guaranteed to live at least as long as the last outstanding @@ -117,9 +176,11 @@ impl Clone for Trusted { /// The set of live, pinned DOM objects that are currently prevented /// from being garbage collected due to outstanding references. +#[allow(unrooted_must_root)] pub struct LiveDOMReferences { // keyed on pointer to Rust DOM object - table: RefCell>>, + reflectable_table: RefCell>>, + promise_table: RefCell>>>, } impl LiveDOMReferences { @@ -127,13 +188,20 @@ impl LiveDOMReferences { pub fn initialize() { LIVE_REFERENCES.with(|ref r| { *r.borrow_mut() = Some(LiveDOMReferences { - table: RefCell::new(HashMap::new()), + reflectable_table: RefCell::new(HashMap::new()), + promise_table: RefCell::new(HashMap::new()), }) }); } + #[allow(unrooted_must_root)] + fn addref_promise(&self, promise: Rc) { + let mut table = self.promise_table.borrow_mut(); + table.entry(&*promise).or_insert(vec![]).push(promise) + } + fn addref(&self, ptr: *const T) -> Arc { - let mut table = self.table.borrow_mut(); + let mut table = self.reflectable_table.borrow_mut(); let capacity = table.capacity(); let len = table.len(); if (0 < capacity) && (capacity <= len) { @@ -173,17 +241,27 @@ fn remove_nulls (table: &mut HashMap>) { } /// A JSTraceDataOp for tracing reflectors held in LIVE_REFERENCES +#[allow(unrooted_must_root)] pub unsafe extern "C" fn trace_refcounted_objects(tracer: *mut JSTracer, _data: *mut os::raw::c_void) { info!("tracing live refcounted references"); LIVE_REFERENCES.with(|ref r| { let r = r.borrow(); let live_references = r.as_ref().unwrap(); - let mut table = live_references.table.borrow_mut(); - remove_nulls(&mut table); - for obj in table.keys() { - let reflectable = &*(*obj as *const Reflector); - trace_reflector(tracer, "refcounted", reflectable); + { + let mut table = live_references.reflectable_table.borrow_mut(); + remove_nulls(&mut table); + for obj in table.keys() { + let reflectable = &*(*obj as *const Reflector); + trace_reflector(tracer, "refcounted", reflectable); + } + } + + { + let table = live_references.promise_table.borrow_mut(); + for promise in table.keys() { + trace_reflector(tracer, "refcounted", (**promise).reflector()); + } } }); } diff --git a/components/script/dom/bindings/trace.rs b/components/script/dom/bindings/trace.rs index f9d81ee210b..53d141d114c 100644 --- a/components/script/dom/bindings/trace.rs +++ b/components/script/dom/bindings/trace.rs @@ -36,7 +36,7 @@ use devtools_traits::CSSError; use devtools_traits::WorkerId; use dom::abstractworker::SharedRt; use dom::bindings::js::{JS, Root}; -use dom::bindings::refcounted::Trusted; +use dom::bindings::refcounted::{Trusted, TrustedPromise}; use dom::bindings::reflector::{Reflectable, Reflector}; use dom::bindings::str::{DOMString, USVString}; use dom::bindings::utils::WindowProxyHandler; @@ -303,6 +303,7 @@ no_jsmanaged_fields!(Metadata); no_jsmanaged_fields!(NetworkError); no_jsmanaged_fields!(Atom, Namespace, QualName); no_jsmanaged_fields!(Trusted); +no_jsmanaged_fields!(TrustedPromise); no_jsmanaged_fields!(PropertyDeclarationBlock); no_jsmanaged_fields!(HashSet); // These three are interdependent, if you plan to put jsmanaged data diff --git a/components/script/dom/promise.rs b/components/script/dom/promise.rs index f167d95bfd2..295a73f43a1 100644 --- a/components/script/dom/promise.rs +++ b/components/script/dom/promise.rs @@ -79,6 +79,14 @@ impl Promise { } } + #[allow(unsafe_code, unrooted_must_root)] + pub fn duplicate(&self) -> Rc { + let cx = self.global().r().get_cx(); + unsafe { + Promise::new_with_js_promise(self.reflector().get_jsobject(), cx) + } + } + #[allow(unsafe_code, unrooted_must_root)] unsafe fn new_with_js_promise(obj: HandleObject, cx: *mut JSContext) -> Rc { assert!(IsPromiseObject(obj)); diff --git a/components/script/dom/testbinding.rs b/components/script/dom/testbinding.rs index a9df0a3a9d2..ad80cc58cc9 100644 --- a/components/script/dom/testbinding.rs +++ b/components/script/dom/testbinding.rs @@ -25,6 +25,7 @@ use dom::bindings::global::{GlobalRef, global_root_from_context}; use dom::bindings::js::Root; use dom::bindings::mozmap::MozMap; use dom::bindings::num::Finite; +use dom::bindings::refcounted::TrustedPromise; use dom::bindings::reflector::{Reflectable, Reflector, reflect_dom_object}; use dom::bindings::str::{ByteString, DOMString, USVString}; use dom::bindings::weakref::MutableWeakRef; @@ -32,12 +33,14 @@ use dom::blob::{Blob, BlobImpl}; use dom::promise::Promise; use dom::promisenativehandler::{PromiseNativeHandler, Callback}; use dom::url::URL; -use js::jsapi::{HandleObject, HandleValue, JSContext, JSObject}; +use js::jsapi::{HandleObject, HandleValue, JSContext, JSObject, JSAutoCompartment}; use js::jsapi::{JS_NewPlainObject, JS_NewUint8ClampedArray}; use js::jsval::{JSVal, NullValue}; +use script_traits::MsDuration; use std::borrow::ToOwned; use std::ptr; use std::rc::Rc; +use timers::OneshotTimerCallback; use util::prefs::PREFS; #[dom_struct] @@ -672,6 +675,17 @@ impl TestBindingMethods for TestBinding { p.maybe_reject_error(self.global().r().get_cx(), Error::Type(s.0)); } + #[allow(unrooted_must_root)] + fn ResolvePromiseDelayed(&self, p: &Promise, value: DOMString, delay: u64) { + let promise = p.duplicate(); + let cb = TestBindingCallback { + promise: TrustedPromise::new(promise), + value: value, + }; + let _ = self.global().r().schedule_callback(OneshotTimerCallback::TestBindingCallback(cb), + MsDuration::new(delay)); + } + #[allow(unrooted_must_root)] fn PromiseNativeHandler(&self, resolve: Option>, @@ -760,3 +774,20 @@ impl TestBinding { pub unsafe fn condition_satisfied(_: *mut JSContext, _: HandleObject) -> bool { true } pub unsafe fn condition_unsatisfied(_: *mut JSContext, _: HandleObject) -> bool { false } } + +#[derive(JSTraceable, HeapSizeOf)] +pub struct TestBindingCallback { + #[ignore_heap_size_of = "unclear ownership semantics"] + promise: TrustedPromise, + value: DOMString, +} + +impl TestBindingCallback { + #[allow(unrooted_must_root)] + pub fn invoke(self) { + let p = self.promise.root(); + let cx = p.global().r().get_cx(); + let _ac = JSAutoCompartment::new(cx, p.reflector().get_jsobject().get()); + p.maybe_resolve_native(cx, &self.value); + } +} diff --git a/components/script/dom/webidls/TestBinding.webidl b/components/script/dom/webidls/TestBinding.webidl index 54e54f27c77..c3274057a40 100644 --- a/components/script/dom/webidls/TestBinding.webidl +++ b/components/script/dom/webidls/TestBinding.webidl @@ -519,6 +519,7 @@ interface TestBinding { void promiseResolveNative(Promise p, any value); void promiseRejectNative(Promise p, any value); void promiseRejectWithTypeError(Promise p, USVString message); + void resolvePromiseDelayed(Promise p, DOMString value, unsigned long long ms); void panic(); }; diff --git a/components/script/timers.rs b/components/script/timers.rs index 8c2d2d9cac4..f75ebf907d5 100644 --- a/components/script/timers.rs +++ b/components/script/timers.rs @@ -8,6 +8,7 @@ use dom::bindings::codegen::Bindings::FunctionBinding::Function; use dom::bindings::global::GlobalRef; use dom::bindings::reflector::Reflectable; use dom::bindings::str::DOMString; +use dom::testbinding::TestBindingCallback; use dom::window::ScriptHelpers; use dom::xmlhttprequest::XHRTimeoutCallback; use euclid::length::Length; @@ -68,6 +69,7 @@ struct OneshotTimer { pub enum OneshotTimerCallback { XhrTimeout(XHRTimeoutCallback), JsTimer(JsTimerTask), + TestBindingCallback(TestBindingCallback), } impl OneshotTimerCallback { @@ -75,6 +77,7 @@ impl OneshotTimerCallback { match self { OneshotTimerCallback::XhrTimeout(callback) => callback.invoke(), OneshotTimerCallback::JsTimer(task) => task.invoke(this, js_timers), + OneshotTimerCallback::TestBindingCallback(callback) => callback.invoke(), } } } diff --git a/tests/wpt/mozilla/tests/mozilla/promise.html b/tests/wpt/mozilla/tests/mozilla/promise.html index 79a0a5578a9..1a68bdb9baa 100644 --- a/tests/wpt/mozilla/tests/mozilla/promise.html +++ b/tests/wpt/mozilla/tests/mozilla/promise.html @@ -51,4 +51,17 @@ assert_equals(rejected, 'success') }); }, 'Native reject callback gets argument'); + + promise_test(function(test) { + var t = new TestBinding; + var resolved; + var p = new Promise(function() {}); + var start = Date.now(); + t.resolvePromiseDelayed(p, 'success', 100); + return p.then(function(v) { + var end = Date.now(); + assert_true(end - start > 100); + assert_equals(v, 'success'); + }); + }, 'Native promise from async callback can be resolved');