io_uring: Add basic wrapper

This is a simple RAII wrapper around a ring instance that structure SQE
/ CQE callbacks to make it convenient for C++ usage.

Change-Id: I9db8b48a81bec8d8aff4a362920f4dd688c27d57
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/meson_options.txt b/meson_options.txt
index 2914f5c..f96c48c 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,4 @@
 option('fd', type: 'feature', description: 'Managed file descriptor support')
+option('io_uring', type: 'feature', description: 'io_uring wrapper support')
 option('tests', type: 'feature', description: 'Build tests')
 option('examples', type: 'boolean', value: true, description: 'Build examples')
diff --git a/src/meson.build b/src/meson.build
index 11bebe7..2a0169a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -68,6 +68,26 @@
   error('File descriptor support required')
 endif
 
+io_uring_dep = dependency('liburing', required: get_option('io_uring'))
+has_io_uring = false
+if not get_option('io_uring').disabled() and has_fd and io_uring_dep.found()
+  has_io_uring = true
+
+  stdplus_deps += [
+    io_uring_dep,
+  ]
+
+  stdplus_srcs += [
+    'stdplus/io_uring.cpp',
+  ]
+
+  install_headers(
+    'stdplus/io_uring.hpp',
+    subdir: 'stdplus')
+elif get_option('io_uring').enabled()
+  error('File descriptor support required')
+endif
+
 stdplus_lib = library(
   'stdplus',
   stdplus_srcs,
diff --git a/src/stdplus/io_uring.cpp b/src/stdplus/io_uring.cpp
new file mode 100644
index 0000000..3ec5da1
--- /dev/null
+++ b/src/stdplus/io_uring.cpp
@@ -0,0 +1,111 @@
+#include <liburing.h>
+#include <sys/eventfd.h>
+
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <stdplus/io_uring.hpp>
+#include <stdplus/util/cexec.hpp>
+
+#include <algorithm>
+
+namespace stdplus
+{
+
+IoUring::IoUring(size_t queue_size)
+{
+    CHECK_RET(io_uring_queue_init(queue_size, &ring, 0), "io_uring_queue_init");
+}
+
+IoUring::~IoUring()
+{
+    io_uring_queue_exit(&ring);
+    io_uring_cqe cqe{};
+    cqe.res = -ECANCELED;
+    for (auto h : handlers)
+    {
+        h->handleCQE(cqe);
+    }
+}
+
+io_uring_sqe& IoUring::getSQE()
+{
+    return *CHECK_ERRNO(io_uring_get_sqe(&ring), "io_uring_get_sqe");
+}
+
+void IoUring::setHandler(io_uring_sqe& sqe, CQEHandler* h) noexcept
+{
+    auto oldh = reinterpret_cast<CQEHandler*>(sqe.user_data);
+    if (oldh == h)
+    {
+        return;
+    }
+    io_uring_cqe cqe{};
+    cqe.res = -ECANCELED;
+    dropHandler(oldh, cqe);
+    if (h != nullptr)
+    {
+        handlers.push_back(h);
+    }
+    io_uring_sqe_set_data(&sqe, h);
+}
+
+void IoUring::cancelHandler(CQEHandler& h)
+{
+    io_uring_prep_cancel(&getSQE(), &h, 0);
+    submit();
+}
+
+void IoUring::submit()
+{
+    CHECK_RET(io_uring_submit(&ring), "io_uring_submit");
+}
+
+void IoUring::process() noexcept
+{
+    io_uring_cqe* cqe;
+    while (io_uring_peek_cqe(&ring, &cqe) == 0)
+    {
+        auto h = reinterpret_cast<CQEHandler*>(cqe->user_data);
+        dropHandler(h, *cqe);
+        io_uring_cqe_seen(&ring, cqe);
+    }
+}
+
+stdplus::ManagedFd& IoUring::getEventFd()
+{
+    if (event_fd)
+    {
+        return *event_fd;
+    }
+    stdplus::ManagedFd efd(CHECK_RET(eventfd(0, EFD_NONBLOCK), "eventfd"));
+    CHECK_RET(io_uring_register_eventfd(&ring, efd.get()),
+              "io_uring_register_eventfd");
+    return *(event_fd = std::move(efd));
+}
+
+void IoUring::processEvents()
+{
+    auto& efd = getEventFd();
+    std::byte b[8];
+    while (!stdplus::fd::read(efd, b).empty())
+    {
+        process();
+    }
+}
+
+void IoUring::dropHandler(CQEHandler* h, io_uring_cqe& cqe) noexcept
+{
+    if (h == nullptr)
+    {
+        return;
+    }
+    auto it = std::find(handlers.begin(), handlers.end(), h);
+    if (it != handlers.end() - 1)
+    {
+        std::swap(*it, handlers.back());
+    }
+    handlers.pop_back();
+    h->handleCQE(cqe);
+}
+
+} // namespace stdplus
diff --git a/src/stdplus/io_uring.hpp b/src/stdplus/io_uring.hpp
new file mode 100644
index 0000000..d3090ed
--- /dev/null
+++ b/src/stdplus/io_uring.hpp
@@ -0,0 +1,84 @@
+#pragma once
+#include <liburing.h>
+
+#include <stdplus/fd/managed.hpp>
+
+#include <optional>
+#include <vector>
+
+namespace stdplus
+{
+
+class IoUring
+{
+  public:
+    struct CQEHandler
+    {
+        CQEHandler() = default;
+        CQEHandler(CQEHandler&&) = delete;
+        CQEHandler& operator=(CQEHandler&&) = delete;
+        CQEHandler(const CQEHandler&) = delete;
+        CQEHandler& operator=(const CQEHandler&) = delete;
+        virtual ~CQEHandler() = default;
+
+        virtual void handleCQE(io_uring_cqe&) noexcept = 0;
+    };
+
+    explicit IoUring(size_t queue_size = 10);
+    IoUring(IoUring&&) = delete;
+    IoUring& operator=(IoUring&&) = delete;
+    IoUring(const IoUring&) = delete;
+    IoUring& operator=(const IoUring&) = delete;
+    ~IoUring();
+
+    /** @brief Gets an unused SQE from the ring
+     *
+     *  @throws std::system_error if the allocation fails
+     *  @return An SQE on the ring
+     */
+    io_uring_sqe& getSQE();
+
+    /** @brief Associates the SQE with a user provided callback handler
+     *
+     *  @param[in] sqe - The SQE that we want to register
+     *  @param[in] h   - The handler which will be run when the CQE comes back
+     */
+    void setHandler(io_uring_sqe& sqe, CQEHandler* h) noexcept;
+
+    /** @brief Cancels the outstanding request associated with a handler
+     *
+     *  @param[in] h - The handler associated with the request
+     */
+    void cancelHandler(CQEHandler& h);
+
+    /** @brief Submits all outstanding SQEs to the kernel
+     *
+     *  @throws std::system_error if the submission fails
+     */
+    void submit();
+
+    /** @brief Non-blocking process all outstanding CQEs */
+    void process() noexcept;
+
+    /** @brief Returns the EventFD associated with the ring
+     *         A new descriptor is created if it does not yet exist
+     *
+     *  @throws std::system_error if constructing the event fd fails
+     *  @return A reference to the descriptor
+     */
+    stdplus::ManagedFd& getEventFd();
+
+    /** @brief Non-blocking process all outstanding eventFd events
+     *         Should be used instead of process() to clear eventFd events.
+     */
+    void processEvents();
+
+  private:
+    io_uring ring;
+    std::optional<stdplus::ManagedFd> event_fd;
+    std::vector<CQEHandler*> handlers;
+
+    void dropHandler(CQEHandler* h, io_uring_cqe& cqe) noexcept;
+};
+
+} // namespace stdplus
diff --git a/test/io_uring.cpp b/test/io_uring.cpp
new file mode 100644
index 0000000..5be1b91
--- /dev/null
+++ b/test/io_uring.cpp
@@ -0,0 +1,134 @@
+#include <poll.h>
+
+#include <stdplus/io_uring.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace stdplus
+{
+
+using testing::_;
+
+class MockHandler : public IoUring::CQEHandler
+{
+  public:
+    MOCK_METHOD(void, handleCQE, (io_uring_cqe&), (noexcept, override));
+};
+
+class IoUringTest : public testing::Test
+{
+  protected:
+    static void SetUpTestCase()
+    {
+        io_uring r;
+        if (io_uring_queue_init(1, &r, 0) == -ENOSYS)
+        {
+            // Not supported, skip running this test
+            exit(77);
+        }
+        io_uring_queue_exit(&r);
+    }
+    std::array<testing::StrictMock<MockHandler>, 2> h;
+    IoUring ring;
+};
+
+TEST_F(IoUringTest, NullHandler)
+{
+    auto& sqe = ring.getSQE();
+    io_uring_prep_nop(&sqe);
+    ring.submit();
+    ring.process();
+}
+
+TEST_F(IoUringTest, HandlerCalled)
+{
+    {
+        auto& sqe = ring.getSQE();
+        io_uring_prep_nop(&sqe);
+        ring.setHandler(sqe, &h[0]);
+    }
+
+    // Nothing should happen without submission
+    ring.process();
+
+    {
+        auto& sqe = ring.getSQE();
+        io_uring_prep_nop(&sqe);
+        ring.setHandler(sqe, &h[1]);
+    }
+
+    // Handle all of the outstanding requests
+    ring.submit();
+    EXPECT_CALL(h[0], handleCQE(_));
+    EXPECT_CALL(h[1], handleCQE(_));
+    ring.process();
+    testing::Mock::VerifyAndClearExpectations(&h[0]);
+    testing::Mock::VerifyAndClearExpectations(&h[1]);
+}
+
+TEST_F(IoUringTest, HandlerReplacement)
+{
+    auto& sqe = ring.getSQE();
+    io_uring_prep_nop(&sqe);
+    ring.setHandler(sqe, &h[0]);
+
+    // Setting a new handler should cancel the old one
+    EXPECT_CALL(h[0], handleCQE(_));
+    ring.setHandler(sqe, &h[1]);
+    testing::Mock::VerifyAndClearExpectations(&h[0]);
+
+    // Setting a null handler should cancel the old one
+    EXPECT_CALL(h[1], handleCQE(_));
+    ring.setHandler(sqe, nullptr);
+    testing::Mock::VerifyAndClearExpectations(&h[1]);
+
+    // Set it back twice and make sure it isn't recognized idempotently
+    ring.setHandler(sqe, &h[1]);
+    ring.setHandler(sqe, &h[1]);
+
+    // Make sure it still works
+    ring.submit();
+    EXPECT_CALL(h[1], handleCQE(_));
+    ring.process();
+    testing::Mock::VerifyAndClearExpectations(&h[1]);
+}
+
+TEST_F(IoUringTest, EventFd)
+{
+    auto& efd = ring.getEventFd();
+
+    for (size_t i = 0; i < h.size(); ++i)
+    {
+        auto& sqe = ring.getSQE();
+        io_uring_prep_nop(&sqe);
+        ring.setHandler(sqe, &h[i]);
+        ring.submit();
+    }
+
+    // Our event fd should become ready
+    pollfd pfd;
+    pfd.fd = efd.get();
+    pfd.events = POLLIN;
+    ASSERT_EQ(1, poll(&pfd, 1, 100));
+
+    // Handle all of the outstanding requests
+    EXPECT_CALL(h[0], handleCQE(_));
+    EXPECT_CALL(h[1], handleCQE(_));
+    ring.processEvents();
+    testing::Mock::VerifyAndClearExpectations(&h[0]);
+    testing::Mock::VerifyAndClearExpectations(&h[1]);
+
+    // Our event fd should be empty
+    ASSERT_EQ(0, poll(&pfd, 1, 100));
+}
+
+TEST_F(IoUringTest, HandleCalledOnDestroy)
+{
+    auto& sqe = ring.getSQE();
+    io_uring_prep_nop(&sqe);
+    ring.setHandler(sqe, &h[0]);
+    EXPECT_CALL(h[0], handleCQE(_));
+}
+
+} // namespace stdplus
diff --git a/test/meson.build b/test/meson.build
index 8a2ae6c..b303032 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -44,6 +44,16 @@
   warning('Not testing file descriptor feature')
 endif
 
+if has_io_uring
+  gtests += [
+    'io_uring',
+  ]
+elif build_tests.enabled()
+  error('Not testing io_uring feature')
+else
+  warning('Not testing io_uring feature')
+endif
+
 if gtest.found() and gmock.found()
   foreach t : gtests
     test(t, executable(t.underscorify(), t + '.cpp',