1 | // SPDX-License-Identifier: MIT OR Apache-2.0 OR Zlib |
2 | |
3 | // Copyright 2023 John Nunley |
4 | // |
5 | // Licensed under the Apache License, Version 2.0, the MIT License, and |
6 | // the Zlib license. You may not use this software except in compliance |
7 | // with at least one of these licenses. You should have received a copy |
8 | // of these licenses with this software. You may also find them at: |
9 | // |
10 | // http://www.apache.org/licenses/LICENSE-2.0 |
11 | // https://opensource.org/licenses/MIT |
12 | // https://opensource.org/licenses/Zlib |
13 | // |
14 | // Unless required by applicable law or agreed to in writing, software |
15 | // distributed under these licenses is distributed on an "AS IS" BASIS, |
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
17 | // See the licenses for the specific language governing permissions and |
18 | // limitations under the licenses. |
19 | |
20 | //! A tiny set of bindings to the [Xlib] library. |
21 | //! |
22 | //! The primary contemporary library for handling [Xlib] is the [`x11-dl`] crate. However, there are three |
23 | //! primary issues. |
24 | //! |
25 | //! 1. **You should not be using Xlib in 2023.** [Xlib] is legacy code, and even that doesn't get across |
26 | //! how poor the API decisions that it's locked itself into are. It has a global error hook for |
27 | //! some reason, thread-safety is a mess, and it has so many soundness holes it might as well be made |
28 | //! out of swiss cheese. You should not be using [Xlib]. If you *have* to use [Xlib], you should just |
29 | //! run all of your logic using the much more sound [XCB] library, or, even more ideally, something |
30 | //! like [`x11rb`]. Then, you take the `Display` pointer and use it for whatever legacy API you've |
31 | //! locked yourself into, and use [XCB] or [`x11rb`] for everything else. Yes, I just called [GLX] |
32 | //! a legacy API. It's the 2020's now. [Vulkan] and [`wgpu`] run everywhere aside from legacy machines. |
33 | //! Not to mention, they support [XCB]. |
34 | //! |
35 | //! 2. Even if you manage to use [`x11-dl`] without tripping over the legacy API, it is a massive crate. |
36 | //! [Xlib] comes with quite a few functions, most of which are unnecessary in the 21st century. |
37 | //! Even if you don't use any of these and just stick to [XCB], you still pay the price for it. |
38 | //! Binaries that use [`x11-dl`] need to dedicate a significant amount of their binary and memory |
39 | //! space to the library. Even on Release builds, I have recorded [`x11-dl`] taking up to seven |
40 | //! percent of the binary. |
41 | //! |
42 | //! 3. Global error handling. [Xlib] has a single global error hook. This is reminiscent of the Unix |
43 | //! signal handling API, in that it makes it difficult to create well-modularized programs |
44 | //! since they will fight with each-other over the error handlers. However, unlike the signal |
45 | //! handling API, there is no way to tell if you're replacing an existing error hook. |
46 | //! |
47 | //! `tiny-xlib` aims to solve all of these problems. It provides a safe API around [Xlib] that is |
48 | //! conducive to being handed off to both [XCB] APIs and legacy [Xlib] APIs. The library only |
49 | //! imports absolutely necessary functions. In addition, it also provides a common API for |
50 | //! handling errors in a safe, modular way. |
51 | //! |
52 | //! # Features |
53 | //! |
54 | //! - Safe API around [Xlib]. See the [`Display`] structure. |
55 | //! - Minimal set of dependencies. |
56 | //! - Implements [`AsRawXcbConnection`], which allows it to be used with [XCB] APIs. |
57 | //! - Modular error handling. |
58 | //! |
59 | //! # Non-Features |
60 | //! |
61 | //! - Any API outside of opening [`Display`]s and handling errors. If this library doesn't support some |
62 | //! feature, it's probably intentional. You should use [XCB] or [`x11rb`] instead. This includes: |
63 | //! - Window management. |
64 | //! - Any extensions outside of `Xlib-xcb`. |
65 | //! - IME handling. |
66 | //! - Hardware rendering. |
67 | //! |
68 | //! # Examples |
69 | //! |
70 | //! ```no_run |
71 | //! use as_raw_xcb_connection::AsRawXcbConnection; |
72 | //! use tiny_xlib::Display; |
73 | //! |
74 | //! use x11rb::connection::Connection; |
75 | //! use x11rb::xcb_ffi::XCBConnection; |
76 | //! |
77 | //! # fn main() -> Result<(), Box<dyn std::error::Error>> { |
78 | //! // Open a display. |
79 | //! let display = Display::new(None)?; |
80 | //! |
81 | //! // Get the XCB connection. |
82 | //! let xcb_conn = display.as_raw_xcb_connection(); |
83 | //! |
84 | //! // Use that pointer to create a new XCB connection. |
85 | //! let xcb_conn = unsafe { |
86 | //! XCBConnection::from_raw_xcb_connection(xcb_conn.cast(), false)? |
87 | //! }; |
88 | //! |
89 | //! // Register a handler for X11 errors. |
90 | //! tiny_xlib::register_error_handler(Box::new(|_, error| { |
91 | //! println!("X11 error: {:?}" , error); |
92 | //! false |
93 | //! })); |
94 | //! |
95 | //! // Do whatever you want with the XCB connection. |
96 | //! loop { |
97 | //! println!("Event: {:?}" , xcb_conn.wait_for_event()?); |
98 | //! } |
99 | //! # Ok(()) } |
100 | //! ``` |
101 | //! |
102 | //! # Optional Features |
103 | //! |
104 | //! - `tracing`, enabled by default, enables telemetry using the [`tracing`] crate. |
105 | //! - `dlopen` uses the [`libloading`] library to load the X11 libraries instead of linking to them |
106 | //! directly. |
107 | //! |
108 | //! [Xlib]: https://en.wikipedia.org/wiki/Xlib |
109 | //! [XCB]: https://xcb.freedesktop.org/ |
110 | //! [`x11-dl`]: https://crates.io/crates/x11-dl |
111 | //! [`x11rb`]: https://crates.io/crates/x11rb |
112 | //! [GLX]: https://en.wikipedia.org/wiki/GLX |
113 | //! [Vulkan]: https://www.khronos.org/vulkan/ |
114 | //! [`wgpu`]: https://crates.io/crates/wgpu |
115 | //! [`Display`]: struct.Display.html |
116 | //! [`AsRawXcbConnection`]: https://docs.rs/as_raw_xcb_connection/latest/as_raw_xcb_connection/trait.AsRawXcbConnection.html |
117 | //! [`tracing`]: https://crates.io/crates/tracing |
118 | //! [`libloading`]: https://crates.io/crates/libloading |
119 | |
120 | #![allow (unused_unsafe)] |
121 | #![cfg_attr (coverage, feature(no_coverage))] |
122 | |
123 | mod ffi; |
124 | |
125 | use std::cell::Cell; |
126 | use std::ffi::CStr; |
127 | use std::fmt; |
128 | use std::io; |
129 | use std::marker::PhantomData; |
130 | use std::mem::{self, ManuallyDrop}; |
131 | use std::os::raw::{c_int, c_void}; |
132 | use std::ptr::{self, NonNull}; |
133 | use std::sync::{Mutex, MutexGuard, Once, PoisonError}; |
134 | |
135 | macro_rules! lock { |
136 | ($e:expr) => {{ |
137 | // Make sure this isn't flagged with coverage. |
138 | #[cfg_attr(coverage, no_coverage)] |
139 | fn unwrapper<T>(guard: PoisonError<MutexGuard<'_, T>>) -> MutexGuard<'_, T> { |
140 | guard.into_inner() |
141 | } |
142 | |
143 | ($e).lock().unwrap_or_else(unwrapper) |
144 | }}; |
145 | } |
146 | |
147 | /// The global bindings to Xlib. |
148 | #[ctor::ctor ] |
149 | static XLIB: io::Result<ffi::Xlib> = { |
150 | #[cfg_attr (coverage, no_coverage)] |
151 | unsafe fn load_xlib_with_error_hook() -> io::Result<ffi::Xlib> { |
152 | // Here's a puzzle: how do you *safely* add an error hook to Xlib? Like signal handling, there |
153 | // is a single global error hook. Therefore, we need to make sure that we economize on the |
154 | // single slot that we have by offering a way to set it. However, unlike signal handling, there |
155 | // is no way to tell if we're replacing an existing error hook. If we replace another library's |
156 | // error hook, we could cause unsound behavior if it assumes that it is the only error hook. |
157 | // |
158 | // However, we don't want to call the default error hook, because it exits the program. So, in |
159 | // order to tell if the error hook is the default one, we need to compare it to the default |
160 | // error hook. However, we can't just compare the function pointers, because the default error |
161 | // hook is a private function that we can't access. |
162 | // |
163 | // In order to access it, before anything else runs, this function is called. It loads Xlib, |
164 | // sets the error hook to a dummy function, reads the resulting error hook into a static |
165 | // variable, and then resets the error hook to the default function. This allows us to read |
166 | // the default error hook and compare it to the one that we're setting. |
167 | #[cfg_attr (coverage, no_coverage)] |
168 | fn error(e: impl std::error::Error) -> io::Error { |
169 | io::Error::new(io::ErrorKind::Other, format!("failed to load Xlib: {}" , e)) |
170 | } |
171 | let xlib = ffi::Xlib::load().map_err(error)?; |
172 | |
173 | // Dummy function we use to set the error hook. |
174 | #[cfg_attr (coverage, no_coverage)] |
175 | unsafe extern "C" fn dummy( |
176 | _display: *mut ffi::Display, |
177 | _error: *mut ffi::XErrorEvent, |
178 | ) -> std::os::raw::c_int { |
179 | 0 |
180 | } |
181 | |
182 | // Set the error hook to the dummy function. |
183 | let default_hook = xlib.set_error_handler(Some(dummy)); |
184 | |
185 | // Read the error hook into a static variable. |
186 | // SAFETY: This should only run once at the start of the program, no need to worry about |
187 | // multithreading. |
188 | DEFAULT_ERROR_HOOK.set(default_hook); |
189 | |
190 | // Set the error hook back to the default function. |
191 | xlib.set_error_handler(default_hook); |
192 | |
193 | Ok(xlib) |
194 | } |
195 | |
196 | unsafe { load_xlib_with_error_hook() } |
197 | }; |
198 | |
199 | #[inline ] |
200 | fn get_xlib(sym: &io::Result<ffi::Xlib>) -> io::Result<&ffi::Xlib> { |
201 | // Eat coverage on the error branch. |
202 | #[cfg_attr (coverage, no_coverage)] |
203 | fn error(e: &io::Error) -> io::Error { |
204 | io::Error::new(e.kind(), error:e.to_string()) |
205 | } |
206 | |
207 | sym.as_ref().map_err(op:error) |
208 | } |
209 | |
210 | /// The default error hook to compare against. |
211 | static DEFAULT_ERROR_HOOK: ErrorHookSlot = ErrorHookSlot::new(); |
212 | |
213 | /// An error handling hook. |
214 | type ErrorHook = Box<dyn FnMut(&Display, &ErrorEvent) -> bool + Send + Sync + 'static>; |
215 | |
216 | /// List of error hooks to invoke. |
217 | static ERROR_HANDLERS: Mutex<HandlerList> = Mutex::new(HandlerList::new()); |
218 | |
219 | /// Global error handler for X11. |
220 | unsafe extern "C" fn error_handler( |
221 | display: *mut ffi::Display, |
222 | error: *mut ffi::XErrorEvent, |
223 | ) -> c_int { |
224 | // Abort the program if the error hook panics. |
225 | struct AbortOnPanic; |
226 | impl Drop for AbortOnPanic { |
227 | #[cfg_attr (coverage, no_coverage)] |
228 | #[cold ] |
229 | #[inline (never)] |
230 | fn drop(&mut self) { |
231 | std::process::abort(); |
232 | } |
233 | } |
234 | |
235 | let bomb = AbortOnPanic; |
236 | |
237 | // Run the previous error hook, if any. |
238 | let mut handlers = lock!(ERROR_HANDLERS); |
239 | handlers.run_prev(display, error); |
240 | |
241 | // Read out the variables. |
242 | // SAFETY: Guaranteed to be a valid display setup. |
243 | let display_ptr = unsafe { Display::from_ptr(display.cast()) }; |
244 | let event = ErrorEvent(ptr::read(error)); |
245 | |
246 | #[cfg (feature = "tracing" )] |
247 | tracing::error!( |
248 | display = ?&*display_ptr, |
249 | error = ?event, |
250 | "got Xlib error" , |
251 | ); |
252 | |
253 | // Invoke the error hooks. |
254 | handlers.iter_mut().any(|(_i, handler)| { |
255 | #[cfg (feature = "tracing" )] |
256 | tracing::trace!(key = _i, "invoking error handler" ); |
257 | |
258 | let stop_going = (handler)(&display_ptr, &event); |
259 | |
260 | #[cfg (feature = "tracing" )] |
261 | { |
262 | if stop_going { |
263 | tracing::trace!("error handler returned true, stopping" ); |
264 | } else { |
265 | tracing::trace!("error handler returned false, continuing" ); |
266 | } |
267 | } |
268 | |
269 | stop_going |
270 | }); |
271 | |
272 | // Defuse the bomb. |
273 | mem::forget(bomb); |
274 | |
275 | // Apparently the return value here has no effect. |
276 | 0 |
277 | } |
278 | |
279 | /// Register the error handler. |
280 | fn setup_error_handler(xlib: &ffi::Xlib) { |
281 | static REGISTERED: Once = Once::new(); |
282 | REGISTERED.call_once(move || { |
283 | // Make sure threads are initialized here. |
284 | unsafe { |
285 | xlib.init_threads(); |
286 | } |
287 | |
288 | // Get the previous error handler. |
289 | let prev: Option …> = unsafe { xlib.set_error_handler(Some(error_handler)) }; |
290 | |
291 | // If it isn't the default error handler, then we need to store it. |
292 | // SAFETY: DEFAULT_ERROR_HOOK is not set after the program starts, so this is safe. |
293 | let default_hook: Option = unsafe { DEFAULT_ERROR_HOOK.get() }; |
294 | if prev != default_hook.flatten() && prev != Some(error_handler) { |
295 | lock!(ERROR_HANDLERS).prev = prev; |
296 | } |
297 | }); |
298 | } |
299 | |
300 | /// A key to the error handler list that can be used to remove handlers. |
301 | #[derive (Debug, Copy, Clone)] |
302 | pub struct HandlerKey(usize); |
303 | |
304 | /// The error event type. |
305 | #[derive (Clone)] |
306 | pub struct ErrorEvent(ffi::XErrorEvent); |
307 | |
308 | // SAFETY: With XInitThreads, ErrorEvent is both Send and Sync. |
309 | unsafe impl Send for ErrorEvent {} |
310 | unsafe impl Sync for ErrorEvent {} |
311 | |
312 | impl ErrorEvent { |
313 | /// Get the serial number of the failed request. |
314 | #[allow (clippy::unnecessary_cast)] |
315 | pub fn serial(&self) -> u64 { |
316 | self.0.serial as u64 |
317 | } |
318 | |
319 | /// Get the error code. |
320 | pub fn error_code(&self) -> u8 { |
321 | self.0.error_code |
322 | } |
323 | |
324 | /// Get the request code. |
325 | pub fn request_code(&self) -> u8 { |
326 | self.0.request_code |
327 | } |
328 | |
329 | /// Get the minor opcode of the failed request. |
330 | pub fn minor_code(&self) -> u8 { |
331 | self.0.minor_code |
332 | } |
333 | |
334 | /// Get the resource ID of the failed request. |
335 | pub fn resource_id(&self) -> usize { |
336 | self.0.resourceid as usize |
337 | } |
338 | } |
339 | |
340 | impl fmt::Debug for ErrorEvent { |
341 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
342 | f&mut DebugStruct<'_, '_>.debug_struct("ErrorEvent" ) |
343 | .field("serial" , &self.serial()) |
344 | .field("error_code" , &self.error_code()) |
345 | .field("request_code" , &self.request_code()) |
346 | .field("minor_code" , &self.minor_code()) |
347 | .field(name:"resource_id" , &self.resource_id()) |
348 | .finish_non_exhaustive() |
349 | } |
350 | } |
351 | |
352 | /// The display pointer. |
353 | pub struct Display { |
354 | /// The display pointer. |
355 | ptr: NonNull<ffi::Display>, |
356 | |
357 | /// This owns the memory that the display pointer points to. |
358 | _marker: PhantomData<Box<ffi::Display>>, |
359 | } |
360 | |
361 | // SAFETY: With XInitThreads, Display is both Send and Sync. |
362 | unsafe impl Send for Display {} |
363 | unsafe impl Sync for Display {} |
364 | |
365 | impl fmt::Debug for Display { |
366 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
367 | f.debug_tuple(name:"Display" ).field(&self.ptr.as_ptr()).finish() |
368 | } |
369 | } |
370 | |
371 | impl Display { |
372 | /// Open a new display. |
373 | pub fn new(name: Option<&CStr>) -> io::Result<Self> { |
374 | let xlib = get_xlib(&XLIB)?; |
375 | |
376 | // Make sure the error handler is registered. |
377 | setup_error_handler(xlib); |
378 | |
379 | let name = name.map_or(std::ptr::null(), |n| n.as_ptr()); |
380 | let pointer = unsafe { xlib.open_display(name) }; |
381 | |
382 | NonNull::new(pointer) |
383 | .map(|ptr| Self { |
384 | ptr, |
385 | _marker: PhantomData, |
386 | }) |
387 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to open display" )) |
388 | } |
389 | |
390 | /// Create a new `Display` from a pointer. |
391 | /// |
392 | /// # Safety |
393 | /// |
394 | /// The pointer must be a valid pointer to an Xlib display. In addition, it should only be dropped if the |
395 | /// user logically owns the display. |
396 | pub unsafe fn from_ptr(ptr: *mut c_void) -> ManuallyDrop<Self> { |
397 | ManuallyDrop::new(Self { |
398 | // SAFETY: "valid" implies non-null |
399 | ptr: NonNull::new_unchecked(ptr.cast()), |
400 | _marker: PhantomData, |
401 | }) |
402 | } |
403 | |
404 | /// Get the pointer to the display. |
405 | pub fn as_ptr(&self) -> *mut c_void { |
406 | self.ptr.as_ptr().cast() |
407 | } |
408 | } |
409 | |
410 | unsafe impl as_raw_xcb_connection::AsRawXcbConnection for Display { |
411 | fn as_raw_xcb_connection(&self) -> *mut as_raw_xcb_connection::xcb_connection_t { |
412 | let xlib: &Xlib = get_xlib(&XLIB).expect(msg:"failed to load Xlib" ); |
413 | unsafe { xlib.get_xcb_connection(self.ptr.as_ptr()) } |
414 | } |
415 | } |
416 | |
417 | impl Drop for Display { |
418 | fn drop(&mut self) { |
419 | // SAFETY: We own the display pointer, so we can drop it. |
420 | if let Ok(xlib: &Xlib) = get_xlib(&XLIB) { |
421 | unsafe { |
422 | xlib.close_display(self.ptr.as_ptr()); |
423 | } |
424 | } |
425 | } |
426 | } |
427 | |
428 | /// Insert an error handler into the list. |
429 | pub fn register_error_handler(handler: ErrorHook) -> io::Result<HandlerKey> { |
430 | // Make sure the error handler is registered. |
431 | setup_error_handler(xlib:get_xlib(&XLIB)?); |
432 | |
433 | // Insert the handler into the list. |
434 | let mut handlers: MutexGuard<'_, HandlerList> = lock!(ERROR_HANDLERS); |
435 | let key: usize = handlers.insert(handler); |
436 | Ok(HandlerKey(key)) |
437 | } |
438 | |
439 | /// Remove an error handler from the list. |
440 | pub fn unregister_error_handler(key: HandlerKey) { |
441 | // Remove the handler from the list. |
442 | let mut handlers: MutexGuard<'_, HandlerList> = lock!(ERROR_HANDLERS); |
443 | handlers.remove(index:key.0); |
444 | } |
445 | |
446 | /// The list of error handlers. |
447 | struct HandlerList { |
448 | /// The inner list of slots. |
449 | slots: Vec<Slot>, |
450 | |
451 | /// The number of filled slots. |
452 | filled: usize, |
453 | |
454 | /// The first unfilled slot. |
455 | unfilled: usize, |
456 | |
457 | /// The last error handler hook. |
458 | prev: ffi::XErrorHook, |
459 | } |
460 | |
461 | /// A slot in the error handler list. |
462 | enum Slot { |
463 | /// A slot that is filled. |
464 | Filled(ErrorHook), |
465 | |
466 | /// A slot that is unfilled. |
467 | /// |
468 | /// This value points to the next unfilled slot. |
469 | Unfilled(usize), |
470 | } |
471 | |
472 | impl HandlerList { |
473 | /// Create a new handler list. |
474 | #[cfg_attr (coverage, no_coverage)] |
475 | const fn new() -> Self { |
476 | Self { |
477 | slots: vec![], |
478 | filled: 0, |
479 | unfilled: 0, |
480 | prev: None, |
481 | } |
482 | } |
483 | |
484 | /// Run the previous error handler. |
485 | unsafe fn run_prev(&mut self, display: *mut ffi::Display, event: *mut ffi::XErrorEvent) { |
486 | if let Some(prev) = self.prev { |
487 | prev(display, event); |
488 | } |
489 | } |
490 | |
491 | /// Push a new error handler. |
492 | /// |
493 | /// Returns the index of the handler. |
494 | fn insert(&mut self, handler: ErrorHook) -> usize { |
495 | // Eat the coverage for the unreachable branch. |
496 | #[cfg_attr (coverage, no_coverage)] |
497 | #[inline (always)] |
498 | fn unwrapper(slot: &Slot) -> usize { |
499 | match slot { |
500 | Slot::Filled(_) => unreachable!(), |
501 | Slot::Unfilled(next) => *next, |
502 | } |
503 | } |
504 | |
505 | let index = self.filled; |
506 | |
507 | if self.unfilled == self.slots.len() { |
508 | self.slots.push(Slot::Filled(handler)); |
509 | self.unfilled += 1; |
510 | } else { |
511 | let unfilled = self.unfilled; |
512 | self.unfilled = unwrapper(&self.slots[unfilled]); |
513 | self.slots[unfilled] = Slot::Filled(handler); |
514 | } |
515 | |
516 | self.filled += 1; |
517 | |
518 | index |
519 | } |
520 | |
521 | /// Remove an error handler. |
522 | fn remove(&mut self, index: usize) { |
523 | let slot = &mut self.slots[index]; |
524 | |
525 | if let Slot::Filled(_) = slot { |
526 | *slot = Slot::Unfilled(self.unfilled); |
527 | self.unfilled = index; |
528 | self.filled -= 1; |
529 | } |
530 | } |
531 | |
532 | /// Iterate over the error handlers. |
533 | fn iter_mut(&mut self) -> impl Iterator<Item = (usize, &mut ErrorHook)> { |
534 | self.slots |
535 | .iter_mut() |
536 | .enumerate() |
537 | .filter_map(|(i, slot)| match slot { |
538 | Slot::Filled(handler) => Some((i, handler)), |
539 | _ => None, |
540 | }) |
541 | } |
542 | } |
543 | |
544 | /// Static unsafe error hook slot. |
545 | struct ErrorHookSlot(Cell<Option<ffi::XErrorHook>>); |
546 | |
547 | unsafe impl Sync for ErrorHookSlot {} |
548 | |
549 | impl ErrorHookSlot { |
550 | #[cfg_attr (coverage, no_coverage)] |
551 | const fn new() -> Self { |
552 | Self(Cell::new(None)) |
553 | } |
554 | |
555 | unsafe fn get(&self) -> Option<ffi::XErrorHook> { |
556 | self.0.get() |
557 | } |
558 | |
559 | #[cfg_attr (coverage, no_coverage)] |
560 | unsafe fn set(&self, hook: ffi::XErrorHook) { |
561 | self.0.set(val:Some(hook)); |
562 | } |
563 | } |
564 | |