From d51f8329a3fc0e697579e1c5f60d1da7b0e2bf3a Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Thu, 20 Nov 2025 19:56:38 +0200 Subject: [PATCH 1/8] Add async-io support --- Cargo.toml | 6 ++++- src/lib.rs | 7 +++-- src/raw_stream.rs | 43 +++++++++++++++++++++++------- src/sync_stream.rs | 44 ++++++++++++++++++++++++------- src/uinput.rs | 66 +++++++++++++++++++++++++++++++++++++--------- 5 files changed, 133 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 43b36f1..b9ecbe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "evdev" -version = "0.13.2" +version = "0.13.3" authors = ["Corey Richardson "] description = "evdev interface for Linux" license = "Apache-2.0 OR MIT" @@ -11,6 +11,7 @@ rust-version = "1.64" [features] serde = ["dep:serde"] +async-io = ["dep:async-io", "dep:async-fs", "dep:futures-lite"] tokio = ["dep:tokio"] stream-trait = ["tokio", "futures-core"] device-test = [] @@ -24,6 +25,9 @@ nix = { version = "0.29", features = ["ioctl", "fs", "event"] } serde = { version = "1.0", features = ["derive"], optional = true } tokio = { version = "1.17", features = ["fs","time", "net"], optional = true } futures-core = { version = "0.3", optional = true } +async-io = { version = "2.6.0", optional = true } +async-fs = { version = "2.2.0", optional = true } +futures-lite = { version = "2.6.1", optional = true } [dev-dependencies] tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "time"] } diff --git a/src/lib.rs b/src/lib.rs index 89529ae..b73380f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,8 +147,8 @@ //! async runtime with the fd returned by `::as_raw_fd` to process events when //! they are ready. //! -//! For demonstrations of how to use this library in blocking, nonblocking, and async (tokio) modes, -//! please reference the "examples" directory. +//! For demonstrations of how to use this library in blocking, nonblocking, and async +//! (tokio / async-io) modes, please reference the "examples" directory. // should really be cfg(target_os = "linux") and maybe also android? #![cfg(unix)] @@ -161,6 +161,9 @@ // (see https://github.com/rust-lang/rust/pull/100883#issuecomment-1264470491) #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#[cfg(all(feature = "tokio", feature = "async-io"))] +compile_error!("Features 'tokio' and 'async-io' are mutually exclusive"); + // has to be first for its macro #[macro_use] mod attribute_set; diff --git a/src/raw_stream.rs b/src/raw_stream.rs index 8714bda..599b109 100644 --- a/src/raw_stream.rs +++ b/src/raw_stream.rs @@ -1,3 +1,4 @@ +#![allow(unused_unsafe)] //! A device implementation with no userspace synchronization performed. use std::fs::{File, OpenOptions}; @@ -636,7 +637,7 @@ impl RawDevice { Ok(keycode as u32) } - #[cfg(feature = "tokio")] + #[cfg(any(feature = "tokio", feature = "async-io"))] #[inline] pub fn into_event_stream(self) -> io::Result { EventStream::new(self) @@ -762,13 +763,16 @@ impl Iterator for EnumerateDevices { } } -#[cfg(feature = "tokio")] -mod tokio_stream { +#[cfg(any(feature = "tokio", feature = "async-io"))] +mod async_stream { use super::*; use std::future::poll_fn; use std::task::{ready, Context, Poll}; + #[cfg(feature = "tokio")] use tokio::io::unix::AsyncFd; + #[cfg(feature = "async-io")] + use async_io::Async as AsyncFd; /// An asynchronous stream of input events. /// @@ -796,10 +800,22 @@ mod tokio_stream { } /// Returns a mutable reference to the underlying device. + #[cfg(feature = "tokio")] pub fn device_mut(&mut self) -> &mut RawDevice { self.device.get_mut() } + /// Returns a mutable reference to the underlying device. + /// This is the same as [EventStream::device_mut] as with `tokio` feature, + /// but is unsafe due to async-io's file descriptor implementation. + /// + /// # Safety + /// Must not drop the mutable reference with mem::swap() or mem::take(). + #[cfg(feature = "async-io")] + pub unsafe fn device_mut_nodrop(&mut self) -> &mut RawDevice { + unsafe{self.device.get_mut()} + } + /// Try to wait for the next event in this stream. Any errors are likely to be fatal, i.e. /// any calls afterwards will likely error as well. pub async fn next_event(&mut self) -> io::Result { @@ -814,13 +830,22 @@ mod tokio_stream { return Poll::Ready(Ok(InputEvent::from(ev))); } - self.device.get_mut().event_buf.clear(); + unsafe{self.device.get_mut().event_buf.clear()}; self.index = 0; loop { - let mut guard = ready!(self.device.poll_read_ready_mut(cx))?; - - let res = guard.try_io(|device| device.get_mut().fill_events()); + let res = { + #[cfg(feature = "tokio")] + { + let mut guard = ready!(self.device.poll_read_ready_mut(cx))?; + guard.try_io(|device| device.get_mut().fill_events()) + } + #[cfg(feature = "async-io")] + { + ready!(self.device.poll_readable(cx))?; + unsafe {io::Result::Ok(self.device.get_mut().fill_events())} + } + }; match res { Ok(res) => { let _ = res?; @@ -844,5 +869,5 @@ mod tokio_stream { } } } -#[cfg(feature = "tokio")] -pub use tokio_stream::EventStream; +#[cfg(any(feature = "tokio", feature = "async-io"))] +pub use async_stream::EventStream; diff --git a/src/sync_stream.rs b/src/sync_stream.rs index c3605f4..3a9b8bd 100644 --- a/src/sync_stream.rs +++ b/src/sync_stream.rs @@ -1,3 +1,5 @@ +#![allow(unused_unsafe)] + use crate::compat::{input_absinfo, input_event}; use crate::constants::*; use crate::device_state::DeviceState; @@ -357,7 +359,7 @@ impl Device { }) } - #[cfg(feature = "tokio")] + #[cfg(any(feature = "tokio", feature = "async-io"))] pub fn into_event_stream(self) -> io::Result { EventStream::new(self) } @@ -779,13 +781,16 @@ impl fmt::Display for Device { } } -#[cfg(feature = "tokio")] -mod tokio_stream { +#[cfg(any(feature = "tokio", feature = "async-io"))] +mod async_stream { use super::*; use std::future::poll_fn; use std::task::{ready, Context, Poll}; + #[cfg(feature = "tokio")] use tokio::io::unix::AsyncFd; + #[cfg(feature = "async-io")] + use async_io::Async as AsyncFd; /// An asynchronous stream of input events. /// @@ -819,10 +824,22 @@ mod tokio_stream { } /// Returns a mutable reference to the underlying device + #[cfg(feature = "tokio")] pub fn device_mut(&mut self) -> &mut Device { self.device.get_mut() } + /// Returns a mutable reference to the underlying device. + /// This is the same as [EventStream::device_mut] as with `tokio` feature, + /// but is unsafe due to async-io's file descriptor implementation. + /// + /// # Safety + /// Must not drop the mutable reference with mem::swap() or mem::take(). + #[cfg(feature = "async-io")] + pub unsafe fn device_mut_nodrop(&mut self) -> &mut Device { + unsafe{self.device.get_mut()} + } + /// Try to wait for the next event in this stream. Any errors are likely to be fatal, i.e. /// any calls afterwards will likely error as well. pub async fn next_event(&mut self) -> io::Result { @@ -832,7 +849,7 @@ mod tokio_stream { /// A lower-level function for directly polling this stream. pub fn poll_event(&mut self, cx: &mut Context<'_>) -> Poll> { 'outer: loop { - let dev = self.device.get_mut(); + let dev = unsafe{self.device.get_mut()}; if let Some(ev) = compensate_events(&mut self.sync, dev) { return Poll::Ready(Ok(ev)); } @@ -856,9 +873,18 @@ mod tokio_stream { self.consumed_to = 0; loop { - let mut guard = ready!(self.device.poll_read_ready_mut(cx))?; - - let res = guard.try_io(|device| device.get_mut().fetch_events_inner()); + let res = { + #[cfg(feature = "tokio")] + { + let mut guard = ready!(self.device.poll_read_ready_mut(cx))?; + guard.try_io(|device| device.get_mut().fetch_events_inner()) + } + #[cfg(feature = "async-io")] + { + ready!(self.device.poll_readable(cx))?; + unsafe {io::Result::Ok(self.device.get_mut().fetch_events_inner())} + } + }; match res { Ok(res) => { self.sync = res?; @@ -883,8 +909,8 @@ mod tokio_stream { } } } -#[cfg(feature = "tokio")] -pub use tokio_stream::EventStream; +#[cfg(any(feature = "tokio", feature = "async-io"))] +pub use async_stream::EventStream; #[cfg(test)] mod tests { diff --git a/src/uinput.rs b/src/uinput.rs index d79e3c7..b445353 100644 --- a/src/uinput.rs +++ b/src/uinput.rs @@ -1,3 +1,4 @@ +#![allow(unused_unsafe)] //! Virtual device emulation for evdev via uinput. //! //! This is quite useful when testing/debugging devices, or synchronization. @@ -300,10 +301,13 @@ impl VirtualDevice { } /// Get the syspaths of the corresponding device nodes in /dev/input. - #[cfg(feature = "tokio")] + #[cfg(any(feature = "tokio", feature = "async-io"))] pub async fn enumerate_dev_nodes(&mut self) -> io::Result { let path = self.get_syspath()?; + #[cfg(feature = "tokio")] let dir = tokio::fs::read_dir(path).await?; + #[cfg(feature = "async-io")] + let dir = async_fs::read_dir(path).await?; Ok(DevNodes { dir }) } @@ -401,7 +405,7 @@ impl VirtualDevice { Ok(self.event_buf.drain(..).map(InputEvent::from)) } - #[cfg(feature = "tokio")] + #[cfg(any(feature = "tokio", feature = "async-io"))] #[inline] pub fn into_event_stream(self) -> io::Result { VirtualEventStream::new(self) @@ -445,16 +449,27 @@ impl Iterator for DevNodesBlocking { /// This struct is returned from the [VirtualDevice::enumerate_dev_nodes_blocking] function and /// will yield the syspaths corresponding to the virtual device. These are of the form /// `/dev/input123`. -#[cfg(feature = "tokio")] +#[cfg(any(feature = "tokio", feature = "async-io"))] pub struct DevNodes { + #[cfg(feature = "tokio")] dir: tokio::fs::ReadDir, + #[cfg(feature = "async-io")] + dir: async_fs::ReadDir, } -#[cfg(feature = "tokio")] +#[cfg(any(feature = "tokio", feature = "async-io"))] impl DevNodes { /// Returns the next entry in the set of device nodes. pub async fn next_entry(&mut self) -> io::Result> { - while let Some(entry) = self.dir.next_entry().await? { + while let Some(entry) = { + #[cfg(feature = "tokio")] + { self.dir.next_entry().await? } + #[cfg(feature = "async-io")] + { match futures_lite::StreamExt::next(&mut self.dir).await { + Some(v) => Some(v?), + None => None + } } + } { // Map the directory name to its file name. let file_name = entry.file_name(); @@ -562,13 +577,18 @@ impl Drop for FFEraseEvent { } } -#[cfg(feature = "tokio")] -mod tokio_stream { +#[cfg(any(feature = "tokio", feature = "async-io"))] +mod async_stream { use super::*; use std::future::poll_fn; use std::task::{ready, Context, Poll}; + + #[cfg(feature = "tokio")] use tokio::io::unix::AsyncFd; + #[cfg(feature = "async-io")] + use async_io::Async as AsyncFd; + /// An asynchronous stream of input events. /// @@ -596,10 +616,22 @@ mod tokio_stream { } /// Returns a mutable reference to the underlying device. + #[cfg(feature = "tokio")] pub fn device_mut(&mut self) -> &mut VirtualDevice { self.device.get_mut() } + /// Returns a mutable reference to the underlying device. + /// This is the same as [VirtualEventStream::device_mut] as with `tokio` feature, + /// but is unsafe due to async-io's file descriptor implementation. + /// + /// # Safety + /// Must not drop the mutable reference with mem::swap() or mem::take(). + #[cfg(feature = "async-io")] + pub unsafe fn device_mut_nodrop(&mut self) -> &mut VirtualDevice { + unsafe{self.device.get_mut()} + } + /// Try to wait for the next event in this stream. Any errors are likely to be fatal, i.e. /// any calls afterwards will likely error as well. pub async fn next_event(&mut self) -> io::Result { @@ -614,13 +646,23 @@ mod tokio_stream { return Poll::Ready(Ok(InputEvent::from(ev))); } - self.device.get_mut().event_buf.clear(); + unsafe{self.device.get_mut().event_buf.clear()}; self.index = 0; loop { - let mut guard = ready!(self.device.poll_read_ready_mut(cx))?; + let res = { + #[cfg(feature = "tokio")] + { + let mut guard = ready!(self.device.poll_read_ready_mut(cx))?; + guard.try_io(|device| device.get_mut().fill_events()) + } + #[cfg(feature = "async-io")] + { + ready!(self.device.poll_readable(cx))?; + unsafe {io::Result::Ok(self.device.get_mut().fill_events())} + } + }; - let res = guard.try_io(|device| device.get_mut().fill_events()); match res { Ok(res) => { let _ = res?; @@ -644,5 +686,5 @@ mod tokio_stream { } } } -#[cfg(feature = "tokio")] -pub use tokio_stream::VirtualEventStream; +#[cfg(any(feature = "tokio", feature = "async-io"))] +pub use async_stream::VirtualEventStream; From 52ff6adc07991c50daf5de77b45b3074d2fafd76 Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Thu, 11 Dec 2025 14:59:10 +0200 Subject: [PATCH 2/8] Add async-io test example --- Cargo.toml | 4 ++++ examples/evtest_async-io.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 examples/evtest_async-io.rs diff --git a/Cargo.toml b/Cargo.toml index b9ecbe8..063b82e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,10 @@ itertools = "0.10" name = "evtest_tokio" required-features = ["tokio"] +[[example]] +name = "evtest_async-io" +required-features = ["async-io"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/examples/evtest_async-io.rs b/examples/evtest_async-io.rs new file mode 100644 index 0000000..dddc2e6 --- /dev/null +++ b/examples/evtest_async-io.rs @@ -0,0 +1,17 @@ +//! Demonstrating how to monitor events with evdev + async-io + +// cli/"tui" shared between the evtest examples +mod _pick_device; + +fn main() { + let d = _pick_device::pick_device(); + println!("{}", d); + println!("Events:"); + let mut events = d.into_event_stream().unwrap(); + futures_lite::future::block_on(async { + loop { + let ev = events.next_event().await.unwrap(); + println!("{:?}", ev); + } + }); +} From 604425c3b4dd8062c369f31762309e9c8b1acc99 Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Thu, 11 Dec 2025 15:41:54 +0200 Subject: [PATCH 3/8] Run cargo fmt --- src/raw_stream.rs | 10 +++++----- src/sync_stream.rs | 10 +++++----- src/uinput.rs | 25 ++++++++++++++----------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/raw_stream.rs b/src/raw_stream.rs index 599b109..701d7ba 100644 --- a/src/raw_stream.rs +++ b/src/raw_stream.rs @@ -767,12 +767,12 @@ impl Iterator for EnumerateDevices { mod async_stream { use super::*; + #[cfg(feature = "async-io")] + use async_io::Async as AsyncFd; use std::future::poll_fn; use std::task::{ready, Context, Poll}; #[cfg(feature = "tokio")] use tokio::io::unix::AsyncFd; - #[cfg(feature = "async-io")] - use async_io::Async as AsyncFd; /// An asynchronous stream of input events. /// @@ -813,7 +813,7 @@ mod async_stream { /// Must not drop the mutable reference with mem::swap() or mem::take(). #[cfg(feature = "async-io")] pub unsafe fn device_mut_nodrop(&mut self) -> &mut RawDevice { - unsafe{self.device.get_mut()} + unsafe { self.device.get_mut() } } /// Try to wait for the next event in this stream. Any errors are likely to be fatal, i.e. @@ -830,7 +830,7 @@ mod async_stream { return Poll::Ready(Ok(InputEvent::from(ev))); } - unsafe{self.device.get_mut().event_buf.clear()}; + unsafe { self.device.get_mut().event_buf.clear() }; self.index = 0; loop { @@ -843,7 +843,7 @@ mod async_stream { #[cfg(feature = "async-io")] { ready!(self.device.poll_readable(cx))?; - unsafe {io::Result::Ok(self.device.get_mut().fill_events())} + unsafe { io::Result::Ok(self.device.get_mut().fill_events()) } } }; match res { diff --git a/src/sync_stream.rs b/src/sync_stream.rs index 3a9b8bd..93fb493 100644 --- a/src/sync_stream.rs +++ b/src/sync_stream.rs @@ -785,12 +785,12 @@ impl fmt::Display for Device { mod async_stream { use super::*; + #[cfg(feature = "async-io")] + use async_io::Async as AsyncFd; use std::future::poll_fn; use std::task::{ready, Context, Poll}; #[cfg(feature = "tokio")] use tokio::io::unix::AsyncFd; - #[cfg(feature = "async-io")] - use async_io::Async as AsyncFd; /// An asynchronous stream of input events. /// @@ -837,7 +837,7 @@ mod async_stream { /// Must not drop the mutable reference with mem::swap() or mem::take(). #[cfg(feature = "async-io")] pub unsafe fn device_mut_nodrop(&mut self) -> &mut Device { - unsafe{self.device.get_mut()} + unsafe { self.device.get_mut() } } /// Try to wait for the next event in this stream. Any errors are likely to be fatal, i.e. @@ -849,7 +849,7 @@ mod async_stream { /// A lower-level function for directly polling this stream. pub fn poll_event(&mut self, cx: &mut Context<'_>) -> Poll> { 'outer: loop { - let dev = unsafe{self.device.get_mut()}; + let dev = unsafe { self.device.get_mut() }; if let Some(ev) = compensate_events(&mut self.sync, dev) { return Poll::Ready(Ok(ev)); } @@ -882,7 +882,7 @@ mod async_stream { #[cfg(feature = "async-io")] { ready!(self.device.poll_readable(cx))?; - unsafe {io::Result::Ok(self.device.get_mut().fetch_events_inner())} + unsafe { io::Result::Ok(self.device.get_mut().fetch_events_inner()) } } }; match res { diff --git a/src/uinput.rs b/src/uinput.rs index b445353..cd59a13 100644 --- a/src/uinput.rs +++ b/src/uinput.rs @@ -463,12 +463,16 @@ impl DevNodes { pub async fn next_entry(&mut self) -> io::Result> { while let Some(entry) = { #[cfg(feature = "tokio")] - { self.dir.next_entry().await? } + { + self.dir.next_entry().await? + } #[cfg(feature = "async-io")] - { match futures_lite::StreamExt::next(&mut self.dir).await { - Some(v) => Some(v?), - None => None - } } + { + match futures_lite::StreamExt::next(&mut self.dir).await { + Some(v) => Some(v?), + None => None, + } + } } { // Map the directory name to its file name. let file_name = entry.file_name(); @@ -584,11 +588,10 @@ mod async_stream { use std::future::poll_fn; use std::task::{ready, Context, Poll}; - #[cfg(feature = "tokio")] - use tokio::io::unix::AsyncFd; #[cfg(feature = "async-io")] use async_io::Async as AsyncFd; - + #[cfg(feature = "tokio")] + use tokio::io::unix::AsyncFd; /// An asynchronous stream of input events. /// @@ -629,7 +632,7 @@ mod async_stream { /// Must not drop the mutable reference with mem::swap() or mem::take(). #[cfg(feature = "async-io")] pub unsafe fn device_mut_nodrop(&mut self) -> &mut VirtualDevice { - unsafe{self.device.get_mut()} + unsafe { self.device.get_mut() } } /// Try to wait for the next event in this stream. Any errors are likely to be fatal, i.e. @@ -646,7 +649,7 @@ mod async_stream { return Poll::Ready(Ok(InputEvent::from(ev))); } - unsafe{self.device.get_mut().event_buf.clear()}; + unsafe { self.device.get_mut().event_buf.clear() }; self.index = 0; loop { @@ -659,7 +662,7 @@ mod async_stream { #[cfg(feature = "async-io")] { ready!(self.device.poll_readable(cx))?; - unsafe {io::Result::Ok(self.device.get_mut().fill_events())} + unsafe { io::Result::Ok(self.device.get_mut().fill_events()) } } }; From cffbf0201c91602e73f999039fd723acba03e761 Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Thu, 11 Dec 2025 16:15:30 +0200 Subject: [PATCH 4/8] Move clippy warning supression to the line it is needed --- src/raw_stream.rs | 3 +-- src/sync_stream.rs | 3 +-- src/uinput.rs | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/raw_stream.rs b/src/raw_stream.rs index 701d7ba..b3df65b 100644 --- a/src/raw_stream.rs +++ b/src/raw_stream.rs @@ -1,4 +1,3 @@ -#![allow(unused_unsafe)] //! A device implementation with no userspace synchronization performed. use std::fs::{File, OpenOptions}; @@ -829,7 +828,7 @@ mod async_stream { self.index += 1; return Poll::Ready(Ok(InputEvent::from(ev))); } - + #[allow(unused_unsafe)] // async-io requires unsafe, tokio does not unsafe { self.device.get_mut().event_buf.clear() }; self.index = 0; diff --git a/src/sync_stream.rs b/src/sync_stream.rs index 93fb493..8777892 100644 --- a/src/sync_stream.rs +++ b/src/sync_stream.rs @@ -1,5 +1,3 @@ -#![allow(unused_unsafe)] - use crate::compat::{input_absinfo, input_event}; use crate::constants::*; use crate::device_state::DeviceState; @@ -849,6 +847,7 @@ mod async_stream { /// A lower-level function for directly polling this stream. pub fn poll_event(&mut self, cx: &mut Context<'_>) -> Poll> { 'outer: loop { + #[allow(unused_unsafe)] // async-io requires unsafe, tokio does not let dev = unsafe { self.device.get_mut() }; if let Some(ev) = compensate_events(&mut self.sync, dev) { return Poll::Ready(Ok(ev)); diff --git a/src/uinput.rs b/src/uinput.rs index cd59a13..666a94c 100644 --- a/src/uinput.rs +++ b/src/uinput.rs @@ -1,4 +1,3 @@ -#![allow(unused_unsafe)] //! Virtual device emulation for evdev via uinput. //! //! This is quite useful when testing/debugging devices, or synchronization. @@ -648,7 +647,7 @@ mod async_stream { self.index += 1; return Poll::Ready(Ok(InputEvent::from(ev))); } - + #[allow(unused_unsafe)] // async-io requires unsafe, tokio does not unsafe { self.device.get_mut().event_buf.clear() }; self.index = 0; From a50ab81007d58fe7032531b3e01c1556bd0b1191 Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Thu, 11 Dec 2025 17:22:32 +0200 Subject: [PATCH 5/8] Add test for async-io --- Cargo.toml | 1 + tests/virtual_device.rs | 107 ++++++++++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 063b82e..002abe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ futures-lite = { version = "2.6.1", optional = true } [dev-dependencies] tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "time"] } itertools = "0.10" +async-executor = "1.13.3" [[example]] name = "evtest_tokio" diff --git a/tests/virtual_device.rs b/tests/virtual_device.rs index 2d8f918..38fb022 100644 --- a/tests/virtual_device.rs +++ b/tests/virtual_device.rs @@ -1,56 +1,87 @@ -#![cfg(feature = "tokio")] +#![cfg(any(feature = "tokio", feature = "async-io"))] use std::error::Error; use std::thread::sleep; use std::time::Duration; +#[cfg(feature = "tokio")] use tokio::time::timeout; +#[cfg(feature = "async-io")] +use futures_lite::FutureExt; use evdev::{uinput::VirtualDevice, AttributeSet, EventType, InputEvent, KeyCode}; -#[tokio::test] -async fn test_virtual_device_actually_emits() -> Result<(), Box> { - let mut keys = AttributeSet::::new(); - let virtual_device_name = "fake-keyboard"; - keys.insert(KeyCode::KEY_ESC); +#[test] +fn test_virtual_device_actually_emits() -> Result<(), Box> { + #[cfg(feature = "async-io")] + let ex = async_executor::Executor::new(); - let mut device = VirtualDevice::builder()? - .name(virtual_device_name) - .with_keys(&keys)? - .build() - .unwrap(); - - let mut maybe_device = None; - sleep(Duration::from_millis(500)); - for (_i, d) in evdev::enumerate() { - println!("{:?}", d.name()); - if d.name() == Some(virtual_device_name) { - maybe_device = Some(d); - break; + let fut = async { + let mut keys = AttributeSet::::new(); + let virtual_device_name = "fake-keyboard"; + keys.insert(KeyCode::KEY_ESC); + + let mut device = VirtualDevice::builder()? + .name(virtual_device_name) + .with_keys(&keys)? + .build() + .unwrap(); + + let mut maybe_device = None; + sleep(Duration::from_millis(500)); + for (_i, d) in evdev::enumerate() { + println!("{:?}", d.name()); + if d.name() == Some(virtual_device_name) { + maybe_device = Some(d); + break; + } } - } - assert!(maybe_device.is_some()); - let listen_device = maybe_device.unwrap(); + assert!(maybe_device.is_some()); + let listen_device = maybe_device.unwrap(); + + let type_ = EventType::KEY; + let code = KeyCode::KEY_ESC.code(); - let type_ = EventType::KEY; - let code = KeyCode::KEY_ESC.code(); + let fut = async move { + // try to read the key code that will be sent through virtual device + let mut events = listen_device.into_event_stream()?; + events.next_event().await + }; - // listen for events on the listen device - let listener = tokio::spawn(async move { - // try to read the key code that will be sent through virtual device - let mut events = listen_device.into_event_stream()?; - events.next_event().await - }); + // listen for events on the listen device + #[cfg(feature = "tokio")] + let listener = tokio::spawn(fut); + #[cfg(feature = "async-io")] + let listener = ex.spawn(fut); - // emit a key code through virtual device - let down_event = InputEvent::new(type_.0, code, 10); - device.emit(&[down_event]).unwrap(); + // emit a key code through virtual device + let down_event = InputEvent::new(type_.0, code, 10); + device.emit(&[down_event]).unwrap(); - let event = timeout(Duration::from_secs(1), listener).await???; + let time = Duration::from_secs(1); + #[cfg(feature = "tokio")] + let event = timeout(time, listener).await???; + #[cfg(feature = "async-io")] + let event = listener.or(async { + async_io::Timer::after(time).await; + Err(std::io::ErrorKind::TimedOut.into()) + }).await?; - assert_eq!(down_event.event_type(), event.event_type()); - assert_eq!(down_event.code(), event.code()); + assert_eq!(down_event.event_type(), event.event_type()); + assert_eq!(down_event.code(), event.code()); + + // wait for listener + Ok(()) + }; + + #[cfg(feature = "tokio")] + let res = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(fut); + #[cfg(feature = "async-io")] + let res = futures_lite::future::block_on(fut); - // wait for listener - Ok(()) + res } From 0a84916664edcb6cbcec7702501865eceed2c5be Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Wed, 25 Feb 2026 12:12:56 +0200 Subject: [PATCH 6/8] Breaking change: Feature 'stream-trait' does not imply feature 'tokio' anymore --- Cargo.toml | 4 ++-- src/lib.rs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 002abe9..89c63b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "evdev" -version = "0.13.3" +version = "0.14.0" authors = ["Corey Richardson "] description = "evdev interface for Linux" license = "Apache-2.0 OR MIT" @@ -13,7 +13,7 @@ rust-version = "1.64" serde = ["dep:serde"] async-io = ["dep:async-io", "dep:async-fs", "dep:futures-lite"] tokio = ["dep:tokio"] -stream-trait = ["tokio", "futures-core"] +stream-trait = ["futures-core"] device-test = [] [dependencies] diff --git a/src/lib.rs b/src/lib.rs index b73380f..b0b63d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,6 +164,9 @@ #[cfg(all(feature = "tokio", feature = "async-io"))] compile_error!("Features 'tokio' and 'async-io' are mutually exclusive"); +#[cfg(all(feature = "stream-trait", not(any(feature = "tokio", feature = "async-io"))))] +compile_error!("Feature 'stream-trait' requires either 'tokio' or 'async-io' feature."); + // has to be first for its macro #[macro_use] mod attribute_set; From 2429bbb988e971a6e86804f82f56357e2994ca2f Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Wed, 25 Feb 2026 14:20:50 +0200 Subject: [PATCH 7/8] Raise a runtime warning if async-io feature is used from tokio runtime --- Cargo.toml | 6 +++--- src/lib.rs | 14 ++++++++++++++ src/raw_stream.rs | 2 ++ src/sync_stream.rs | 2 ++ src/uinput.rs | 2 ++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89c63b7..375a220 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,8 @@ rust-version = "1.64" [features] serde = ["dep:serde"] -async-io = ["dep:async-io", "dep:async-fs", "dep:futures-lite"] -tokio = ["dep:tokio"] +async-io = ["dep:async-io", "dep:async-fs", "dep:futures-lite", "dep:tokio", "tokio?/rt"] +tokio = ["dep:tokio", "tokio/fs", "tokio/time", "tokio/net"] stream-trait = ["futures-core"] device-test = [] @@ -23,7 +23,7 @@ cfg-if = "1.0" nix = { version = "0.29", features = ["ioctl", "fs", "event"] } serde = { version = "1.0", features = ["derive"], optional = true } -tokio = { version = "1.17", features = ["fs","time", "net"], optional = true } +tokio = { version = "1.17", optional = true } futures-core = { version = "0.3", optional = true } async-io = { version = "2.6.0", optional = true } async-fs = { version = "2.2.0", optional = true } diff --git a/src/lib.rs b/src/lib.rs index b0b63d9..12a5180 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -613,3 +613,17 @@ pub struct AutoRepeat { /// The duration, in milliseconds, between auto-repetitions of a held-down key. pub period: u32, } + +#[cfg(feature = "async-io")] +fn warn_if_tokio() { + static WARN: std::sync::Once = std::sync::Once::new(); + WARN.call_once(|| { + if tokio::runtime::Handle::try_current().is_ok() { + eprintln!( + "Warning: evdev is configured with feature 'async-io', but is called \ + from tokio runtime. While it works, it causes wakeup storms with 100x \ + performance overhead. Please use evdev with feauture 'tokio' instead." + ); + } + }); +} diff --git a/src/raw_stream.rs b/src/raw_stream.rs index b3df65b..da4129e 100644 --- a/src/raw_stream.rs +++ b/src/raw_stream.rs @@ -639,6 +639,8 @@ impl RawDevice { #[cfg(any(feature = "tokio", feature = "async-io"))] #[inline] pub fn into_event_stream(self) -> io::Result { + #[cfg(feature = "async-io")] + crate::warn_if_tokio(); EventStream::new(self) } diff --git a/src/sync_stream.rs b/src/sync_stream.rs index 8777892..de45d5a 100644 --- a/src/sync_stream.rs +++ b/src/sync_stream.rs @@ -359,6 +359,8 @@ impl Device { #[cfg(any(feature = "tokio", feature = "async-io"))] pub fn into_event_stream(self) -> io::Result { + #[cfg(feature = "async-io")] + crate::warn_if_tokio(); EventStream::new(self) } diff --git a/src/uinput.rs b/src/uinput.rs index 666a94c..86bb411 100644 --- a/src/uinput.rs +++ b/src/uinput.rs @@ -407,6 +407,8 @@ impl VirtualDevice { #[cfg(any(feature = "tokio", feature = "async-io"))] #[inline] pub fn into_event_stream(self) -> io::Result { + #[cfg(feature = "async-io")] + crate::warn_if_tokio(); VirtualEventStream::new(self) } } From c7ff124bd0f75e3a6d5df446e0481e481d4c3514 Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Wed, 25 Feb 2026 14:34:42 +0200 Subject: [PATCH 8/8] Update changelog to 0.14.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a293f5b..2365243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ ### Fixed +## evdev 0.14.0 (2026-02-25) +[b946f93...2429bbb](https://github.com/emberian/evdev/compare/b946f93...2429bbb) + +### Added +- Add `smol`/`async-io` support + +### Breaking changes +- Feature `stream-trait` does not imply feature `tokio` anymore, and thus `stream-trait` requires either `tokio` or `async-io` to be enabled. + +### Fixed + ## evdev 0.13.1 (2025-03-31) [7cbae16...6aed780](https://github.com/emberian/evdev/compare/7cbae16...6aed780)