diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 0000000..4022c25 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,18 @@ +task: + only_if: $CIRRUS_BRANCH == 'main' || $CIRRUS_PR != '' + matrix: + - name: FreeBSD 12.1 + freebsd_instance: + image_family: freebsd-12-1-snap + - name: FreeBSD 13.0 + freebsd_instance: + image_family: freebsd-13-0-snap + # Install Rust + setup_script: + - fetch https://sh.rustup.rs -o rustup.sh + - sh rustup.sh -y --profile=minimal --default-toolchain stable + test_script: + - . $HOME/.cargo/env + - mkdir -p $HOME/sockets + - export XDG_RUNTIME_DIR="$HOME/sockets" + - cargo test --features=log diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d8d16f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +env: + RUST_BACKTRACE: 1 + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Cdebuginfo=0 --deny=warnings" + RUSTDOCFLAGS: "--deny=warnings" + +jobs: + fmt: + name: Check Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: nightly + components: rustfmt + - name: Check Formatting + run: cargo +nightly fmt --all -- --check + + tests: + name: Tests + runs-on: ubuntu-latest + strategy: + matrix: + rust_version: ["1.65", stable, nightly] + + steps: + - uses: actions/checkout@v3 + + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ matrix.rust_version }} + + - name: Check documentation + run: cargo doc --features=log --no-deps --document-private-items + + - name: Run tests + run: cargo test --verbose --features=log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..989b65a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "calloop-wayland-source" +description = "A wayland-rs client event source for callloop" +version = "0.1.0" +edition = "2021" +authors = ["Kirill Chibisov "] +license = "MIT" +readme = "README.md" +keywords = ["wayland", "windowing"] +rust-version = "1.65.0" + +[dependencies] +wayland-client = "0.30.2" +wayland-backend = "0.1.0" +calloop = "0.10.0" +log = { version = "0.4.19", optional = true } +rustix = { version = "0.38.4", default-features = false, features = ["std"] } diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..bac2738 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2023 Kirill Chibisov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dae0410 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# calloop-wayland-source + +Use [`EventQueue`] from the [`wayland-client`](https://crates.io/crates/wayland-client) +with the [`calloop`](https://crates.io/crates/calloop). + +[`EventQueue`]: https://docs.rs/wayland-client/0.30/wayland_client/struct.EventQueue.html diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..8057fee --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,16 @@ +format_code_in_doc_comments = true +match_block_trailing_comma = true +condense_wildcard_suffixes = true +use_field_init_shorthand = true +normalize_doc_attributes = true +overflow_delimited_expr = true +imports_granularity = "Module" +use_small_heuristics = "Max" +normalize_comments = true +reorder_impl_items = true +use_try_shorthand = true +newline_style = "Unix" +format_strings = true +wrap_comments = true +comment_width = 80 +edition = "2021" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9836998 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT + +//! Utilities for using an [`EventQueue`] from wayland-client with an event loop +//! that performs polling with [`calloop`](https://crates.io/crates/calloop). +//! +//! # Example +//! +//! ```no_run,rust +//! use wayland_client::{Connection, QueueHandle}; +//! use calloop::EventLoop; +//! use calloop_wayland_source::WaylandSource; +//! +//! // Create a Wayland connection and a queue. +//! let connection = Connection::connect_to_env().unwrap(); +//! let event_queue = connection.new_event_queue(); +//! let queue_handle = event_queue.handle(); +//! +//! // Create the calloop event loop to drive everytihng. +//! let mut event_loop: EventLoop<()> = EventLoop::try_new().unwrap(); +//! let loop_handle = event_loop.handle(); +//! +//! // Insert the wayland source into the calloop's event loop. +//! WaylandSource::new(event_queue).unwrap().insert(loop_handle).unwrap(); +//! +//! // This will start dispatching the event loop and processing pending wayland requests. +//! while let Ok(_) = event_loop.dispatch(None, &mut ()) { +//! // Your logic here. +//! } +//! ``` + +use std::io; +use std::os::unix::io::{AsRawFd, RawFd}; + +use calloop::generic::Generic; +use calloop::{ + EventSource, InsertError, Interest, LoopHandle, Mode, Poll, PostAction, Readiness, + RegistrationToken, Token, TokenFactory, +}; +use rustix::io::Errno; +use wayland_backend::client::{ReadEventsGuard, WaylandError}; +use wayland_client::{DispatchError, EventQueue}; + +#[cfg(feature = "log")] +use log::error as log_error; +#[cfg(not(feature = "log"))] +use std::eprintln as log_error; + +/// An adapter to insert an [`EventQueue`] into a calloop +/// [`EventLoop`](calloop::EventLoop). +/// +/// This type implements [`EventSource`] which generates an event whenever +/// events on the event queue need to be dispatched. The event queue available +/// in the callback calloop registers may be used to dispatch pending +/// events using [`EventQueue::dispatch_pending`]. +/// +/// [`WaylandSource::insert`] can be used to insert this source into an event +/// loop and automatically dispatch pending events on the event queue. +#[derive(Debug)] +pub struct WaylandSource { + queue: EventQueue, + fd: Generic, + read_guard: Option, +} + +impl WaylandSource { + /// Wrap an [`EventQueue`] as a [`WaylandSource`]. + pub fn new(queue: EventQueue) -> Result, WaylandError> { + let guard = queue.prepare_read()?; + let fd = Generic::new(guard.connection_fd().as_raw_fd(), Interest::READ, Mode::Level); + drop(guard); + + Ok(WaylandSource { queue, fd, read_guard: None }) + } + + /// Access the underlying event queue + /// + /// Note that you should be careful when interacting with it if you invoke + /// methods that interact with the wayland socket (such as `dispatch()` + /// or `prepare_read()`). These may interfere with the proper waking up + /// of this event source in the event loop. + pub fn queue(&mut self) -> &mut EventQueue { + &mut self.queue + } + + /// Insert this source into the given event loop. + /// + /// This adapter will pass the event loop's shared data as the `D` type for + /// the event loop. + pub fn insert(self, handle: LoopHandle) -> Result> + where + D: 'static, + { + handle.insert_source(self, |_, queue, data| queue.dispatch_pending(data)) + } +} + +impl EventSource for WaylandSource { + type Error = calloop::Error; + type Event = (); + /// The underlying event queue. + /// + /// You should call [`EventQueue::dispatch_pending`] inside your callback + /// using this queue. + type Metadata = EventQueue; + type Ret = Result; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: F, + ) -> Result + where + F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + let queue = &mut self.queue; + let read_guard = &mut self.read_guard; + + let action = self.fd.process_events(readiness, token, |_, _| { + // 1. read events from the socket if any are available + if let Some(guard) = read_guard.take() { + // might be None if some other thread read events before us, concurrently + if let Err(WaylandError::Io(err)) = guard.read() { + if err.kind() != io::ErrorKind::WouldBlock { + return Err(err); + } + } + } + + // 2. dispatch any pending events in the queue + // This is done to ensure we are not waiting for messages that are already in + // the buffer. + Self::loop_callback_pending(queue, &mut callback)?; + *read_guard = Some(Self::prepare_read(queue)?); + + // 3. Once dispatching is finished, flush the responses to the compositor + if let Err(WaylandError::Io(e)) = queue.flush() { + if e.kind() != io::ErrorKind::WouldBlock { + // in case of error, forward it and fast-exit + return Err(e); + } + // WouldBlock error means the compositor could not process all + // our messages quickly. Either it is slowed + // down or we are a spammer. Should not really + // happen, if it does we do nothing and will flush again later + } + + Ok(PostAction::Continue) + })?; + + Ok(action) + } + + fn register( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.fd.register(poll, token_factory) + } + + fn reregister( + &mut self, + poll: &mut Poll, + token_factory: &mut TokenFactory, + ) -> calloop::Result<()> { + self.fd.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.fd.unregister(poll) + } + + fn pre_run(&mut self, mut callback: F) -> calloop::Result<()> + where + F: FnMut((), &mut Self::Metadata) -> Self::Ret, + { + debug_assert!(self.read_guard.is_none()); + + // flush the display before starting to poll + if let Err(WaylandError::Io(err)) = self.queue.flush() { + if err.kind() != io::ErrorKind::WouldBlock { + // in case of error, don't prepare a read, if the error is persistent, it'll + // trigger in other wayland methods anyway + log_error!("Error trying to flush the wayland display: {}", err); + return Err(err.into()); + } + } + + // ensure we are not waiting for messages that are already in the buffer. + Self::loop_callback_pending(&mut self.queue, &mut callback)?; + self.read_guard = Some(Self::prepare_read(&mut self.queue)?); + + Ok(()) + } + + fn post_run(&mut self, _: F) -> calloop::Result<()> + where + F: FnMut((), &mut Self::Metadata) -> Self::Ret, + { + // Drop implementation of ReadEventsGuard will do cleanup + self.read_guard.take(); + Ok(()) + } +} + +impl WaylandSource { + /// Loop over the callback until all pending messages have been dispatched. + fn loop_callback_pending(queue: &mut EventQueue, callback: &mut F) -> io::Result<()> + where + F: FnMut((), &mut EventQueue) -> Result, + { + // Loop on the callback until no pending events are left. + loop { + match callback((), queue) { + // No more pending events. + Ok(0) => break Ok(()), + Ok(_) => continue, + Err(DispatchError::Backend(WaylandError::Io(err))) => { + return Err(err); + }, + Err(DispatchError::Backend(WaylandError::Protocol(err))) => { + log_error!("Protocol error received on display: {}", err); + + break Err(Errno::PROTO.into()); + }, + Err(DispatchError::BadMessage { interface, sender_id, opcode }) => { + log_error!( + "Bad message on interface \"{}\": (sender_id: {}, opcode: {})", + interface, + sender_id, + opcode, + ); + + break Err(Errno::PROTO.into()); + }, + } + } + } + + fn prepare_read(queue: &mut EventQueue) -> io::Result { + queue.prepare_read().map_err(|err| match err { + WaylandError::Io(err) => err, + + WaylandError::Protocol(err) => { + log_error!("Protocol error received on display: {}", err); + + Errno::PROTO.into() + }, + }) + } +}