Skip to content

Commit

Permalink
Support an equivalent of Trusted<T> for Rc<Promise> objects named Tru…
Browse files Browse the repository at this point in the history
…stedPromise.
  • Loading branch information
jdm committed Sep 22, 2016
1 parent 27d44c8 commit 498ccd4
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 10 deletions.
94 changes: 86 additions & 8 deletions components/script/dom/bindings/refcounted.rs
Expand Up @@ -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;
Expand All @@ -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};


Expand All @@ -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<Promise> 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<Promise>) -> 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<Promise> {
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
Expand Down Expand Up @@ -117,23 +176,32 @@ impl<T: Reflectable> Clone for Trusted<T> {

/// 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<HashMap<*const libc::c_void, Weak<TrustedReference>>>,
reflectable_table: RefCell<HashMap<*const libc::c_void, Weak<TrustedReference>>>,
promise_table: RefCell<HashMap<*const Promise, Vec<Rc<Promise>>>>,
}

impl LiveDOMReferences {
/// Set up the thread-local data required for storing the outstanding DOM references.
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<Promise>) {
let mut table = self.promise_table.borrow_mut();
table.entry(&*promise).or_insert(vec![]).push(promise)
}

fn addref<T: Reflectable>(&self, ptr: *const T) -> Arc<TrustedReference> {
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) {
Expand Down Expand Up @@ -173,17 +241,27 @@ fn remove_nulls<K: Eq + Hash + Clone, V> (table: &mut HashMap<K, Weak<V>>) {
}

/// 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());
}
}
});
}
3 changes: 2 additions & 1 deletion components/script/dom/bindings/trace.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -303,6 +303,7 @@ no_jsmanaged_fields!(Metadata);
no_jsmanaged_fields!(NetworkError);
no_jsmanaged_fields!(Atom, Namespace, QualName);
no_jsmanaged_fields!(Trusted<T: Reflectable>);
no_jsmanaged_fields!(TrustedPromise);
no_jsmanaged_fields!(PropertyDeclarationBlock);
no_jsmanaged_fields!(HashSet<T>);
// These three are interdependent, if you plan to put jsmanaged data
Expand Down
8 changes: 8 additions & 0 deletions components/script/dom/promise.rs
Expand Up @@ -79,6 +79,14 @@ impl Promise {
}
}

#[allow(unsafe_code, unrooted_must_root)]
pub fn duplicate(&self) -> Rc<Promise> {
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<Promise> {
assert!(IsPromiseObject(obj));
Expand Down
33 changes: 32 additions & 1 deletion components/script/dom/testbinding.rs
Expand Up @@ -25,19 +25,22 @@ 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;
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]
Expand Down Expand Up @@ -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<Rc<SimpleCallback>>,
Expand Down Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions components/script/dom/webidls/TestBinding.webidl
Expand Up @@ -519,6 +519,7 @@ interface TestBinding {
void promiseResolveNative(Promise<any> p, any value);
void promiseRejectNative(Promise<any> p, any value);
void promiseRejectWithTypeError(Promise<any> p, USVString message);
void resolvePromiseDelayed(Promise<any> p, DOMString value, unsigned long long ms);

void panic();
};
Expand Down
3 changes: 3 additions & 0 deletions components/script/timers.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -68,13 +69,15 @@ struct OneshotTimer {
pub enum OneshotTimerCallback {
XhrTimeout(XHRTimeoutCallback),
JsTimer(JsTimerTask),
TestBindingCallback(TestBindingCallback),
}

impl OneshotTimerCallback {
fn invoke<T: Reflectable>(self, this: &T, js_timers: &JsTimers) {
match self {
OneshotTimerCallback::XhrTimeout(callback) => callback.invoke(),
OneshotTimerCallback::JsTimer(task) => task.invoke(this, js_timers),
OneshotTimerCallback::TestBindingCallback(callback) => callback.invoke(),
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions tests/wpt/mozilla/tests/mozilla/promise.html
Expand Up @@ -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');
</script>

0 comments on commit 498ccd4

Please sign in to comment.