tiny_xlib/lib.rs
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(coverage_attribute))]
122
123mod ffi;
124
125use std::cell::Cell;
126use std::ffi::CStr;
127use std::fmt;
128use std::io;
129use std::marker::PhantomData;
130use std::mem::{self, ManuallyDrop};
131use std::os::raw::{c_int, c_void};
132use std::ptr::{self, NonNull};
133use std::sync::{Mutex, MutexGuard, Once, PoisonError};
134
135macro_rules! lock {
136 ($e:expr) => {{
137 // Make sure this isn't flagged with coverage.
138 #[cfg_attr(coverage, coverage(off))]
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
147ctor::declarative::ctor! {
148 #[ctor(unsafe)]
149 static XLIB: io::Result<ffi::Xlib> = {
150 #[cfg_attr(coverage, coverage(off))]
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, coverage(off))]
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, coverage(off))]
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
200#[inline]
201fn get_xlib(sym: &io::Result<ffi::Xlib>) -> io::Result<&ffi::Xlib> {
202 // Eat coverage on the error branch.
203 #[cfg_attr(coverage, coverage(off))]
204 fn error(e: &io::Error) -> io::Error {
205 io::Error::new(e.kind(), e.to_string())
206 }
207
208 sym.as_ref().map_err(error)
209}
210
211/// The default error hook to compare against.
212static DEFAULT_ERROR_HOOK: ErrorHookSlot = ErrorHookSlot::new();
213
214/// An error handling hook.
215type ErrorHook = Box<dyn FnMut(&Display, &ErrorEvent) -> bool + Send + Sync + 'static>;
216
217/// List of error hooks to invoke.
218static ERROR_HANDLERS: Mutex<HandlerList> = Mutex::new(HandlerList::new());
219
220/// Global error handler for X11.
221unsafe extern "C" fn error_handler(
222 display: *mut ffi::Display,
223 error: *mut ffi::XErrorEvent,
224) -> c_int {
225 // Abort the program if the error hook panics.
226 struct AbortOnPanic;
227 impl Drop for AbortOnPanic {
228 #[cfg_attr(coverage, coverage(off))]
229 #[cold]
230 #[inline(never)]
231 fn drop(&mut self) {
232 std::process::abort();
233 }
234 }
235
236 let bomb = AbortOnPanic;
237
238 let mut handlers = lock!(ERROR_HANDLERS);
239
240 let prev = handlers.prev;
241 if let Some(prev) = prev {
242 // Drop the mutex lock to make sure no deadlocks occur. Otherwise, if the prev handlers
243 // tries to add its own handler, we'll deadlock.
244 drop(handlers);
245
246 unsafe {
247 // Run the previous error hook, if any.
248 prev(display, error);
249 }
250
251 // Restore the mutex lock.
252 handlers = lock!(ERROR_HANDLERS);
253 }
254
255 // Read out the variables.
256 // SAFETY: Guaranteed to be a valid display setup.
257 let display_ptr = unsafe { Display::from_ptr(display.cast()) };
258 let event = ErrorEvent(ptr::read(error));
259
260 #[cfg(feature = "tracing")]
261 tracing::error!(
262 display = ?&*display_ptr,
263 error = ?event,
264 "got Xlib error",
265 );
266
267 // Invoke the error hooks.
268 handlers.iter_mut().any(|(_i, handler)| {
269 #[cfg(feature = "tracing")]
270 tracing::trace!(key = _i, "invoking error handler");
271
272 let stop_going = (handler)(&display_ptr, &event);
273
274 #[cfg(feature = "tracing")]
275 {
276 if stop_going {
277 tracing::trace!("error handler returned true, stopping");
278 } else {
279 tracing::trace!("error handler returned false, continuing");
280 }
281 }
282
283 stop_going
284 });
285
286 // Defuse the bomb.
287 mem::forget(bomb);
288
289 // Apparently the return value here has no effect.
290 0
291}
292
293/// Register the error handler.
294fn setup_error_handler(xlib: &ffi::Xlib) {
295 static REGISTERED: Once = Once::new();
296 REGISTERED.call_once(move || {
297 // Make sure threads are initialized here.
298 unsafe {
299 xlib.init_threads();
300 }
301
302 // Get the previous error handler.
303 let prev = unsafe { xlib.set_error_handler(Some(error_handler)) };
304
305 // If it isn't the default error handler, then we need to store it.
306 // SAFETY: DEFAULT_ERROR_HOOK is not set after the program starts, so this is safe.
307 let default_hook = unsafe { DEFAULT_ERROR_HOOK.get() };
308 // TODO(MSRV 1.85): Use core::ptr::fn_addr_eq
309 #[allow(unpredictable_function_pointer_comparisons)]
310 if prev != default_hook.flatten() && prev != Some(error_handler) {
311 lock!(ERROR_HANDLERS).prev = prev;
312 }
313 });
314}
315
316/// A key to the error handler list that can be used to remove handlers.
317#[derive(Debug, Copy, Clone)]
318pub struct HandlerKey(usize);
319
320/// The error event type.
321#[derive(Clone)]
322pub struct ErrorEvent(ffi::XErrorEvent);
323
324// SAFETY: With XInitThreads, ErrorEvent is both Send and Sync.
325unsafe impl Send for ErrorEvent {}
326unsafe impl Sync for ErrorEvent {}
327
328impl ErrorEvent {
329 /// Get the serial number of the failed request.
330 #[allow(clippy::unnecessary_cast)]
331 pub fn serial(&self) -> u64 {
332 self.0.serial as u64
333 }
334
335 /// Get the error code.
336 pub fn error_code(&self) -> u8 {
337 self.0.error_code
338 }
339
340 /// Get the request code.
341 pub fn request_code(&self) -> u8 {
342 self.0.request_code
343 }
344
345 /// Get the minor opcode of the failed request.
346 pub fn minor_code(&self) -> u8 {
347 self.0.minor_code
348 }
349
350 /// Get the resource ID of the failed request.
351 pub fn resource_id(&self) -> usize {
352 self.0.resourceid as usize
353 }
354}
355
356impl fmt::Debug for ErrorEvent {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 f.debug_struct("ErrorEvent")
359 .field("serial", &self.serial())
360 .field("error_code", &self.error_code())
361 .field("request_code", &self.request_code())
362 .field("minor_code", &self.minor_code())
363 .field("resource_id", &self.resource_id())
364 .finish_non_exhaustive()
365 }
366}
367
368/// The display pointer.
369pub struct Display {
370 /// The display pointer.
371 ptr: NonNull<ffi::Display>,
372
373 /// This owns the memory that the display pointer points to.
374 _marker: PhantomData<Box<ffi::Display>>,
375}
376
377// SAFETY: With XInitThreads, Display is both Send and Sync.
378unsafe impl Send for Display {}
379unsafe impl Sync for Display {}
380
381impl fmt::Debug for Display {
382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 f.debug_tuple("Display").field(&self.ptr.as_ptr()).finish()
384 }
385}
386
387impl Display {
388 /// Open a new display.
389 pub fn new(name: Option<&CStr>) -> io::Result<Self> {
390 let xlib = get_xlib(&XLIB)?;
391
392 // Make sure the error handler is registered.
393 setup_error_handler(xlib);
394
395 let name = name.map_or(std::ptr::null(), |n| n.as_ptr());
396 let pointer = unsafe { xlib.open_display(name) };
397
398 NonNull::new(pointer)
399 .map(|ptr| Self {
400 ptr,
401 _marker: PhantomData,
402 })
403 .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to open display"))
404 }
405
406 /// Create a new `Display` from a pointer.
407 ///
408 /// # Safety
409 ///
410 /// The pointer must be a valid pointer to an Xlib display. In addition, it should only be dropped if the
411 /// user logically owns the display.
412 pub unsafe fn from_ptr(ptr: *mut c_void) -> ManuallyDrop<Self> {
413 ManuallyDrop::new(Self {
414 // SAFETY: "valid" implies non-null
415 ptr: NonNull::new_unchecked(ptr.cast()),
416 _marker: PhantomData,
417 })
418 }
419
420 /// Get the pointer to the display.
421 pub fn as_ptr(&self) -> *mut c_void {
422 self.ptr.as_ptr().cast()
423 }
424
425 /// Get the default screen index for this display.
426 pub fn screen_index(&self) -> usize {
427 let xlib = get_xlib(&XLIB).expect("failed to load Xlib");
428
429 // SAFETY: Valid display pointer.
430 let index = unsafe { xlib.default_screen(self.ptr.as_ptr()) };
431
432 // Cast down to usize.
433 index.try_into().unwrap_or_else(|_| {
434 #[cfg(feature = "tracing")]
435 tracing::error!(
436 "XDefaultScreen returned a value out of usize range (how?!), returning zero"
437 );
438 0
439 })
440 }
441}
442
443unsafe impl as_raw_xcb_connection::AsRawXcbConnection for Display {
444 fn as_raw_xcb_connection(&self) -> *mut as_raw_xcb_connection::xcb_connection_t {
445 let xlib = get_xlib(&XLIB).expect("failed to load Xlib");
446 unsafe { xlib.get_xcb_connection(self.ptr.as_ptr()) }
447 }
448}
449
450impl Drop for Display {
451 fn drop(&mut self) {
452 // SAFETY: We own the display pointer, so we can drop it.
453 if let Ok(xlib) = get_xlib(&XLIB) {
454 unsafe {
455 xlib.close_display(self.ptr.as_ptr());
456 }
457 }
458 }
459}
460
461/// Insert an error handler into the list.
462pub fn register_error_handler(handler: ErrorHook) -> io::Result<HandlerKey> {
463 // Make sure the error handler is registered.
464 setup_error_handler(get_xlib(&XLIB)?);
465
466 // Insert the handler into the list.
467 let mut handlers = lock!(ERROR_HANDLERS);
468 let key = handlers.insert(handler);
469 Ok(HandlerKey(key))
470}
471
472/// Remove an error handler from the list.
473pub fn unregister_error_handler(key: HandlerKey) {
474 // Remove the handler from the list.
475 let mut handlers = lock!(ERROR_HANDLERS);
476 handlers.remove(key.0);
477}
478
479/// The list of error handlers.
480struct HandlerList {
481 /// The inner list of slots.
482 slots: Vec<Slot>,
483
484 /// The number of filled slots.
485 filled: usize,
486
487 /// The first unfilled slot.
488 unfilled: usize,
489
490 /// The last error handler hook.
491 prev: ffi::XErrorHook,
492}
493
494/// A slot in the error handler list.
495enum Slot {
496 /// A slot that is filled.
497 Filled(ErrorHook),
498
499 /// A slot that is unfilled.
500 ///
501 /// This value points to the next unfilled slot.
502 Unfilled(usize),
503}
504
505impl HandlerList {
506 /// Create a new handler list.
507 #[cfg_attr(coverage, coverage(off))]
508 const fn new() -> Self {
509 Self {
510 slots: vec![],
511 filled: 0,
512 unfilled: 0,
513 prev: None,
514 }
515 }
516
517 /// Push a new error handler.
518 ///
519 /// Returns the index of the handler.
520 fn insert(&mut self, handler: ErrorHook) -> usize {
521 // Eat the coverage for the unreachable branch.
522 #[cfg_attr(coverage, coverage(off))]
523 #[inline(always)]
524 fn unwrapper(slot: &Slot) -> usize {
525 match slot {
526 Slot::Filled(_) => unreachable!(),
527 Slot::Unfilled(next) => *next,
528 }
529 }
530
531 let index = self.filled;
532
533 if self.unfilled == self.slots.len() {
534 self.slots.push(Slot::Filled(handler));
535 self.unfilled += 1;
536 } else {
537 let unfilled = self.unfilled;
538 self.unfilled = unwrapper(&self.slots[unfilled]);
539 self.slots[unfilled] = Slot::Filled(handler);
540 }
541
542 self.filled += 1;
543
544 index
545 }
546
547 /// Remove an error handler.
548 fn remove(&mut self, index: usize) {
549 let slot = &mut self.slots[index];
550
551 if let Slot::Filled(_) = slot {
552 *slot = Slot::Unfilled(self.unfilled);
553 self.unfilled = index;
554 self.filled -= 1;
555 }
556 }
557
558 /// Iterate over the error handlers.
559 fn iter_mut(&mut self) -> impl Iterator<Item = (usize, &mut ErrorHook)> {
560 self.slots
561 .iter_mut()
562 .enumerate()
563 .filter_map(|(i, slot)| match slot {
564 Slot::Filled(handler) => Some((i, handler)),
565 _ => None,
566 })
567 }
568}
569
570/// Static unsafe error hook slot.
571struct ErrorHookSlot(Cell<Option<ffi::XErrorHook>>);
572
573unsafe impl Sync for ErrorHookSlot {}
574
575impl ErrorHookSlot {
576 #[cfg_attr(coverage, coverage(off))]
577 const fn new() -> Self {
578 Self(Cell::new(None))
579 }
580
581 unsafe fn get(&self) -> Option<ffi::XErrorHook> {
582 self.0.get()
583 }
584
585 #[cfg_attr(coverage, coverage(off))]
586 unsafe fn set(&self, hook: ffi::XErrorHook) {
587 self.0.set(Some(hook));
588 }
589}