Make canvas in WindowBuilder safe (#3320)

This commit is contained in:
daxpedda 2023-12-26 01:22:10 +01:00 committed by GitHub
parent 843d7904d6
commit e0fea25b06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 214 additions and 169 deletions

View file

@ -34,7 +34,6 @@ use crate::event_loop::EventLoopWindowTarget;
use crate::platform_impl::PlatformCustomCursorBuilder;
use crate::window::CustomCursor;
use crate::window::{Window, WindowBuilder};
use crate::SendSyncWrapper;
use web_sys::HtmlCanvasElement;
@ -105,7 +104,7 @@ pub trait WindowBuilderExtWebSys {
impl WindowBuilderExtWebSys for WindowBuilder {
fn with_canvas(mut self, canvas: Option<HtmlCanvasElement>) -> Self {
self.platform_specific.canvas = SendSyncWrapper(canvas);
self.platform_specific.set_canvas(canvas);
self
}

View file

@ -1,3 +1,4 @@
use super::super::main_thread::MainThreadMarker;
use super::{channel, AsyncReceiver, AsyncSender, Wrapper};
use std::{
cell::Ref,
@ -10,10 +11,11 @@ struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
impl<T> Dispatcher<T> {
#[track_caller]
pub fn new(value: T) -> Option<(Self, DispatchRunner<T>)> {
pub fn new(main_thread: MainThreadMarker, value: T) -> Option<(Self, DispatchRunner<T>)> {
let (sender, receiver) = channel::<Closure<T>>();
Wrapper::new(
main_thread,
value,
|value, Closure(closure)| {
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything

View file

@ -1,3 +1,4 @@
use super::super::main_thread::MainThreadMarker;
use super::Wrapper;
use atomic_waker::AtomicWaker;
use std::future;
@ -19,7 +20,7 @@ struct Sender(Arc<Inner>);
impl<T> WakerSpawner<T> {
#[track_caller]
pub fn new(value: T, handler: fn(&T, usize)) -> Option<Self> {
pub fn new(main_thread: MainThreadMarker, value: T, handler: fn(&T, usize)) -> Option<Self> {
let inner = Arc::new(Inner {
counter: AtomicUsize::new(0),
waker: AtomicWaker::new(),
@ -31,6 +32,7 @@ impl<T> WakerSpawner<T> {
let sender = Sender(Arc::clone(&inner));
let wrapper = Wrapper::new(
main_thread,
handler,
|handler, count| {
let handler = handler.borrow();
@ -86,7 +88,7 @@ impl<T> WakerSpawner<T> {
pub fn fetch(&self) -> usize {
debug_assert!(
self.0.is_main_thread(),
MainThreadMarker::new().is_some(),
"this should only be called from the main thread"
);

View file

@ -1,9 +1,8 @@
use super::super::main_thread::MainThreadMarker;
use std::cell::{Ref, RefCell};
use std::future::Future;
use std::marker::PhantomData;
use std::sync::Arc;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads.
// `value` **must** only be accessed on the main thread.
@ -34,36 +33,15 @@ unsafe impl<const SYNC: bool, V> Send for Value<SYNC, V> {}
unsafe impl<V> Sync for Value<true, V> {}
impl<const SYNC: bool, V, S: Clone + Send, E> Wrapper<SYNC, V, S, E> {
thread_local! {
static MAIN_THREAD: bool = {
#[wasm_bindgen]
extern "C" {
#[derive(Clone)]
type Global;
#[wasm_bindgen(method, getter, js_name = Window)]
fn window(this: &Global) -> JsValue;
}
let global: Global = js_sys::global().unchecked_into();
!global.window().is_undefined()
};
}
#[track_caller]
pub fn new<R: Future<Output = ()>>(
_: MainThreadMarker,
value: V,
handler: fn(&RefCell<Option<V>>, E),
receiver: impl 'static + FnOnce(Arc<RefCell<Option<V>>>) -> R,
sender_data: S,
sender_handler: fn(&S, E),
) -> Option<Self> {
Self::MAIN_THREAD.with(|safe| {
if !safe {
panic!("only callable from inside the `Window`")
}
});
let value = Arc::new(RefCell::new(Some(value)));
wasm_bindgen_futures::spawn_local({
@ -86,29 +64,16 @@ impl<const SYNC: bool, V, S: Clone + Send, E> Wrapper<SYNC, V, S, E> {
}
pub fn send(&self, event: E) {
Self::MAIN_THREAD.with(|is_main_thread| {
if *is_main_thread {
(self.handler)(&self.value.value, event)
} else {
(self.sender_handler)(&self.sender_data, event)
}
})
}
pub fn is_main_thread(&self) -> bool {
Self::MAIN_THREAD.with(|is_main_thread| *is_main_thread)
if MainThreadMarker::new().is_some() {
(self.handler)(&self.value.value, event)
} else {
(self.sender_handler)(&self.sender_data, event)
}
}
pub fn value(&self) -> Option<Ref<'_, V>> {
Self::MAIN_THREAD.with(|is_main_thread| {
if *is_main_thread {
Some(Ref::map(self.value.value.borrow(), |value| {
value.as_ref().unwrap()
}))
} else {
None
}
})
MainThreadMarker::new()
.map(|_| Ref::map(self.value.value.borrow(), |value| value.as_ref().unwrap()))
}
pub fn with_sender_data<T>(&self, f: impl FnOnce(&S) -> T) -> T {

View file

@ -9,12 +9,8 @@ use std::{
task::{Poll, Waker},
};
use crate::{
cursor::{BadImage, Cursor, CursorImage},
platform_impl::platform::r#async,
};
use crate::cursor::{BadImage, Cursor, CursorImage};
use cursor_icon::CursorIcon;
use once_cell::sync::Lazy;
use wasm_bindgen::{closure::Closure, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
@ -22,9 +18,9 @@ use web_sys::{
ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
};
use self::thread_safe::ThreadSafe;
use super::{backend::Style, r#async::AsyncSender, EventLoopWindowTarget};
use super::backend::Style;
use super::main_thread::{MainThreadMarker, MainThreadSafe};
use super::EventLoopWindowTarget;
#[derive(Debug)]
pub(crate) enum CustomCursorBuilder {
@ -51,7 +47,7 @@ impl CustomCursorBuilder {
}
#[derive(Clone, Debug)]
pub struct CustomCursor(Arc<Inner>);
pub struct CustomCursor(Arc<MainThreadSafe<RefCell<ImageState>>>);
impl Hash for CustomCursor {
fn hash<H: Hasher>(&self, state: &mut H) {
@ -68,14 +64,22 @@ impl PartialEq for CustomCursor {
impl Eq for CustomCursor {}
impl CustomCursor {
fn new(main_thread: MainThreadMarker) -> Self {
Self(Arc::new(MainThreadSafe::new(
main_thread,
RefCell::new(ImageState::Loading(None)),
)))
}
pub(crate) fn build<T>(
builder: CustomCursorBuilder,
window_target: &EventLoopWindowTarget<T>,
) -> Self {
Lazy::force(&DROP_HANDLER);
let main_thread = window_target.runner.main_thread();
Self(match builder {
match builder {
CustomCursorBuilder::Image(image) => ImageState::from_rgba(
main_thread,
window_target.runner.window(),
window_target.runner.document().clone(),
&image,
@ -84,47 +88,7 @@ impl CustomCursor {
url,
hotspot_x,
hotspot_y,
} => ImageState::from_url(url, hotspot_x, hotspot_y),
})
}
}
#[derive(Debug)]
struct Inner(Option<ThreadSafe<RefCell<ImageState>>>);
static DROP_HANDLER: Lazy<AsyncSender<ThreadSafe<RefCell<ImageState>>>> = Lazy::new(|| {
let (sender, receiver) = r#async::channel();
wasm_bindgen_futures::spawn_local(async move { while receiver.next().await.is_ok() {} });
sender
});
impl Inner {
fn new() -> Arc<Self> {
Arc::new(Inner(Some(ThreadSafe::new(RefCell::new(
ImageState::Loading(None),
)))))
}
fn get(&self) -> &RefCell<ImageState> {
self.0
.as_ref()
.expect("value has accidently already been dropped")
.get()
}
}
impl Drop for Inner {
fn drop(&mut self) {
let value = self
.0
.take()
.expect("value has accidently already been dropped");
if !value.in_origin_thread() {
DROP_HANDLER
.send(value)
.expect("sender dropped in main thread")
} => ImageState::from_url(main_thread, url, hotspot_x, hotspot_y),
}
}
}
@ -133,8 +97,9 @@ impl Drop for Inner {
pub struct CursorState(Rc<RefCell<State>>);
impl CursorState {
pub fn new(style: Style) -> Self {
pub fn new(main_thread: MainThreadMarker, style: Style) -> Self {
Self(Rc::new(RefCell::new(State {
main_thread,
style,
visible: true,
cursor: SelectedCursor::default(),
@ -147,7 +112,9 @@ impl CursorState {
match cursor {
Cursor::Icon(icon) => {
if let SelectedCursor::ImageLoading { state, .. } = &this.cursor {
if let ImageState::Loading(state) = state.get().borrow_mut().deref_mut() {
if let ImageState::Loading(state) =
state.0.get(this.main_thread).borrow_mut().deref_mut()
{
state.take();
}
}
@ -155,10 +122,16 @@ impl CursorState {
this.cursor = SelectedCursor::Named(icon);
this.set_style();
}
Cursor::Custom(cursor) => match cursor.inner.0.get().borrow_mut().deref_mut() {
Cursor::Custom(cursor) => match cursor
.inner
.0
.get(this.main_thread)
.borrow_mut()
.deref_mut()
{
ImageState::Loading(state) => {
this.cursor = SelectedCursor::ImageLoading {
state: cursor.inner.0.clone(),
state: cursor.inner.clone(),
previous: mem::take(&mut this.cursor).into(),
};
*state = Some(Rc::downgrade(&self.0));
@ -187,6 +160,7 @@ impl CursorState {
#[derive(Debug)]
struct State {
main_thread: MainThreadMarker,
style: Style,
visible: bool,
cursor: SelectedCursor,
@ -210,7 +184,7 @@ impl State {
enum SelectedCursor {
Named(CursorIcon),
ImageLoading {
state: Arc<Inner>,
state: CustomCursor,
previous: Previous,
},
ImageReady(Rc<Image>),
@ -264,7 +238,12 @@ enum ImageState {
}
impl ImageState {
fn from_rgba(window: &Window, document: Document, image: &CursorImage) -> Arc<Inner> {
fn from_rgba(
main_thread: MainThreadMarker,
window: &Window,
document: Document,
image: &CursorImage,
) -> CustomCursor {
// 1. Create an `ImageData` from the RGBA data.
// 2. Create an `ImageBitmap` from the `ImageData`.
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
@ -316,10 +295,11 @@ impl ImageState {
.expect("unexpected exception in `createImageBitmap()`"),
);
let this = Inner::new();
#[allow(clippy::arc_with_non_send_sync)]
let this = CustomCursor::new(main_thread);
wasm_bindgen_futures::spawn_local({
let weak = Arc::downgrade(&this);
let weak = Arc::downgrade(&this.0);
let CursorImage {
width,
height,
@ -394,7 +374,7 @@ impl ImageState {
let Some(this) = weak.upgrade() else {
return;
};
let mut this = this.get().borrow_mut();
let mut this = this.get(main_thread).borrow_mut();
let Some(blob) = blob else {
log::error!("creating custom cursor failed");
@ -422,17 +402,24 @@ impl ImageState {
.expect("unexpected exception in `URL.createObjectURL()`")
};
Self::decode(weak, url, true, hotspot_x, hotspot_y).await;
Self::decode(main_thread, weak, url, true, hotspot_x, hotspot_y).await;
}
});
this
}
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Arc<Inner> {
let this = Inner::new();
fn from_url(
main_thread: MainThreadMarker,
url: String,
hotspot_x: u16,
hotspot_y: u16,
) -> CustomCursor {
#[allow(clippy::arc_with_non_send_sync)]
let this = CustomCursor::new(main_thread);
wasm_bindgen_futures::spawn_local(Self::decode(
Arc::downgrade(&this),
main_thread,
Arc::downgrade(&this.0),
url,
false,
hotspot_x,
@ -443,7 +430,8 @@ impl ImageState {
}
async fn decode(
weak: sync::Weak<Inner>,
main_thread: MainThreadMarker,
weak: sync::Weak<MainThreadSafe<RefCell<ImageState>>>,
url: String,
object: bool,
hotspot_x: u16,
@ -462,7 +450,7 @@ impl ImageState {
let Some(this) = weak.upgrade() else {
return;
};
let mut this = this.get().borrow_mut();
let mut this = this.get(main_thread).borrow_mut();
let ImageState::Loading(state) = this.deref_mut() else {
unreachable!("found invalid state");
@ -533,46 +521,3 @@ impl Image {
})
}
}
mod thread_safe {
use std::mem;
use std::thread::{self, ThreadId};
#[derive(Debug)]
pub struct ThreadSafe<T> {
origin_thread: ThreadId,
value: T,
}
impl<T> ThreadSafe<T> {
pub fn new(value: T) -> Self {
Self {
origin_thread: thread::current().id(),
value,
}
}
pub fn get(&self) -> &T {
if self.origin_thread == thread::current().id() {
&self.value
} else {
panic!("value not accessible outside its origin thread")
}
}
pub fn in_origin_thread(&self) -> bool {
self.origin_thread == thread::current().id()
}
}
impl<T> Drop for ThreadSafe<T> {
fn drop(&mut self) {
if mem::needs_drop::<T>() && self.origin_thread != thread::current().id() {
panic!("value can't be dropped outside its origin thread")
}
}
}
unsafe impl<T> Send for ThreadSafe<T> {}
unsafe impl<T> Sync for ThreadSafe<T> {}
}

View file

@ -1,3 +1,4 @@
use super::super::main_thread::MainThreadMarker;
use super::super::DeviceId;
use super::{backend, state::State};
use crate::dpi::PhysicalSize;
@ -37,6 +38,7 @@ impl Clone for Shared {
type OnEventHandle<T> = RefCell<Option<EventListenerHandle<dyn FnMut(T)>>>;
pub struct Execution {
main_thread: MainThreadMarker,
proxy_spawner: WakerSpawner<Weak<Self>>,
control_flow: Cell<ControlFlow>,
poll_strategy: Cell<PollStrategy>,
@ -143,13 +145,14 @@ impl Runner {
impl Shared {
pub fn new() -> Self {
let main_thread = MainThreadMarker::new().expect("only callable from inside the `Window`");
#[allow(clippy::disallowed_methods)]
let window = web_sys::window().expect("only callable from inside the `Window`");
#[allow(clippy::disallowed_methods)]
let document = window.document().expect("Failed to obtain document");
Shared(Rc::<Execution>::new_cyclic(|weak| {
let proxy_spawner = WakerSpawner::new(weak.clone(), |runner, count| {
let proxy_spawner = WakerSpawner::new(main_thread, weak.clone(), |runner, count| {
if let Some(runner) = runner.upgrade() {
Shared(runner).send_events(iter::repeat(Event::UserEvent(())).take(count))
}
@ -157,6 +160,7 @@ impl Shared {
.expect("`EventLoop` has to be created in the main thread");
Execution {
main_thread,
proxy_spawner,
control_flow: Cell::new(ControlFlow::default()),
poll_strategy: Cell::new(PollStrategy::default()),
@ -184,6 +188,10 @@ impl Shared {
}))
}
pub fn main_thread(&self) -> MainThreadMarker {
self.0.main_thread
}
pub fn window(&self) -> &web_sys::Window {
&self.0.window
}

View file

@ -0,0 +1,96 @@
use std::fmt::{self, Debug, Formatter};
use std::marker::PhantomData;
use std::mem;
use std::sync::OnceLock;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use super::r#async::{self, AsyncSender};
thread_local! {
static MAIN_THREAD: bool = {
#[wasm_bindgen]
extern "C" {
#[derive(Clone)]
type Global;
#[wasm_bindgen(method, getter, js_name = Window)]
fn window(this: &Global) -> JsValue;
}
let global: Global = js_sys::global().unchecked_into();
!global.window().is_undefined()
};
}
#[derive(Clone, Copy, Debug)]
pub struct MainThreadMarker(PhantomData<*const ()>);
impl MainThreadMarker {
pub fn new() -> Option<Self> {
MAIN_THREAD.with(|is| is.then_some(Self(PhantomData)))
}
}
pub struct MainThreadSafe<T: 'static>(Option<T>);
impl<T> MainThreadSafe<T> {
pub fn new(_: MainThreadMarker, value: T) -> Self {
DROP_HANDLER.get_or_init(|| {
let (sender, receiver) = r#async::channel();
wasm_bindgen_futures::spawn_local(
async move { while receiver.next().await.is_ok() {} },
);
sender
});
Self(Some(value))
}
pub fn into_inner(mut self, _: MainThreadMarker) -> T {
self.0.take().expect("already taken or dropped")
}
pub fn get(&self, _: MainThreadMarker) -> &T {
self.0.as_ref().expect("already taken or dropped")
}
}
impl<T: Debug> Debug for MainThreadSafe<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if MainThreadMarker::new().is_some() {
f.debug_tuple("MainThreadSafe").field(&self.0).finish()
} else {
f.debug_struct("MainThreadSafe").finish_non_exhaustive()
}
}
}
impl<T> Drop for MainThreadSafe<T> {
fn drop(&mut self) {
if let Some(value) = self.0.take() {
if mem::needs_drop::<T>() && MainThreadMarker::new().is_none() {
DROP_HANDLER
.get()
.expect("drop handler not initialized when setting canvas")
.send(DropBox(Box::new(value)))
.expect("sender dropped in main thread")
}
}
}
}
unsafe impl<T> Send for MainThreadSafe<T> {}
unsafe impl<T> Sync for MainThreadSafe<T> {}
static DROP_HANDLER: OnceLock<AsyncSender<DropBox>> = OnceLock::new();
struct DropBox(Box<dyn Any>);
unsafe impl Send for DropBox {}
unsafe impl Sync for DropBox {}
trait Any {}
impl<T> Any for T {}

View file

@ -23,6 +23,7 @@ mod device;
mod error;
mod event_loop;
mod keyboard;
mod main_thread;
mod monitor;
mod window;

View file

@ -17,6 +17,7 @@ use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey};
use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes};
use crate::window::{WindowAttributes, WindowId as RootWindowId};
use super::super::main_thread::MainThreadMarker;
use super::super::WindowId;
use super::animation_frame::AnimationFrameHandler;
use super::event_handle::EventListenerHandle;
@ -66,13 +67,18 @@ pub struct Style {
impl Canvas {
pub fn create(
main_thread: MainThreadMarker,
id: WindowId,
window: web_sys::Window,
document: Document,
attr: &WindowAttributes,
platform_attr: PlatformSpecificWindowBuilderAttributes,
mut platform_attr: PlatformSpecificWindowBuilderAttributes,
) -> Result<Self, RootOE> {
let canvas = match platform_attr.canvas.0 {
let canvas = match platform_attr.canvas.take().map(|canvas| {
Arc::try_unwrap(canvas)
.map(|canvas| canvas.into_inner(main_thread))
.unwrap_or_else(|canvas| canvas.get(main_thread).clone())
}) {
Some(canvas) => canvas,
None => document
.create_element("canvas")

View file

@ -5,9 +5,9 @@ use crate::window::{
Cursor, CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType,
WindowAttributes, WindowButtons, WindowId as RootWI, WindowLevel,
};
use crate::SendSyncWrapper;
use super::cursor::CursorState;
use super::main_thread::{MainThreadMarker, MainThreadSafe};
use super::r#async::Dispatcher;
use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen};
use web_sys::HtmlCanvasElement;
@ -15,6 +15,7 @@ use web_sys::HtmlCanvasElement;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use std::sync::Arc;
pub struct Window {
inner: Dispatcher<Inner>,
@ -38,10 +39,16 @@ impl Window {
let window = target.runner.window();
let document = target.runner.document();
let canvas =
backend::Canvas::create(id, window.clone(), document.clone(), &attr, platform_attr)?;
let canvas = backend::Canvas::create(
target.runner.main_thread(),
id,
window.clone(),
document.clone(),
&attr,
platform_attr,
)?;
let canvas = Rc::new(RefCell::new(canvas));
let cursor = CursorState::new(canvas.borrow().style().clone());
let cursor = CursorState::new(target.runner.main_thread(), canvas.borrow().style().clone());
target.register(&canvas, id);
@ -62,7 +69,7 @@ impl Window {
inner.set_window_icon(attr.window_icon);
let canvas = Rc::downgrade(&inner.canvas);
let (dispatcher, runner) = Dispatcher::new(inner).unwrap();
let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner).unwrap();
target.runner.add_canvas(RootWI(id), canvas, runner);
Ok(Window { inner: dispatcher })
@ -465,16 +472,30 @@ impl From<u64> for WindowId {
#[derive(Clone)]
pub struct PlatformSpecificWindowBuilderAttributes {
pub(crate) canvas: SendSyncWrapper<Option<backend::RawCanvasType>>,
pub(crate) canvas: Option<Arc<MainThreadSafe<backend::RawCanvasType>>>,
pub(crate) prevent_default: bool,
pub(crate) focusable: bool,
pub(crate) append: bool,
}
impl PlatformSpecificWindowBuilderAttributes {
pub(crate) fn set_canvas(&mut self, canvas: Option<backend::RawCanvasType>) {
let Some(canvas) = canvas else {
self.canvas = None;
return;
};
let main_thread = MainThreadMarker::new()
.expect("received a `HtmlCanvasElement` outside the window context");
self.canvas = Some(Arc::new(MainThreadSafe::new(main_thread, canvas)));
}
}
impl Default for PlatformSpecificWindowBuilderAttributes {
fn default() -> Self {
Self {
canvas: SendSyncWrapper(None),
canvas: None,
prevent_default: true,
focusable: true,
append: false,