Add C++ wrapper for liburing

Test: th
Bug: 250637970
Change-Id: I6303617ad02c6be2f2a6d3b28cd81f10406302bb
diff --git a/.gitignore b/.gitignore
index 84f29a3..f51c633 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,7 @@
 /update_engine_unittests
 *.pyc
 .vscode
+.git
+.cache
+.xmake
+
diff --git a/liburing_cpp/.gitignore b/liburing_cpp/.gitignore
new file mode 100644
index 0000000..e52e042
--- /dev/null
+++ b/liburing_cpp/.gitignore
@@ -0,0 +1,6 @@
+build
+.xmake
+.cache
+.vscode
+compile_commands.json
+
diff --git a/liburing_cpp/Android.bp b/liburing_cpp/Android.bp
new file mode 100644
index 0000000..2296f7e
--- /dev/null
+++ b/liburing_cpp/Android.bp
@@ -0,0 +1,31 @@
+
+cc_library {
+	name: "liburing_cpp",
+	host_supported: true,
+	recovery_available: true,
+	srcs: [
+		"src/IoUring.cpp",
+		"src/IoUringSQE.cpp",
+	],
+	static_libs: [
+		"liburing",
+	],
+	include_dirs: ["bionic/libc/kernel"],
+	export_include_dirs: [
+		"include",
+	],
+}
+
+
+cc_test_host {
+	name: "liburing_cpp_tests",
+	srcs: [
+		"tests/BasicTests.cpp",
+		"tests/main.cpp",
+	],
+	static_libs: [
+		"libgtest",
+		"liburing",
+		"liburing_cpp",
+	],
+}
\ No newline at end of file
diff --git a/liburing_cpp/README.md b/liburing_cpp/README.md
new file mode 100644
index 0000000..1efabae
--- /dev/null
+++ b/liburing_cpp/README.md
@@ -0,0 +1,26 @@
+# liburing_cpp
+
+This project provides a idiomatic C++ wrapper for liburing.
+
+## Source Code Headers
+
+Every file containing source code must include copyright and license
+information. This includes any JS/CSS files that you might be serving out to
+browsers. (This is to help well-intentioned people avoid accidental copying that
+doesn't comply with the license.)
+
+Apache header:
+
+    Copyright 2022 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        https://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
diff --git a/liburing_cpp/include/liburing_cpp/BufferView.h b/liburing_cpp/include/liburing_cpp/BufferView.h
new file mode 100644
index 0000000..eff93df
--- /dev/null
+++ b/liburing_cpp/include/liburing_cpp/BufferView.h
@@ -0,0 +1,50 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef __BUFFER_VIEW_CPP_H
+#define __BUFFER_VIEW_CPP_H
+
+#include <stddef.h>
+#include <assert.h>
+
+// non-owning reference to a contiguous memory region. Similar to
+// std::string_view, but allows you to modify underlying memory region.
+template <typename T>
+struct BufferView {
+  constexpr BufferView() = default;
+  constexpr BufferView(T* data, size_t size) : ptr_(data), size_(size) {}
+
+  T* data() { return ptr_; }
+  size_t size() const { return size_; }
+  T* begin() { return ptr_; };
+  T* end() { return ptr_ + size_; }
+  bool empty() const { return size_ == 0; }
+
+  T& operator[](const size_t idx) {
+    assert(idx < size_);
+    return ptr_[idx];
+  }
+  const T& operator[](const size_t idx) const {
+    assert(idx < size_);
+    return ptr_[idx];
+  }
+
+ private:
+  T* ptr_{};
+  size_t size_{};
+};
+
+#endif
diff --git a/liburing_cpp/include/liburing_cpp/IoUring.h b/liburing_cpp/include/liburing_cpp/IoUring.h
new file mode 100644
index 0000000..324fb10
--- /dev/null
+++ b/liburing_cpp/include/liburing_cpp/IoUring.h
@@ -0,0 +1,79 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef __IO_URING_CPP_H
+#define __IO_URING_CPP_H
+
+#include <errno.h>
+#include <string.h>
+
+#include <sys/uio.h>
+
+#include <algorithm>
+#include <memory>
+#include <optional>
+#include <variant>
+
+#include "IoUringCQE.h"
+#include "IoUringSQE.h"
+
+namespace io_uring_cpp {
+
+template <typename Err, typename Res>
+struct [[nodiscard]] Result : public std::variant<Err, Res> {
+  constexpr bool IsOk() const { return std::holds_alternative<Res>(*this); }
+  constexpr bool IsErr() const { return std::holds_alternative<Err>(*this); }
+  constexpr Err GetError() const { return std::get<Err>(*this); }
+  constexpr const Res& GetResult() const& { return std::get<Res>(*this); }
+  constexpr Res&& GetResult() && { return std::get<Res>(*this); }
+};
+
+class IoUringInterface {
+ public:
+  virtual ~IoUringInterface() {}
+  // Registration helpers
+  // Register a fixed set of buffers to kernel.
+  virtual Errno RegisterBuffers(const struct iovec* iovecs,
+                                size_t iovec_size) = 0;
+  virtual Errno UnregisterBuffers() = 0;
+
+  // Register a set of file descriptors to kernel.
+  virtual Errno RegisterFiles(const int* files, size_t files_size) = 0;
+  virtual Errno UnregisterFiles() = 0;
+  // Append a submission entry into this io_uring. This does not submit the
+  // operation to the kernel. For that, call |IoUringInterface::Submit()|
+  virtual IoUringSQE PrepRead(int fd, void *buf, unsigned nbytes,
+                              uint64_t offset) = 0;
+  // Caller is responsible for making sure the input memory is available until
+  // this write operation completes.
+  virtual IoUringSQE PrepWrite(int fd, const void *buf, unsigned nbytes,
+                               uint64_t offset) = 0;
+
+  // Ring operations
+  virtual IoUringSubmitResult Submit() = 0;
+  // Submit and block until |completions| number of CQEs are available
+  virtual IoUringSubmitResult SubmitAndWait(size_t completions) = 0;
+  virtual Result<Errno, IoUringCQE> PopCQE() = 0;
+  virtual Result<Errno, std::vector<IoUringCQE>> PopCQE(unsigned int count) = 0;
+  virtual Result<Errno, IoUringCQE> PeekCQE() = 0;
+
+  static std::unique_ptr<IoUringInterface> CreateLinuxIoUring(int queue_depth,
+                                                              int flags);
+};
+
+}  // namespace io_uring_cpp
+
+#endif
diff --git a/liburing_cpp/include/liburing_cpp/IoUringCQE.h b/liburing_cpp/include/liburing_cpp/IoUringCQE.h
new file mode 100644
index 0000000..e75731e
--- /dev/null
+++ b/liburing_cpp/include/liburing_cpp/IoUringCQE.h
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef __IO_URING_CQE_CPP_H
+#define __IO_URING_CQE_CPP_H
+
+#include <stdint.h>
+
+#include <type_traits>
+
+namespace io_uring_cpp {
+
+struct IoUringCQE {
+  int32_t res;
+  uint32_t flags;
+  template <typename T>
+  T GetData() const {
+    static_assert(
+        std::is_trivially_copy_constructible_v<T>,
+        "Only trivially copiable types can be passed for io_uring data");
+    static_assert(sizeof(T) <= 8,
+                  "io_uring SQE's data field has size of 8 bytes, can't pass "
+                  "data larger than that.");
+    return *reinterpret_cast<const T*>(&userdata);
+  }
+
+  constexpr IoUringCQE(int32_t res, uint32_t flags, uint64_t userdata)
+      : res(res), flags(flags), userdata(userdata) {}
+
+ private:
+  uint64_t userdata;
+};
+
+}  // namespace io_uring_cpp
+
+#endif
diff --git a/liburing_cpp/include/liburing_cpp/IoUringSQE.h b/liburing_cpp/include/liburing_cpp/IoUringSQE.h
new file mode 100644
index 0000000..ca2b365
--- /dev/null
+++ b/liburing_cpp/include/liburing_cpp/IoUringSQE.h
@@ -0,0 +1,79 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include <errno.h>
+#include <string.h>
+
+#include <algorithm>
+#include <iostream>
+#include <numeric>
+#include <type_traits>
+#include <vector>
+
+#ifndef __IO_URING_SQE_CPP_H
+#define __IO_URING_SQE_CPP_H
+
+namespace io_uring_cpp {
+
+struct [[nodiscard]] IoUringSubmitResult {
+  constexpr bool IsOk() const { return ret > 0; }
+  const char *ErrMsg() const {
+    if (IsOk()) {
+      return nullptr;
+    }
+    return strerror(-ret);
+  }
+  constexpr auto ErrCode() const { return std::min(ret, 0); }
+  constexpr auto EntriesSubmitted() const { return std::max(ret, 0); }
+
+  int ret;
+};
+
+struct [[nodiscard]] Errno {
+  constexpr Errno(int ret) : error_code(ret) {
+    error_code = std::abs(error_code);
+  }
+  int error_code;
+  constexpr int ErrCode() { return error_code; }
+  const char *ErrMsg();
+  constexpr bool IsOk() const { return error_code == 0; }
+};
+
+std::ostream &operator<<(std::ostream &, Errno err);
+
+struct [[nodiscard]] IoUringSQE {
+  constexpr IoUringSQE(void *p) : sqe(p) {}
+  IoUringSQE &SetFlags(unsigned int flags);
+  template <typename T>
+  IoUringSQE &SetData(const T &data) {
+    static_assert(
+        std::is_trivially_copy_constructible_v<T>,
+        "Only trivially copiable types can be passed for io_uring data");
+    static_assert(sizeof(T) <= 8,
+                  "io_uring SQE's data field has size of 8 bytes, can't pass "
+                  "data larger than that.");
+    return SetData(*reinterpret_cast<const uint64_t*>(&data));
+  }
+  IoUringSQE& SetData(uint64_t data);
+
+  constexpr bool IsOk() const { return sqe != nullptr; }
+
+ private:
+  void *sqe;
+};
+
+}  // namespace io_uring_cpp
+
+#endif
diff --git a/liburing_cpp/src/IoUring.cpp b/liburing_cpp/src/IoUring.cpp
new file mode 100644
index 0000000..a5fe76f
--- /dev/null
+++ b/liburing_cpp/src/IoUring.cpp
@@ -0,0 +1,187 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include <asm-generic/errno-base.h>
+#include <liburing_cpp/IoUring.h>
+#include <string.h>
+
+#include <algorithm>
+#include <iostream>
+#include <memory>
+
+#include "liburing.h"
+#include "liburing_cpp/IoUringCQE.h"
+
+namespace io_uring_cpp {
+
+template <typename T>
+bool IsZeroInitialized(const T& val) {
+  auto begin = reinterpret_cast<const char*>(&val);
+  auto end = begin + sizeof(val);
+  return std::all_of(begin, end, [](const auto& a) { return a == 0; });
+}
+
+class IoUring final : public IoUringInterface {
+ public:
+  ~IoUring() override {
+    if (!IsZeroInitialized(ring)) {
+      if (buffer_registered_) {
+        UnregisterBuffers();
+      }
+      if (files_registered_) {
+        UnregisterFiles();
+      }
+      io_uring_queue_exit(&ring);
+    }
+  }
+  IoUring(const IoUring&) = delete;
+  IoUring(IoUring&& rhs) {
+    ring = rhs.ring;
+    memset(&rhs.ring, 0, sizeof(rhs.ring));
+  }
+  IoUring& operator=(IoUring&& rhs) {
+    std::swap(ring, rhs.ring);
+    return *this;
+  }
+  Errno RegisterBuffers(const struct iovec* iovecs,
+                        size_t iovec_size) override {
+    const auto ret =
+        Errno(io_uring_register_buffers(&ring, iovecs, iovec_size));
+    buffer_registered_ = ret.IsOk();
+    return ret;
+  }
+
+  Errno UnregisterBuffers() override {
+    const auto ret = Errno(io_uring_unregister_buffers(&ring));
+    buffer_registered_ = !ret.IsOk();
+    return ret;
+  }
+
+  Errno RegisterFiles(const int* files, size_t files_size) override {
+    const auto ret = Errno(io_uring_register_files(&ring, files, files_size));
+    files_registered_ = ret.IsOk();
+    return ret;
+  }
+
+  Errno UnregisterFiles() {
+    const auto ret = Errno(io_uring_unregister_files(&ring));
+    files_registered_ = !ret.IsOk();
+    return ret;
+  }
+
+  IoUringSQE PrepRead(int fd, void* buf, unsigned nbytes,
+                      uint64_t offset) override {
+    auto sqe = io_uring_get_sqe(&ring);
+    if (sqe == nullptr) {
+      return IoUringSQE{nullptr};
+    }
+    io_uring_prep_read(sqe, fd, buf, nbytes, offset);
+    return IoUringSQE{static_cast<void*>(sqe)};
+  }
+  IoUringSQE PrepWrite(int fd, const void* buf, unsigned nbytes,
+                       uint64_t offset) override {
+    auto sqe = io_uring_get_sqe(&ring);
+    if (sqe == nullptr) {
+      return IoUringSQE{nullptr};
+    }
+    io_uring_prep_write(sqe, fd, buf, nbytes, offset);
+    return IoUringSQE{static_cast<void*>(sqe)};
+  }
+  IoUringSubmitResult Submit() override {
+    return IoUringSubmitResult{io_uring_submit(&ring)};
+  }
+
+  IoUringSubmitResult SubmitAndWait(size_t completions) override {
+    return IoUringSubmitResult{io_uring_submit_and_wait(&ring, completions)};
+  }
+
+  Result<Errno, std::vector<IoUringCQE>> PopCQE(
+      const unsigned int count) override {
+    std::vector<io_uring_cqe*> cqe_ptrs;
+    cqe_ptrs.resize(count);
+    const auto ret = io_uring_wait_cqe_nr(&ring, cqe_ptrs.data(), count);
+    if (ret != 0) {
+      return {Errno(ret)};
+    }
+    const auto filled = io_uring_peek_batch_cqe(&ring, cqe_ptrs.data(), count);
+    if (filled != count) {
+      return {Errno(EAGAIN)};
+    }
+    std::vector<IoUringCQE> cqes;
+    cqes.reserve(count);
+    for (const auto& cqe : cqe_ptrs) {
+      if (cqe == nullptr) {
+        return {Errno(EAGAIN)};
+      }
+      cqes.push_back(IoUringCQE(cqe->res, cqe->flags, cqe->user_data));
+      io_uring_cqe_seen(&ring, cqe);
+    }
+    return {cqes};
+  }
+
+  Result<Errno, IoUringCQE> PopCQE() override {
+    struct io_uring_cqe* ptr{};
+    const auto ret = io_uring_wait_cqe(&ring, &ptr);
+    if (ret != 0) {
+      return {Errno(ret)};
+    }
+    const auto cqe = IoUringCQE(ptr->res, ptr->flags, ptr->user_data);
+    io_uring_cqe_seen(&ring, ptr);
+    return {cqe};
+  }
+
+  Result<Errno, IoUringCQE> PeekCQE() override {
+    struct io_uring_cqe* ptr{};
+    const auto ret = io_uring_peek_cqe(&ring, &ptr);
+    if (ret != 0) {
+      return {Errno(ret)};
+    }
+    return {IoUringCQE(ptr->res, ptr->flags, ptr->user_data)};
+  }
+
+  IoUring(struct io_uring r) : ring(r) {}
+
+ private:
+  struct io_uring ring {};
+  bool buffer_registered_ = false;
+  bool files_registered_ = false;
+  std::atomic<size_t> request_id_{};
+};
+
+const char* Errno::ErrMsg() {
+  if (error_code == 0) {
+    return nullptr;
+  }
+  return strerror(error_code);
+}
+
+std::ostream& operator<<(std::ostream& out, Errno err) {
+  out << err.ErrCode() << ", " << err.ErrMsg();
+  return out;
+}
+
+std::unique_ptr<IoUringInterface> IoUringInterface::CreateLinuxIoUring(
+    int queue_depth, int flags) {
+  struct io_uring ring {};
+  const auto err = io_uring_queue_init(queue_depth, &ring, flags);
+  if (err) {
+    errno = -err;
+    return {};
+  }
+  return std::unique_ptr<IoUringInterface>(new IoUring(ring));
+}
+
+}  // namespace io_uring_cpp
diff --git a/liburing_cpp/src/IoUringSQE.cpp b/liburing_cpp/src/IoUringSQE.cpp
new file mode 100644
index 0000000..fc3d3ea
--- /dev/null
+++ b/liburing_cpp/src/IoUringSQE.cpp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include <liburing_cpp/IoUringSQE.h>
+
+#include <cstdint>
+
+#include "liburing.h"
+
+namespace io_uring_cpp {
+
+IoUringSQE &IoUringSQE::SetFlags(unsigned int flags) {
+  if (IsOk()) {
+    ::io_uring_sqe_set_flags(static_cast<struct io_uring_sqe *>(sqe), flags);
+  }
+  return *this;
+}
+
+IoUringSQE &IoUringSQE::SetData(uint64_t data) {
+  if (IsOk()) {
+    ::io_uring_sqe_set_data(static_cast<struct io_uring_sqe *>(sqe),
+                            reinterpret_cast<void *>(data));
+  }
+  return *this;
+}
+
+}  // namespace io_uring_cpp
diff --git a/liburing_cpp/tests/BasicTests.cpp b/liburing_cpp/tests/BasicTests.cpp
new file mode 100644
index 0000000..81288f6
--- /dev/null
+++ b/liburing_cpp/tests/BasicTests.cpp
@@ -0,0 +1,194 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include <gtest/gtest.h>
+#include <liburing_cpp/IoUring.h>
+
+#include <linux/fs.h>
+#include <stdio.h>
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <vector>
+
+using namespace io_uring_cpp;
+
+class IoUringTest : public ::testing::Test {
+ public:
+  IoUringTest() { fp = tmpfile(); }
+  ~IoUringTest() {
+    if (fp) {
+      fclose(fp);
+    }
+  }
+  void SetUp() override {
+    struct utsname buffer {};
+
+    ASSERT_EQ(uname(&buffer), 0)
+        << strerror(errno) << "Failed to get kernel version number";
+    int major = 0;
+    int minor = 0;
+    const auto matched = sscanf(buffer.release, "%d.%d", &major, &minor);
+    ASSERT_EQ(matched, 2) << "Unexpected kernel version format: "
+                          << buffer.release;
+
+    if (major < 5 || (major == 5 && minor < 6)) {
+      GTEST_SKIP() << "Kernel version does not support io_uring "
+                   << buffer.release;
+      return;
+    }
+
+    ring = IoUringInterface::CreateLinuxIoUring(4096, 0);
+    ASSERT_NE(ring, nullptr);
+  }
+  void Write(int fd, const void* data, const size_t len) {
+    const auto buf = static_cast<const char*>(data);
+    constexpr size_t IO_BATCH_SIZE = 4096;
+    size_t i = 0;
+    for (i = 0; i < len; i += IO_BATCH_SIZE) {
+      const auto sqe = ring->PrepWrite(fd, buf + i, IO_BATCH_SIZE, i);
+      ASSERT_TRUE(sqe.IsOk());
+    }
+    const auto bytes_remaining = len - i;
+    if (bytes_remaining) {
+      ASSERT_TRUE(ring->PrepWrite(fd, buf + i, bytes_remaining, i).IsOk());
+    }
+    const auto ret = ring->Submit();
+    ASSERT_TRUE(ret.IsOk()) << ret.ErrMsg();
+    for (size_t i = (len + IO_BATCH_SIZE - 1) / IO_BATCH_SIZE; i > 0; i--) {
+      const auto cqe = ring->PopCQE();
+      ASSERT_TRUE(cqe.IsOk());
+      ASSERT_GT(cqe.GetResult().res, 0);
+    }
+  }
+  std::unique_ptr<IoUringInterface> ring;
+  FILE* fp = nullptr;
+};
+
+TEST_F(IoUringTest, SmallRead) {
+  int fd = open("/proc/self/maps", O_RDONLY);
+  std::array<char, 1024> buf{};
+  const auto sqe = ring->PrepRead(fd, buf.data(), buf.size(), 0);
+  ASSERT_TRUE(sqe.IsOk()) << "Submission Queue is full!";
+  const auto ret = ring->Submit();
+  ASSERT_TRUE(ret.IsOk()) << ret.ErrMsg();
+  const auto cqe = ring->PopCQE();
+  ASSERT_TRUE(cqe.IsOk()) << cqe.GetError();
+  ASSERT_GT(cqe.GetResult().res, 0);
+}
+
+TEST_F(IoUringTest, SmallWrite) {
+  auto fp = tmpfile();
+  int fd = fileno(fp);
+  std::string buffer(256, 'A');
+  const auto sqe = ring->PrepWrite(fd, buffer.data(), buffer.size(), 0);
+  ASSERT_TRUE(sqe.IsOk()) << "Submission Queue is full!";
+  const auto ret = ring->Submit();
+  ASSERT_TRUE(ret.IsOk()) << ret.ErrMsg();
+  const auto cqe = ring->PopCQE();
+  ASSERT_TRUE(cqe.IsOk()) << cqe.GetError();
+
+  const auto bytes_read = pread(fd, buffer.data(), buffer.size(), 0);
+
+  ASSERT_EQ(bytes_read, buffer.size());
+
+  ASSERT_TRUE(std::all_of(buffer.begin(), buffer.end(), [](const auto& a) {
+    return a == 'A';
+  })) << buffer;
+  fclose(fp);
+}
+
+TEST_F(IoUringTest, ChunkedWrite) {
+  int fd = fileno(fp);
+  std::string buffer(16 * 1024 * 1024, 'A');
+  ASSERT_NO_FATAL_FAILURE(Write(fd, buffer.data(), buffer.size()));
+
+  const auto bytes_read = pread(fd, buffer.data(), buffer.size(), 0);
+
+  ASSERT_EQ(bytes_read, buffer.size());
+
+  ASSERT_TRUE(std::all_of(buffer.begin(), buffer.end(), [](const auto& a) {
+    return a == 'A';
+  })) << buffer;
+}
+
+// Page size doesn't really matter. We can replace 4096 with any value.
+static constexpr size_t kBlockSize = 4096;
+constexpr std::array<unsigned char, 4096> GetArbitraryPageData() {
+  std::array<unsigned char, kBlockSize> arr{};
+  int i = 0;
+  for (auto& a : arr) {
+    a = i++;
+  }
+  return arr;
+}
+
+void WriteTestData(int fd, const size_t offset, const size_t size) {
+  ASSERT_EQ(size % kBlockSize, 0);
+  static const auto data = GetArbitraryPageData();
+  size_t bytes_written = 0;
+  size_t cur_offset = offset;
+  while (bytes_written < size) {
+    const auto ret = pwrite(fd, data.data(), kBlockSize, cur_offset);
+    ASSERT_GT(ret, 0) << "Failed to pwrite " << strerror(errno);
+    bytes_written += ret;
+    cur_offset += ret;
+  }
+}
+
+TEST_F(IoUringTest, ExtentRead) {
+  const int fd = fileno(fp);
+  ASSERT_NO_FATAL_FAILURE(WriteTestData(fd, kBlockSize * 3, kBlockSize));
+  ASSERT_NO_FATAL_FAILURE(WriteTestData(fd, kBlockSize * 5, kBlockSize));
+  ASSERT_NO_FATAL_FAILURE(WriteTestData(fd, kBlockSize * 8, kBlockSize));
+  ASSERT_NO_FATAL_FAILURE(WriteTestData(fd, kBlockSize * 13, kBlockSize));
+  fsync(fd);
+
+  std::vector<unsigned char> data;
+  data.resize(kBlockSize * 4);
+
+  ASSERT_TRUE(
+      ring->PrepRead(fd, data.data(), kBlockSize, 3 * kBlockSize).IsOk());
+  ASSERT_TRUE(
+      ring->PrepRead(fd, data.data() + kBlockSize, kBlockSize, 5 * kBlockSize)
+          .IsOk());
+  ASSERT_TRUE(
+      ring->PrepRead(
+              fd, data.data() + kBlockSize * 2, kBlockSize, 8 * kBlockSize)
+          .IsOk());
+  ASSERT_TRUE(
+      ring->PrepRead(
+              fd, data.data() + kBlockSize * 3, kBlockSize, 13 * kBlockSize)
+          .IsOk());
+  ring->SubmitAndWait(4);
+  const auto cqes = ring->PopCQE(4);
+  if (cqes.IsErr()) {
+    FAIL() << cqes.GetError().ErrMsg();
+    return;
+  }
+  for (const auto& cqe : cqes.GetResult()) {
+    ASSERT_GT(cqe.res, 0);
+  }
+  for (int i = 0; i < data.size(); ++i) {
+    ASSERT_EQ(data[i], i % 256);
+  }
+}
\ No newline at end of file
diff --git a/liburing_cpp/tests/main.cpp b/liburing_cpp/tests/main.cpp
new file mode 100644
index 0000000..9874bf2
--- /dev/null
+++ b/liburing_cpp/tests/main.cpp
@@ -0,0 +1,22 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include <gtest/gtest.h>
+
+int main(int argc, char *argv[]) {
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
\ No newline at end of file
diff --git a/liburing_cpp/xmake.lua b/liburing_cpp/xmake.lua
new file mode 100644
index 0000000..93e52ae
--- /dev/null
+++ b/liburing_cpp/xmake.lua
@@ -0,0 +1,19 @@
+add_requires("liburing", "gtest")
+
+target("liburing_cpp")
+  set_kind("static")
+  add_files("src/*.cpp")
+  add_packages("liburing")
+  set_languages("c++17")
+  add_includedirs("include", {public = true})
+  add_cxflags("-g")
+
+
+target("liburing_cpp_tests")
+  set_kind("binary")
+  add_files("tests/*.cpp")
+  set_languages("c++17")
+  add_deps("liburing_cpp")
+  add_packages("gtest", "liburing")
+  add_cxflags("-g")
+