From ac3d88fc043d628979675dc3ef99ba5e1b4d58ca Mon Sep 17 00:00:00 2001 From: Yuqian Yang Date: Thu, 4 Sep 2025 02:06:20 +0800 Subject: Add unix event loop. --- include/cru/base/Base.h | 1 + include/cru/base/platform/unix/EventLoop.h | 95 ++++++++++++++++++++++++++++++ include/cru/base/platform/unix/Timer.h | 43 -------------- src/base/Base.cpp | 1 + src/base/CMakeLists.txt | 2 +- src/base/platform/unix/EventLoop.cpp | 92 +++++++++++++++++++++++++++++ src/base/platform/unix/Timer.cpp | 5 -- test/base/CMakeLists.txt | 2 +- test/base/platform/unix/EventLoopTest.cpp | 24 ++++++++ test/base/platform/unix/TimerTest.cpp | 24 -------- 10 files changed, 215 insertions(+), 74 deletions(-) create mode 100644 include/cru/base/platform/unix/EventLoop.h delete mode 100644 include/cru/base/platform/unix/Timer.h create mode 100644 src/base/platform/unix/EventLoop.cpp delete mode 100644 src/base/platform/unix/Timer.cpp create mode 100644 test/base/platform/unix/EventLoopTest.cpp delete mode 100644 test/base/platform/unix/TimerTest.cpp diff --git a/include/cru/base/Base.h b/include/cru/base/Base.h index fd409360..f8f8c8c0 100644 --- a/include/cru/base/Base.h +++ b/include/cru/base/Base.h @@ -93,6 +93,7 @@ struct CRU_BASE_API Interface { }; [[noreturn]] void CRU_BASE_API UnreachableCode(); +[[noreturn]] void CRU_BASE_API NotImplemented(); using Index = std::ptrdiff_t; diff --git a/include/cru/base/platform/unix/EventLoop.h b/include/cru/base/platform/unix/EventLoop.h new file mode 100644 index 00000000..bc2bcbf8 --- /dev/null +++ b/include/cru/base/platform/unix/EventLoop.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#if !defined(__unix) && !defined(__APPLE__) +#error "This file can only be included on unix." +#endif + +#include "../../Base.h" +#include "../../Exception.h" +#include "UnixFile.h" + +#include +#include +#include + +namespace cru::platform::unix { +class UnixTimerFile : public Object2 { + public: + template + explicit UnixTimerFile(std::chrono::duration time) { + auto fds = OpenUniDirectionalPipe(); + this->read_fd_ = std::move(fds.read); + this->write_fd_ = std::move(fds.write); + + this->thread_ = std::thread([this, time] { + std::this_thread::sleep_for(time); + constexpr auto buffer = ""; + auto written = ::write(this->write_fd_, buffer, 1); + if (written != 1) { + throw Exception( + "Failed to write to pipe in UnixTimerFile thread at timeout."); + } + }); + this->thread_.detach(); + } + + int GetReadFd() const; + + private: + UnixFileDescriptor write_fd_; + UnixFileDescriptor read_fd_; + std::thread thread_; +}; + +class UnixEventLoop : public Object2 { + public: + UnixEventLoop(); + + int Run(); + void RequestQuit(int exit_code = 0); + + int SetTimer(std::function action, std::chrono::milliseconds timeout, + bool repeat); + + int SetImmediate(std::function action) { + return this->SetTimer(std::move(action), std::chrono::milliseconds::zero(), + false); + } + + int SetTimeout(std::function action, + std::chrono::milliseconds timeout) { + return this->SetTimer(std::move(action), std::move(timeout), false); + } + + int SetInterval(std::function action, + std::chrono::milliseconds interval) { + return this->SetTimer(std::move(action), std::move(interval), true); + } + + private: + struct TimerData { + int id; + std::chrono::milliseconds original_timeout; + std::chrono::milliseconds timeout; + bool repeat; + std::function action; + + TimerData(int id, std::chrono::milliseconds timeout, bool repeat, + std::function action) + : id(id), + original_timeout(timeout), + timeout(timeout), + repeat(repeat), + action(std::move(action)) {} + }; + + std::thread::id running_thread_; + + std::atomic_int timer_tag_; + std::vector timers_; + + std::optional exit_code_; +}; + +} // namespace cru::platform::unix diff --git a/include/cru/base/platform/unix/Timer.h b/include/cru/base/platform/unix/Timer.h deleted file mode 100644 index 17abd1cc..00000000 --- a/include/cru/base/platform/unix/Timer.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#if !defined(__unix) && !defined(__APPLE__) -#error "This file can only be included on unix." -#endif - -#include "../../Base.h" -#include "../../Exception.h" -#include "UnixFile.h" - -#include -#include -#include - -namespace cru::platform::unix { -class UnixTimerFile : public Object2 { - public: - template - explicit UnixTimerFile(std::chrono::duration time) { - auto fds = OpenUniDirectionalPipe(); - this->read_fd_ = std::move(fds.read); - this->write_fd_ = std::move(fds.write); - - this->thread_ = std::thread([this, time] { - std::this_thread::sleep_for(time); - constexpr auto buffer = ""; - auto written = ::write(this->write_fd_, buffer, 1); - if (written != 1) { - throw Exception( - "Failed to write to pipe in UnixTimerFile thread at timeout."); - } - }); - this->thread_.detach(); - } - - int GetReadFd() const; - - private: - UnixFileDescriptor write_fd_; - UnixFileDescriptor read_fd_; - std::thread thread_; -}; -} // namespace cru::platform::unix diff --git a/src/base/Base.cpp b/src/base/Base.cpp index 1704f8a9..c6d9ac07 100644 --- a/src/base/Base.cpp +++ b/src/base/Base.cpp @@ -4,4 +4,5 @@ namespace cru { void UnreachableCode() { std::terminate(); } +void NotImplemented() { std::terminate(); } } // namespace cru diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 300898af..7e7b0127 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -24,8 +24,8 @@ target_compile_definitions(CruBase PUBLIC $<$:CRU_DEBUG>) if (UNIX AND NOT EMSCRIPTEN) target_sources(CruBase PRIVATE + platform/unix/EventLoop.cpp platform/unix/PosixSpawnSubProcess.cpp - platform/unix/Timer.cpp platform/unix/UnixFile.cpp platform/unix/UnixFileStream.cpp ) diff --git a/src/base/platform/unix/EventLoop.cpp b/src/base/platform/unix/EventLoop.cpp new file mode 100644 index 00000000..337d8a44 --- /dev/null +++ b/src/base/platform/unix/EventLoop.cpp @@ -0,0 +1,92 @@ +#include "cru/base/platform/unix/EventLoop.h" +#include "cru/base/Exception.h" + +#include +#include +#include +#include + +namespace cru::platform::unix { +int UnixTimerFile::GetReadFd() const { return this->read_fd_; } + +UnixEventLoop::UnixEventLoop() : timer_tag_(1) {} + +int UnixEventLoop::Run() { + running_thread_ = std::this_thread::get_id(); + + pollfd poll_fds[1]; + + while (!exit_code_) { + int poll_timeout = -1; + + auto iter = std::ranges::find_if(timers_, [](const TimerData &timer) { + return timer.timeout <= std::chrono::milliseconds::zero(); + }); + if (iter != timers_.end()) { + auto &timer = *iter; + if (timer.repeat) { + while (timer.timeout <= std::chrono::milliseconds::zero()) { + timer.timeout += timer.original_timeout; + timer.action(); + } + } else { + auto action = timer.action; + timers_.erase(iter); + action(); + } + continue; + } + + if (!timers_.empty()) { + poll_timeout = + std::ranges::min_element(timers_, [](const TimerData &left, + const TimerData &right) { + return left.timeout < right.timeout; + })->timeout.count(); + } + + auto start = std::chrono::steady_clock::now(); + + ::poll(poll_fds, sizeof poll_fds / sizeof *poll_fds, poll_timeout); + + // TODO: A Big Implement to handle X events. + + auto end = std::chrono::steady_clock::now(); + auto time = + std::chrono::duration_cast(end - start); + + for (auto &timer : timers_) { + timer.timeout -= time; + } + } + + return exit_code_.value(); +} + +void UnixEventLoop::RequestQuit(int exit_code) {} + +int UnixEventLoop::SetTimer(std::function action, + std::chrono::milliseconds timeout, bool repeat) { + if (repeat) { + if (timeout <= std::chrono::milliseconds::zero()) { + throw Exception("Interval must be bigger than 0."); + } + } else { + if (timeout < std::chrono::milliseconds::zero()) { + throw Exception("Timeout must be at least 0."); + } + } + + auto tag = timer_tag_++; + + if (std::this_thread::get_id() == running_thread_) { + timers_.push_back( + TimerData(tag, std::move(timeout), repeat, std::move(action))); + } else { + // TODO: Implement + } + + return tag; +} + +} // namespace cru::platform::unix diff --git a/src/base/platform/unix/Timer.cpp b/src/base/platform/unix/Timer.cpp deleted file mode 100644 index 25264dc8..00000000 --- a/src/base/platform/unix/Timer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "cru/base/platform/unix/Timer.h" - -namespace cru::platform::unix { -int UnixTimerFile::GetReadFd() const { return this->read_fd_; } -} // namespace cru::platform::unix diff --git a/test/base/CMakeLists.txt b/test/base/CMakeLists.txt index 5abba240..95313ccb 100644 --- a/test/base/CMakeLists.txt +++ b/test/base/CMakeLists.txt @@ -25,7 +25,7 @@ target_compile_definitions(CruBaseTest PRIVATE if (UNIX AND NOT EMSCRIPTEN) target_sources(CruBaseTest PRIVATE - platform/unix/TimerTest.cpp + platform/unix/EventLoopTest.cpp platform/unix/UnixFileTest.cpp platform/unix/UnixFileStreamTest.cpp ) diff --git a/test/base/platform/unix/EventLoopTest.cpp b/test/base/platform/unix/EventLoopTest.cpp new file mode 100644 index 00000000..f5936b2a --- /dev/null +++ b/test/base/platform/unix/EventLoopTest.cpp @@ -0,0 +1,24 @@ +#include "cru/base/platform/unix/EventLoop.h" + +#include + +#include +#include + +TEST_CASE("UnixTimerFile Work", "[unix][time]") { + using namespace cru; + using namespace cru::platform::unix; + + auto test_miliseconds = 300; + auto test_duration = std::chrono::milliseconds(test_miliseconds); + auto start = std::chrono::steady_clock::now(); + REQUIRE((std::chrono::steady_clock::now() - start) < test_duration); + + UnixTimerFile timer(test_duration); + + struct pollfd fds[1]; + fds[0].fd = timer.GetReadFd(); + fds[0].events = POLLIN; + REQUIRE(::poll(fds, 1, test_miliseconds * 2) == 1); + REQUIRE((std::chrono::steady_clock::now() - start) > test_duration); +} diff --git a/test/base/platform/unix/TimerTest.cpp b/test/base/platform/unix/TimerTest.cpp deleted file mode 100644 index dc3cc1ba..00000000 --- a/test/base/platform/unix/TimerTest.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "cru/base/platform/unix/Timer.h" - -#include - -#include -#include - -TEST_CASE("UnixTimerFile Work", "[unix][time]") { - using namespace cru; - using namespace cru::platform::unix; - - auto test_miliseconds = 300; - auto test_duration = std::chrono::milliseconds(test_miliseconds); - auto start = std::chrono::steady_clock::now(); - REQUIRE((std::chrono::steady_clock::now() - start) < test_duration); - - UnixTimerFile timer(test_duration); - - struct pollfd fds[1]; - fds[0].fd = timer.GetReadFd(); - fds[0].events = POLLIN; - REQUIRE(::poll(fds, 1, test_miliseconds * 2) == 1); - REQUIRE((std::chrono::steady_clock::now() - start) > test_duration); -} -- cgit v1.2.3