use std::io;
use std::os::fd::{AsRawFd, IntoRawFd, OwnedFd, RawFd};
use io_uring::{IoUring, opcode, types::Fd};
use thiserror::Error;
use super::op::{Completion, SqeFlags, UserData};
use super::socket::SockAddr;
#[derive(Clone, Copy, Eq, PartialEq, Default)]
pub struct ProactorSetupFlags(u32);
impl ProactorSetupFlags {
pub const NONE: Self = Self(0);
pub const COOP_TASKRUN: Self = Self(1 << 0);
pub const TASKRUN_FLAG: Self = Self(1 << 1);
pub const SINGLE_ISSUER: Self = Self(1 << 2);
pub const DEFER_TASKRUN: Self = Self(1 << 3);
#[inline]
#[must_use]
pub const fn empty() -> Self {
Self::NONE
}
#[inline]
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[inline]
#[must_use]
pub const fn contains(self, flag: Self) -> bool {
(self.0 & flag.0) == flag.0
}
#[inline]
#[must_use]
pub const fn bits(self) -> u32 {
self.0
}
}
impl std::fmt::Debug for ProactorSetupFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut list = f.debug_list();
if self.contains(Self::COOP_TASKRUN) {
list.entry(&"COOP_TASKRUN");
}
if self.contains(Self::TASKRUN_FLAG) {
list.entry(&"TASKRUN_FLAG");
}
if self.contains(Self::SINGLE_ISSUER) {
list.entry(&"SINGLE_ISSUER");
}
if self.contains(Self::DEFER_TASKRUN) {
list.entry(&"DEFER_TASKRUN");
}
list.finish()
}
}
impl std::ops::BitOr for ProactorSetupFlags {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for ProactorSetupFlags {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProactorConfig {
pub sq_entries: u32,
pub cq_entries: Option<u32>,
pub setup_flags: ProactorSetupFlags,
}
impl Default for ProactorConfig {
fn default() -> Self {
Self {
sq_entries: 256,
cq_entries: None,
setup_flags: ProactorSetupFlags::NONE,
}
}
}
impl ProactorConfig {
#[inline]
#[must_use]
pub const fn with_sq_entries(mut self, entries: u32) -> Self {
self.sq_entries = entries;
self
}
#[inline]
#[must_use]
pub const fn with_cq_entries(mut self, entries: u32) -> Self {
self.cq_entries = Some(entries);
self
}
#[inline]
#[must_use]
pub const fn with_setup_flags(mut self, flags: ProactorSetupFlags) -> Self {
self.setup_flags = flags;
self
}
}
#[derive(Debug, Error)]
pub enum ProactorError {
#[error("io_uring init failed: {0}")]
Init(#[source] io::Error),
#[error("invalid proactor config: {0}")]
InvalidConfig(&'static str),
#[error("submission queue full")]
SqFull,
#[error("io_uring submit failed: {0}")]
Submit(#[source] io::Error),
}
pub struct Proactor {
ring: IoUring,
}
impl std::fmt::Debug for Proactor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let params = self.ring.params();
f.debug_struct("Proactor")
.field("sq_entries", ¶ms.sq_entries())
.field("cq_entries", ¶ms.cq_entries())
.finish()
}
}
impl Proactor {
pub fn new(config: ProactorConfig) -> Result<Self, ProactorError> {
validate_config(config)?;
let mut builder = IoUring::builder();
if let Some(cq_entries) = config.cq_entries {
builder.setup_cqsize(cq_entries);
}
apply_setup_flags(&mut builder, config.setup_flags);
let ring = builder
.build(config.sq_entries)
.map_err(ProactorError::Init)?;
Ok(Self { ring })
}
#[must_use]
pub fn supports_recvsend_bundle(&self) -> bool {
self.ring.params().is_feature_recvsend_bundle()
}
pub unsafe fn submit_connect(
&mut self,
fd: RawFd,
addr: &SockAddr,
user_data: UserData,
flags: SqeFlags,
) -> Result<(), ProactorError> {
let entry = opcode::Connect::new(Fd(fd), addr.as_ptr(), addr.len())
.build()
.flags(flags.into_io_uring())
.user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
Ok(())
}
pub unsafe fn submit_recv(
&mut self,
fd: RawFd,
buf: *mut u8,
len: u32,
user_data: UserData,
flags: SqeFlags,
) -> Result<(), ProactorError> {
let entry = opcode::Recv::new(Fd(fd), buf, len)
.build()
.flags(flags.into_io_uring())
.user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
Ok(())
}
pub unsafe fn submit_send(
&mut self,
fd: RawFd,
buf: *const u8,
len: u32,
user_data: UserData,
flags: SqeFlags,
) -> Result<(), ProactorError> {
let entry = opcode::Send::new(Fd(fd), buf, len)
.build()
.flags(flags.into_io_uring())
.user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
Ok(())
}
pub fn submit_close(&mut self, fd: OwnedFd, user_data: UserData) -> Result<(), ProactorError> {
let raw = fd.as_raw_fd();
let entry = opcode::Close::new(Fd(raw))
.build()
.user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
let _ = fd.into_raw_fd();
Ok(())
}
pub unsafe fn submit_close_raw(
&mut self,
fd: RawFd,
user_data: UserData,
) -> Result<(), ProactorError> {
let entry = opcode::Close::new(Fd(fd))
.build()
.user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
Ok(())
}
pub fn submit_nop(&mut self, user_data: UserData) -> Result<(), ProactorError> {
let entry = opcode::Nop::new().build().user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
Ok(())
}
pub unsafe fn submit_recv_multishot(
&mut self,
fd: RawFd,
buf_group: u16,
user_data: UserData,
) -> Result<(), ProactorError> {
let entry = opcode::RecvMulti::new(Fd(fd), buf_group)
.build()
.user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
Ok(())
}
pub unsafe fn submit_recv_multishot_bundle(
&mut self,
fd: RawFd,
buf_group: u16,
user_data: UserData,
) -> Result<(), ProactorError> {
let entry = opcode::RecvMultiBundle::new(Fd(fd), buf_group)
.build()
.user_data(user_data.raw());
unsafe {
self.ring
.submission()
.push(&entry)
.map_err(|_| ProactorError::SqFull)?;
}
Ok(())
}
pub unsafe fn register_buf_ring(
&mut self,
ring_addr: *const u8,
ring_entries: u16,
bgid: u16,
) -> Result<(), ProactorError> {
unsafe {
self.ring
.submitter()
.register_buf_ring_with_flags(ring_addr as u64, ring_entries, bgid, 0)
.map_err(ProactorError::Submit)?;
}
Ok(())
}
pub fn unregister_buf_ring(&mut self, bgid: u16) -> Result<(), ProactorError> {
self.ring
.submitter()
.unregister_buf_ring(bgid)
.map_err(ProactorError::Submit)?;
Ok(())
}
pub fn submit_and_wait(&mut self, wait_nr: usize) -> Result<usize, ProactorError> {
self.ring
.submit_and_wait(wait_nr)
.map_err(ProactorError::Submit)
}
pub fn submit(&mut self) -> Result<usize, ProactorError> {
self.ring
.submitter()
.submit()
.map_err(ProactorError::Submit)
}
pub fn wait_for_cqe(&mut self, wait_nr: usize) -> Result<usize, ProactorError> {
if wait_nr == 0 {
return Ok(0);
}
self.ring
.submit_and_wait(wait_nr)
.map_err(ProactorError::Submit)
}
pub fn drain_completions(&mut self, mut sink: impl FnMut(Completion)) -> usize {
let cq = self.ring.completion();
let mut count = 0;
for cqe in cq {
count += 1;
sink(Completion {
user_data: UserData::from_raw(cqe.user_data()),
result: cqe.result(),
flags: cqe.flags(),
});
}
count
}
}
fn validate_config(config: ProactorConfig) -> Result<(), ProactorError> {
if config.sq_entries == 0 || !config.sq_entries.is_power_of_two() {
return Err(ProactorError::InvalidConfig(
"sq_entries must be a non-zero power of two",
));
}
if let Some(cq_entries) = config.cq_entries {
if cq_entries <= config.sq_entries {
return Err(ProactorError::InvalidConfig(
"cq_entries must be greater than sq_entries",
));
}
if !cq_entries.is_power_of_two() {
return Err(ProactorError::InvalidConfig(
"cq_entries must be a power of two",
));
}
}
let flags = config.setup_flags;
if flags.contains(ProactorSetupFlags::DEFER_TASKRUN)
&& !flags.contains(ProactorSetupFlags::SINGLE_ISSUER)
{
return Err(ProactorError::InvalidConfig(
"DEFER_TASKRUN requires SINGLE_ISSUER",
));
}
if flags.contains(ProactorSetupFlags::TASKRUN_FLAG)
&& !(flags.contains(ProactorSetupFlags::COOP_TASKRUN)
|| flags.contains(ProactorSetupFlags::DEFER_TASKRUN))
{
return Err(ProactorError::InvalidConfig(
"TASKRUN_FLAG requires COOP_TASKRUN or DEFER_TASKRUN",
));
}
if flags.contains(ProactorSetupFlags::COOP_TASKRUN)
&& flags.contains(ProactorSetupFlags::DEFER_TASKRUN)
{
return Err(ProactorError::InvalidConfig(
"COOP_TASKRUN and DEFER_TASKRUN are separate taskrun modes",
));
}
Ok(())
}
fn apply_setup_flags(builder: &mut io_uring::Builder, flags: ProactorSetupFlags) {
if flags.contains(ProactorSetupFlags::COOP_TASKRUN) {
builder.setup_coop_taskrun();
}
if flags.contains(ProactorSetupFlags::TASKRUN_FLAG) {
builder.setup_taskrun_flag();
}
if flags.contains(ProactorSetupFlags::SINGLE_ISSUER) {
builder.setup_single_issuer();
}
if flags.contains(ProactorSetupFlags::DEFER_TASKRUN) {
builder.setup_defer_taskrun();
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::proactor::op::OpKind;
use crate::proactor::socket::{Domain, TcpSocket};
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
#[test]
fn connect_to_local_listener_yields_cqe() {
let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).unwrap();
let local_addr = listener.local_addr().unwrap();
let sock_addr = SockAddr::from_std(local_addr);
let socket = TcpSocket::new(Domain::V4).unwrap();
socket.set_nodelay(true).unwrap();
let fd = socket.as_raw_fd();
let mut proactor = Proactor::new(ProactorConfig::default()).unwrap();
let ud = UserData::new(OpKind::Connect, 1);
unsafe {
proactor
.submit_connect(fd, &sock_addr, ud, SqeFlags::NONE)
.unwrap();
}
proactor.submit_and_wait(1).unwrap();
let mut got: Option<Completion> = None;
let n = proactor.drain_completions(|c| got = Some(c));
assert_eq!(n, 1);
let c = got.expect("one completion");
assert_eq!(c.user_data.kind(), Some(OpKind::Connect));
assert_eq!(c.user_data.token(), 1);
assert_eq!(
c.result, 0,
"connect should succeed (errno = {})",
-c.result
);
let (_peer, _) = listener.accept().unwrap();
}
#[test]
fn send_recv_roundtrip() {
use std::io::{Read, Write};
use std::thread;
let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).unwrap();
let local_addr = listener.local_addr().unwrap();
let server = thread::spawn(move || {
let (mut s, _) = listener.accept().unwrap();
let mut buf = [0_u8; 4];
s.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"ping");
s.write_all(b"pong").unwrap();
});
let sock_addr = SockAddr::from_std(local_addr);
let socket = TcpSocket::new(Domain::V4).unwrap();
socket.set_nodelay(true).unwrap();
let fd = socket.as_raw_fd();
let mut proactor = Proactor::new(ProactorConfig::default()).unwrap();
let ud_connect = UserData::new(OpKind::Connect, 1);
unsafe {
proactor
.submit_connect(fd, &sock_addr, ud_connect, SqeFlags::NONE)
.unwrap();
}
proactor.submit_and_wait(1).unwrap();
let mut got: Option<Completion> = None;
proactor.drain_completions(|c| got = Some(c));
let c = got.unwrap();
assert_eq!(c.user_data.kind(), Some(OpKind::Connect));
c.to_result().expect("connect ok");
let payload = *b"ping";
let ud_send = UserData::new(OpKind::Send, 2);
unsafe {
proactor
.submit_send(fd, payload.as_ptr(), 4, ud_send, SqeFlags::NONE)
.unwrap();
}
proactor.submit_and_wait(1).unwrap();
got = None;
proactor.drain_completions(|c| got = Some(c));
let c = got.unwrap();
assert_eq!(c.user_data.kind(), Some(OpKind::Send));
assert_eq!(c.to_result().unwrap(), 4);
let mut buf = [0_u8; 4];
let ud_recv = UserData::new(OpKind::Recv, 3);
unsafe {
proactor
.submit_recv(fd, buf.as_mut_ptr(), 4, ud_recv, SqeFlags::NONE)
.unwrap();
}
proactor.submit_and_wait(1).unwrap();
got = None;
proactor.drain_completions(|c| got = Some(c));
let c = got.unwrap();
assert_eq!(c.user_data.kind(), Some(OpKind::Recv));
assert_eq!(c.to_result().unwrap(), 4);
assert_eq!(&buf, b"pong");
server.join().unwrap();
}
#[test]
fn drain_empty_cq_returns_zero() {
let mut proactor = Proactor::new(ProactorConfig::default()).unwrap();
let mut sink_called = false;
let n = proactor.drain_completions(|_| sink_called = true);
assert_eq!(n, 0);
assert!(!sink_called);
}
#[test]
fn submit_close_keeps_fd_owned_when_sq_full() {
use std::fs::File;
use std::os::fd::{FromRawFd, IntoRawFd};
let mut proactor = Proactor::new(ProactorConfig::default().with_sq_entries(2)).unwrap();
let mut pushed = 0_u64;
loop {
match proactor.submit_nop(UserData::new(OpKind::Nop, pushed)) {
Ok(()) => pushed += 1,
Err(e) => {
assert!(
matches!(e, ProactorError::SqFull),
"unexpected submit_nop error: {e}"
);
break;
}
}
assert!(pushed < 1024, "test failed to fill the submission queue");
}
assert!(pushed > 0);
let file = File::open("/dev/null").unwrap();
let raw = file.into_raw_fd();
let owned = unsafe { OwnedFd::from_raw_fd(raw) };
assert!(matches!(
proactor.submit_close(owned, UserData::new(OpKind::Close, 99)),
Err(ProactorError::SqFull)
));
let rc = unsafe { libc::fcntl(raw, libc::F_GETFD) };
assert_eq!(rc, -1);
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EBADF)
);
}
}