From 1410b4e4e6efa60084bee35a65671d66391ef5bc Mon Sep 17 00:00:00 2001 From: Artur Kovacs Date: Sun, 30 Aug 2020 14:21:06 +0200 Subject: [PATCH] X11 clipboard impl work in progress. --- examples/primary_selection.rs | 4 +- src/x11_clipboard.rs | 204 ++++++++++++++++++++-------------- 2 files changed, 123 insertions(+), 85 deletions(-) diff --git a/examples/primary_selection.rs b/examples/primary_selection.rs index fd3d170..1809f47 100644 --- a/examples/primary_selection.rs +++ b/examples/primary_selection.rs @@ -2,11 +2,11 @@ extern crate clipboard; use clipboard::ClipboardProvider; #[cfg(target_os = "linux")] -use clipboard::x11_clipboard::{X11ClipboardContext, Primary}; +use clipboard::x11_clipboard::{X11ClipboardContext}; #[cfg(target_os = "linux")] fn main() { - let mut ctx: X11ClipboardContext = ClipboardProvider::new().unwrap(); + let mut ctx: X11ClipboardContext = ClipboardProvider::new().unwrap(); let the_string = "Hello, world!"; diff --git a/src/x11_clipboard.rs b/src/x11_clipboard.rs index 01f074f..7c038fb 100644 --- a/src/x11_clipboard.rs +++ b/src/x11_clipboard.rs @@ -129,6 +129,7 @@ mod locking { } +// TODO move this back to the manager struct as a regular field. /// Variable used to wait more time if we've received an INCR /// notification, which means that we're going to receive large /// amounts of data from the selection owner. @@ -162,13 +163,6 @@ type BufferPtr = Option>; type Atoms = Vec; type NotifyCallback = Option bool) + Send + Sync + 'static>>; -struct SendableConnection { - con: xcb::Connection -} - -/// Need to manually `impl Send` because the connection contains pointers, -/// and no pointer is `Send` by default. -unsafe impl Send for SendableConnection {} lazy_static! { static ref MANAGER: Option> = { @@ -180,25 +174,41 @@ lazy_static! { /// Connection to X11 server /// This is an `Arc>` because it's shared between the user's thread and /// the x11 event processing thread. - static ref CONNECTION: Mutex = { + static ref SHARED_STATE: Mutex = { let connection = xcb::Connection::connect(None).unwrap().0; - Mutex::new(SendableConnection { con: connection }) + Mutex::new(SharedState { + conn: connection, + atoms: Default::default(), + common_atoms: Default::default() + }) }; - static ref ATOM_HANDLER: Mutex = Mutex::new(Default::default()); + // Used to wait/notify the arrival of the SelectionNotify event when + // we requested the clipboard content from other selection owner. + static ref CONDVAR: Condvar = Condvar::new(); + + //static ref ATOM_HANDLER: Mutex = Mutex::new(Default::default()); } +/// The name indicates that objects in this struct are shared between +/// the event processing thread and the user tread. However it's important +/// that the `Manager` itself is also shared. So the real reason for splitting these +/// apart from the `Manager` is to conform to Rust's locking and aliasing rules but that is hard to +/// convey in a short name. +struct SharedState { + conn: xcb::Connection, -#[derive(Default)] -struct AtomHandler { // Cache of known atoms atoms: BTreeMap, // Cache of common used atoms by us common_atoms: Atoms, } +/// Need to manually `impl Send` because the connection contains pointers, +/// and no pointer is `Send` by default. +unsafe impl Send for SharedState {} -impl AtomHandler { +impl SharedState { fn get_atom_by_id(&mut self, id: usize) -> xproto::Atom { if self.common_atoms.is_empty() { self.common_atoms = self.get_atoms(&COMMON_ATOM_NAMES); @@ -207,7 +217,7 @@ impl AtomHandler { } fn get_atoms(&mut self, names: &[&'static str]) -> Atoms { - let connection = CONNECTION.lock().unwrap(); + let connection = SHARED_STATE.lock().unwrap(); let mut results = vec![0; names.len()]; let mut cookies = HashMap::with_capacity(names.len()); @@ -217,14 +227,14 @@ impl AtomHandler { if let Some(pair) = found { *res = *pair.1; } else { - cookies.insert(name, xproto::intern_atom(&connection.con, false, name)); + cookies.insert(name, xproto::intern_atom(&connection.conn, false, name)); } } for (res, name) in results.iter_mut().zip(names.iter()) { if *res == 0 { let reply = unsafe { xcb::ffi::xproto::xcb_intern_atom_reply( - connection.con.get_raw_conn(), + connection.conn.get_raw_conn(), cookies.get(name).unwrap().cookie, std::ptr::null_mut() )}; @@ -241,10 +251,6 @@ impl AtomHandler { } } -fn get_atom_by_id(id: usize) -> xproto::Atom { - ATOM_HANDLER.lock().unwrap().get_atom_by_id(id) -} - struct Manager { /// Original comment: "Access to the whole Manager" @@ -266,15 +272,14 @@ struct Manager { // all events related about the clipboard in a background thread window: xcb::xproto::Window, - // Used to wait/notify the arrival of the SelectionNotify event when - // we requested the clipboard content from other selection owner. - condvar: Condvar, - // Thread used to run a background message loop to wait X11 events // about clipboard. The X11 selection owner will be a hidden window // created by us just for the clipboard purpose/communication. thread_handle: Option>, + // WARNING: The callback must not attempt to lock the manager or the shared state. + // (Otherwise the code needs to be restructured slightly) + // // Internal callback used when a SelectionNotify is received (or the // whole data content is received by the INCR method). So this // callback can use the notification by different purposes (e.g. get @@ -337,8 +342,8 @@ impl Manager { XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_CW_EVENT_MASK }; - let connection = CONNECTION.lock().unwrap(); - let setup = connection.con.get_setup(); + let connection = SHARED_STATE.lock().unwrap(); + let setup = connection.conn.get_setup(); if std::ptr::null() == setup.ptr { return Err("Could not get setup for connection".into()); } @@ -352,10 +357,10 @@ impl Manager { XCB_EVENT_MASK_PROPERTY_CHANGE | // To receive DestroyNotify event and stop the message loop. XCB_EVENT_MASK_STRUCTURE_NOTIFY; - let mut window = connection.con.generate_id(); + let window = connection.conn.generate_id(); unsafe { xcb::ffi::xproto::xcb_create_window( - connection.con.get_raw_conn(), + connection.conn.get_raw_conn(), 0, window, (*screen).root, @@ -372,7 +377,6 @@ impl Manager { Ok(Manager { //mutex: Mutex::new(()), window: 0, - condvar: Condvar::new(), thread_handle: Some(thread_handle), callback: None, callback_result: false, @@ -429,12 +433,13 @@ impl Manager { self.reply_offset += n; } - fn call_callback(&mut self, reply: *mut xcb_get_property_reply_t) { + // Rust impl: It's strange, the reply attribute is also unused in the original code. + fn call_callback(&mut self, _reply: *mut xcb_get_property_reply_t) { self.callback_result = false; if let Some(callback) = &self.callback { self.callback_result = callback(); } - self.condvar.notify_one(); + CONDVAR.notify_one(); self.reply_data = None; } @@ -453,13 +458,14 @@ impl Drop for Manager { //xcb::xproto::Window let mut x11_clipboard_manager = 0; { - let con = CONNECTION.lock().unwrap(); + let mut shared = SHARED_STATE.lock().unwrap(); + let clipboard_manager_atom = shared.get_atom_by_id(CLIPBOARD_MANAGER); let cookie = { - get_selection_owner(&con.con, get_atom_by_id(CLIPBOARD_MANAGER)) + get_selection_owner(&shared.conn, clipboard_manager_atom) }; let reply = { unsafe { xcb::ffi::xproto::xcb_get_selection_owner_reply( - con.con.get_raw_conn(), + shared.conn.get_raw_conn(), cookie.cookie, std::ptr::null_mut() )} @@ -472,8 +478,14 @@ impl Drop for Manager { } } if x11_clipboard_manager != 0 { - let atoms = vec![get_atom_by_id(SAVE_TARGETS)]; - let selection = get_atom_by_id(CLIPBOARD_MANAGER); + let atoms; + let selection; + { + let mut shared = SHARED_STATE.lock().unwrap(); + atoms = vec![shared.get_atom_by_id(SAVE_TARGETS)]; + selection = shared.get_atom_by_id(CLIPBOARD_MANAGER); + } // let go of the shared state lock before invoking `get_data_from_selection_owner` + // Start the SAVE_TARGETS mechanism so the X11 // CLIPBOARD_MANAGER will save our clipboard data // from now on. @@ -486,16 +498,16 @@ impl Drop for Manager { } if self.window != 0 { - let con = CONNECTION.lock().unwrap(); + let con = SHARED_STATE.lock().unwrap(); unsafe { xcb::ffi::xproto::xcb_destroy_window( - con.con.get_raw_conn(), + con.conn.get_raw_conn(), self.window )}; - con.con.flush(); + con.conn.flush(); } if let Some(handle) = self.thread_handle.take() { - handle.join(); + handle.join().ok(); } // This is not needed because the connection is automatically disconnected when droped @@ -509,7 +521,6 @@ fn process_x11_events() { XCB_DESTROY_NOTIFY, XCB_SELECTION_CLEAR, XCB_SELECTION_REQUEST, - XCB_SELECTION_NOTIFY, XCB_PROPERTY_NOTIFY }; @@ -517,7 +528,7 @@ fn process_x11_events() { let mut event; while !stop { event = { - let maybe_event = CONNECTION.lock().unwrap().con.wait_for_event(); + let maybe_event = SHARED_STATE.lock().unwrap().conn.wait_for_event(); if let Some(e) = maybe_event { e } else { @@ -568,7 +579,11 @@ fn process_x11_events() { fn handle_selection_clear_event(event: *mut xcb_selection_clear_event_t) { let selection = unsafe { (*event).selection }; - if selection == get_atom_by_id(CLIPBOARD) { + let clipboard_atom = { + let mut shared = SHARED_STATE.lock().unwrap(); + shared.get_atom_by_id(CLIPBOARD) + }; + if selection == clipboard_atom { if let Some(manager) = &*MANAGER { manager.lock().unwrap().clear_data(); } @@ -582,7 +597,6 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) { } else { return; }; - let mut conn = CONNECTION.lock().unwrap(); let target; let requestor; let property; @@ -595,39 +609,55 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) { time = (*event).time; selection = (*event).selection; } - if target == get_atom_by_id(TARGETS) { + let targets_atom; + let save_targets_atom; + let multiple_atom; + let atom_atom; + { + let mut shared = SHARED_STATE.lock().unwrap(); + targets_atom = shared.get_atom_by_id(TARGETS); + save_targets_atom = shared.get_atom_by_id(SAVE_TARGETS); + multiple_atom = shared.get_atom_by_id(MULTIPLE); + atom_atom = shared.get_atom_by_id(ATOM); + } + if target == targets_atom { let mut targets = Atoms::with_capacity(4); - targets.push(get_atom_by_id(TARGETS)); - targets.push(get_atom_by_id(SAVE_TARGETS)); - targets.push(get_atom_by_id(MULTIPLE)); + targets.push(targets_atom); + targets.push(save_targets_atom); + targets.push(multiple_atom); let manager = manager_mutex.lock().unwrap(); for atom in manager.data.keys() { targets.push(*atom); } + let shared = SHARED_STATE.lock().unwrap(); // Set the "property" of "requestor" with the clipboard // formats ("targets", atoms) that we provide. unsafe { xcb_change_property( - conn.con.get_raw_conn(), + shared.conn.get_raw_conn(), XCB_PROP_MODE_REPLACE as u8, requestor, property, - get_atom_by_id(ATOM), + atom_atom, 8 * std::mem::size_of::() as u8, targets.len() as u32, targets.as_ptr() as *const _ )}; - } else if target == get_atom_by_id(SAVE_TARGETS) { + } else if target == save_targets_atom { // Do nothing - } else if target == get_atom_by_id(MULTIPLE) { + } else if target == multiple_atom { let mut manager = manager_mutex.lock().unwrap(); - let reply = get_and_delete_property( - &mut conn.con, - requestor, - property, - get_atom_by_id(ATOM_PAIR), - false - ); + let reply = { + let mut shared = SHARED_STATE.lock().unwrap(); + let atom_pair_atom = shared.get_atom_by_id(ATOM_PAIR); + get_and_delete_property( + &mut shared.conn, + requestor, + property, + atom_pair_atom, + false + ) + }; if reply != std::ptr::null_mut() { let mut ptr: *mut xcb_atom_t = unsafe { xcb_get_property_value(reply) } as *mut xcb_atom_t; let end = unsafe { ptr.offset( @@ -645,8 +675,9 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) { if !manager.set_requestor_property_with_clipboard_content( requestor, property, target ) { + let shared = SHARED_STATE.lock().unwrap(); unsafe { xcb_change_property( - conn.con.get_raw_conn(), + shared.conn.get_raw_conn(), XCB_PROP_MODE_REPLACE as u8, requestor, property, @@ -677,14 +708,15 @@ fn handle_selection_request_event(event: *mut xcb_selection_request_event_t) { target: target, property: property, }; + let shared = SHARED_STATE.lock().unwrap(); unsafe { xcb_send_event( - conn.con.get_raw_conn(), + shared.conn.get_raw_conn(), 0, requestor, XCB_EVENT_MASK_NO_EVENT, ¬ify as *const _ as *const _ )}; - conn.con.flush(); + shared.conn.flush(); } fn handle_selection_notify_event(event: *mut xcb_selection_notify_event_t) { @@ -702,18 +734,18 @@ fn handle_selection_notify_event(event: *mut xcb_selection_notify_event_t) { requestor = (*event).requestor; property = (*event).property; } - let mut connection = CONNECTION.lock().unwrap(); + let mut shared = SHARED_STATE.lock().unwrap(); let mut manager = manager_mutex.lock().unwrap(); assert_eq!(requestor, manager.window); - if target == get_atom_by_id(TARGETS) { - manager.target_atom = get_atom_by_id(ATOM); + if target == shared.get_atom_by_id(TARGETS) { + manager.target_atom = shared.get_atom_by_id(ATOM); } else { manager.target_atom = target; } let mut reply = get_and_delete_property( - &mut connection.con, + &mut shared.conn, requestor, property, manager.target_atom, @@ -723,11 +755,12 @@ fn handle_selection_notify_event(event: *mut xcb_selection_notify_event_t) { let reply_type = unsafe { (*reply).type_ }; // In this case, We're going to receive the clipboard content in // chunks of data with several PropertyNotify events. - if reply_type == get_atom_by_id(INCR) { + let incr_atom = shared.get_atom_by_id(INCR); + if reply_type == incr_atom { unsafe { libc::free(reply as *mut _); } reply = get_and_delete_property( - &mut connection.con, - requestor, property, get_atom_by_id(INCR), true + &mut shared.conn, + requestor, property, incr_atom, true ); if reply != std::ptr::null_mut() { if unsafe { xcb_get_property_value_length(reply) } == 4 { @@ -768,10 +801,10 @@ fn handle_property_notify_event(event: *mut xcb_property_notify_event_t) { window = (*event).window; } let mut manager = manager_mutex.lock().unwrap(); - let mut conn = CONNECTION.lock().unwrap(); - if manager.incr_process && state == XCB_PROPERTY_NEW_VALUE && atom == get_atom_by_id(CLIPBOARD) { + let mut shared = SHARED_STATE.lock().unwrap(); + if manager.incr_process && state == XCB_PROPERTY_NEW_VALUE && atom == shared.get_atom_by_id(CLIPBOARD) { let reply = get_and_delete_property( - &mut conn.con, window, atom, manager.target_atom, true + &mut shared.conn, window, atom, manager.target_atom, true ); if reply != std::ptr::null_mut() { INCR_RECEIVED.store(true, Ordering::SeqCst); @@ -830,10 +863,11 @@ fn get_data_from_selection_owner( } else { return false; }; - let mut manager = manager_mutex.lock().unwrap(); if selection == 0 { - selection = get_atom_by_id(CLIPBOARD); + let mut shared = SHARED_STATE.lock().unwrap(); + selection = shared.get_atom_by_id(CLIPBOARD); } + let mut manager = manager_mutex.lock().unwrap(); manager.callback = callback; // Clear data if we are not the selection owner. @@ -845,23 +879,24 @@ fn get_data_from_selection_owner( // text format/atom. for atom in atoms.iter() { { - let connection = CONNECTION.lock().unwrap(); + let mut shared = SHARED_STATE.lock().unwrap(); + let clipboard_atom = shared.get_atom_by_id(CLIPBOARD); xproto::convert_selection( - &connection.con, + &shared.conn, manager.window, selection, *atom, - get_atom_by_id(CLIPBOARD), + clipboard_atom, xcb::base::CURRENT_TIME ); - connection.con.flush(); + shared.conn.flush(); } // We use the "m_incr_received" to wait several timeouts in case // that we've received the INCR SelectionNotify or // PropertyNotify events. 'incr_loop: loop { - match manager.condvar.wait_timeout(manager, CV_TIMEOUT) { + match CONDVAR.wait_timeout(manager, CV_TIMEOUT) { Ok((guard, status)) => { manager = guard; if !status.timed_out() { @@ -889,10 +924,13 @@ fn get_data_from_selection_owner( fn get_x11_selection_owner() -> xcb::xproto::Window { let mut result = 0; - let conn = CONNECTION.lock().unwrap(); - let cookie = xproto::get_selection_owner(&conn.con, get_atom_by_id(CLIPBOARD)); - let reply = unsafe { - xcb_get_selection_owner_reply(conn.con.get_raw_conn(), cookie.cookie, std::ptr::null_mut()) + let mut shared = SHARED_STATE.lock().unwrap(); + let clipboard_atom = shared.get_atom_by_id(CLIPBOARD); + let cookie = xproto::get_selection_owner(&shared.conn, clipboard_atom); + let reply = unsafe { + xcb_get_selection_owner_reply( + shared.conn.get_raw_conn(), cookie.cookie, std::ptr::null_mut() + ) }; if reply != std::ptr::null_mut() { result = unsafe { (*reply).owner };