From 7e24ee02f2a1cbf374b816f8aec37ddee564f919 Mon Sep 17 00:00:00 2001 From: HikariTish <809456206@qq.com> Date: Tue, 4 Aug 2026 23:17:09 +0800 Subject: [PATCH 1/5] feat: implement lazy single-consumer Task --- .idea/.gitignore | 10 + .idea/cmp.iml | 2 + .idea/editor.xml | 350 +++++++++++++++++++++++++++++++++++ .idea/modules.xml | 8 + .idea/vcs.xml | 6 + README.md | 51 +++-- README.zh.hant.md | 44 +++-- README.zh.md | 44 +++-- docs/architecture.md | 75 ++++++-- docs/architecture.zh.hant.md | 67 +++++-- docs/architecture.zh.md | 67 +++++-- examples/basic/src/main.cpp | 44 +++++ src/cmp.cppm | 259 ++++++++++++++++++++++++++ tests/cmp_test.cpp | 310 ++++++++++++++++++++++++++++++- 14 files changed, 1238 insertions(+), 99 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/cmp.iml create mode 100644 .idea/editor.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..7f050be --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cmp.iml b/.idea/cmp.iml new file mode 100644 index 0000000..962e49f --- /dev/null +++ b/.idea/cmp.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..4abd7d2 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,350 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c19669f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c8397c9 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 8a7467d..bf1e439 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP is currently in its **bootstrap stage**. The package exports the root module -> `mcpplibs.cmp`, but it does not provide coroutine runtime APIs yet. +> CMP now provides its first coroutine primitive: a lazy, single-consumer `Task` / `Task`. +> Scheduling, cancellation, timers, asynchronous I/O, and a public root runner are not implemented. CMP is being built as a modern coroutine runtime and library on standard stackless C++ coroutines. The intended direction is an explicit `co_await` model that can grow, in small @@ -71,21 +71,43 @@ cd examples/basic mcpp run ``` -The example exits successfully without output. Its purpose is to prove that an independent mcpp -package can resolve the path dependency and import `mcpplibs.cmp`. +The example prints `Coroutine result: 42` from inside a `Task` coroutine and exits +successfully. It proves that an independent mcpp package can resolve the path dependency, import +`mcpplibs.cmp`, compose Tasks, and execute the synchronous chain. -## Current Module +## Current Task API ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -The module deliberately has no public declarations during bootstrap. Future public APIs will use -the namespace `mcpplibs::cmp`. +`Task` is lazy: calling `answer()` creates a suspended coroutine. It starts when consumed by +`co_await`. A Task is move-only, has one consumer, and can only be awaited as an rvalue. It stores +either a value or an exception, transfers directly between child and continuation, and destroys +an unconsumed frame through RAII. `Task`, copying, move assignment, and detached execution are +deliberately unsupported. + +A translation unit that defines a coroutine must import `std` so the compiler can see the standard +coroutine protocol types. CMP imports `std` privately and does not re-export the whole standard +library. + +CMP does not yet provide `sync_wait` or a scheduler, so the current API is a composition primitive +rather than a complete application entry point. The standalone example therefore defines a small, +private root coroutine that is suitable only for its synchronously completing chain. A moved-from +Task must not be awaited. ## Repository Layout @@ -94,7 +116,7 @@ the namespace `mcpplibs::cmp`. ├── .xlings.json # pinned project tool environment ├── mcpp.toml # package identity and test dependency ├── src/cmp.cppm # root module interface -├── tests/cmp_test.cpp # import smoke test +├── tests/cmp_test.cpp # Task contract and lifetime tests ├── examples/basic/ # standalone path-dependency consumer ├── docs/architecture.md # current structure, boundaries, and evolution └── .github/workflows/ # Linux, macOS, and Windows CI @@ -122,17 +144,16 @@ belong in `[dependencies]`; test-only dependencies belong in `[dev-dependencies] ## Roadmap -Runtime work will be split into independently reviewable phases: +Runtime work is split into independently reviewable phases: -1. package identity and importable-module bootstrap; -2. coroutine task and lifetime semantics; +1. package identity and importable-module bootstrap — implemented; +2. coroutine task and lifetime semantics — initial `Task` implemented; 3. a minimal single-thread scheduler; 4. timers, cancellation, and structured wake-up paths; 5. multi-worker scheduling and work stealing; 6. asynchronous I/O integration and a blocking pool. -The order after the bootstrap is directional, not a promise that any listed feature is already -implemented. +The remaining order is directional, not a promise that a listed feature is already implemented. ## Contributing diff --git a/README.zh.hant.md b/README.zh.hant.md index d7438b4..97892c2 100644 --- a/README.zh.hant.md +++ b/README.zh.hant.md @@ -16,7 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP 目前處於 **bootstrap 階段**。套件已經匯出根模組 `mcpplibs.cmp`,但尚未提供協程執行期 API。 +> CMP 已提供第一個協程基礎型別:延遲啟動、單一消費者的 `Task` / `Task`。 +> 排程、取消、計時器、非同步 I/O 和公開根任務驅動器尚未實作。 CMP 計畫以標準無堆疊 C++ 協程建構現代協程執行期與函式庫。專案將以明確的 `co_await` 為主線,透過經過驗證的小步驟逐步探索排程、計時器、非同步 I/O、取消,以及阻塞工作的安全隔離。 @@ -66,20 +67,39 @@ cd examples/basic mcpp run ``` -範例會以成功狀態結束且不產生輸出。它只負責證明獨立 mcpp 套件能夠解析路徑相依並匯入 -`mcpplibs.cmp`。 +範例會從 `Task` 協程內部印出 `Coroutine result: 42`,然後以成功狀態結束。它負責 +證明獨立 mcpp 套件能夠解析路徑相依、匯入 `mcpplibs.cmp`、組合 Task 並執行同步協程鏈。 -## 目前模組 +## 目前 Task API ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -bootstrap 階段的模組刻意不包含公開宣告。未來公共 API 將使用 `mcpplibs::cmp` 命名空間。 +`Task` 採延遲啟動:呼叫 `answer()` 只建立處於暫停狀態的協程,在被 `co_await` 消費時才 +開始執行。Task 只能移動、只有一個消費者且只能作為右值等待;它保存值或例外,在子協程與 +continuation 之間直接轉移,並透過 RAII 銷毀未消費的協程框架。目前刻意不支援 `Task`、 +複製、移動賦值和 detached 執行。 + +定義協程的轉譯單元必須匯入 `std`,使編譯器能夠看到標準協程協定型別。CMP 私下匯入 +`std`,不會向使用端重新匯出整個標準函式庫。 + +CMP 尚未提供 `sync_wait` 或排程器,因此目前 API 是協程組合基礎,而不是完整的應用程式入口。 +獨立範例因此定義了一個很小的私有根協程,只適用於其中同步完成的協程鏈。已經被移動的 +Task 不得再次等待。 ## 儲存庫結構 @@ -88,7 +108,7 @@ bootstrap 階段的模組刻意不包含公開宣告。未來公共 API 將使 ├── .xlings.json # 固定的專案工具環境 ├── mcpp.toml # 套件識別與測試相依 ├── src/cmp.cppm # 根模組介面 -├── tests/cmp_test.cpp # 匯入 smoke 測試 +├── tests/cmp_test.cpp # Task 契約和生命週期測試 ├── examples/basic/ # 獨立的路徑相依 consumer ├── docs/architecture.zh.hant.md # 目前結構、邊界與演進方向 └── .github/workflows/ # Linux、macOS 和 Windows CI @@ -114,16 +134,16 @@ CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定 ## 路線圖 -執行期工作將拆分為可以獨立審查的階段: +執行期工作拆分為可以獨立審查的階段: -1. 套件識別與可匯入模組 bootstrap; -2. 協程 task 與生命週期語意; +1. 套件識別與可匯入模組 bootstrap——已完成; +2. 協程 task 與生命週期語意——已實作初始 `Task`; 3. 最小單執行緒排程器; 4. 計時器、取消和結構化喚醒路徑; 5. 多 worker 排程與 work stealing; 6. 非同步 I/O 整合和 blocking pool。 -bootstrap 之後的順序只是方向,不代表這些能力已經實作。 +剩餘順序只是方向,不代表列出的能力已經實作。 ## 參與貢獻 diff --git a/README.zh.md b/README.zh.md index b57de0a..4b93282 100644 --- a/README.zh.md +++ b/README.zh.md @@ -16,7 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP 当前处于 **bootstrap 阶段**。包已经导出根模块 `mcpplibs.cmp`,但尚未提供协程运行时 API。 +> CMP 已经提供第一个协程基础类型:懒启动、单消费者的 `Task` / `Task`。 +> 调度、取消、定时器、异步 I/O 和公共根任务驱动器尚未实现。 CMP 计划基于标准无栈 C++ 协程构建现代协程运行时和库。项目将以显式 `co_await` 为主线, 通过经过验证的小步骤逐步探索调度、定时器、异步 I/O、取消以及阻塞工作的安全隔离。 @@ -66,20 +67,39 @@ cd examples/basic mcpp run ``` -示例会以成功状态退出且不产生输出。它只负责证明独立 mcpp 包能够解析路径依赖并导入 -`mcpplibs.cmp`。 +示例会从 `Task` 协程内部打印 `Coroutine result: 42`,然后以成功状态退出。它负责 +证明独立 mcpp 包能够解析路径依赖、导入 `mcpplibs.cmp`、组合 Task 并执行同步协程链。 -## 当前模块 +## 当前 Task API ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -bootstrap 阶段的模块刻意不包含公开声明。未来公共 API 将使用 `mcpplibs::cmp` 命名空间。 +`Task` 采用懒启动:调用 `answer()` 只创建处于挂起状态的协程,在被 `co_await` 消费时才 +开始执行。Task 只能移动、只有一个消费者且只能作为右值等待;它保存值或异常,在子协程与 +continuation 之间直接转移,并通过 RAII 销毁未消费的协程帧。当前刻意不支持 `Task`、 +复制、移动赋值和 detached 执行。 + +定义协程的翻译单元必须导入 `std`,使编译器能够看到标准协程协议类型。CMP 私有导入 +`std`,不会向使用方重新导出整个标准库。 + +CMP 尚未提供 `sync_wait` 或调度器,因此当前 API 是协程组合基础,而不是完整的应用入口。 +独立示例因此定义了一个很小的私有根协程,只适用于其中同步完成的协程链。已经被移动的 +Task 不得再次等待。 ## 仓库结构 @@ -88,7 +108,7 @@ bootstrap 阶段的模块刻意不包含公开声明。未来公共 API 将使 ├── .xlings.json # 固定的项目工具环境 ├── mcpp.toml # 包身份和测试依赖 ├── src/cmp.cppm # 根模块接口 -├── tests/cmp_test.cpp # 导入 smoke 测试 +├── tests/cmp_test.cpp # Task 契约和生命周期测试 ├── examples/basic/ # 独立的路径依赖 consumer ├── docs/architecture.zh.md # 当前结构、边界和演进方向 └── .github/workflows/ # Linux、macOS 和 Windows CI @@ -114,16 +134,16 @@ CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。 ## 路线图 -运行时工作将拆分为可以独立审查的阶段: +运行时工作拆分为可以独立审查的阶段: -1. 包身份和可导入模块 bootstrap; -2. 协程 task 与生命周期语义; +1. 包身份和可导入模块 bootstrap——已完成; +2. 协程 task 与生命周期语义——已实现初始 `Task`; 3. 最小单线程调度器; 4. 定时器、取消和结构化唤醒路径; 5. 多 worker 调度与 work stealing; 6. 异步 I/O 集成和 blocking pool。 -bootstrap 之后的顺序只是方向,不代表这些能力已经实现。 +剩余顺序只是方向,不代表列出的能力已经实现。 ## 参与贡献 diff --git a/docs/architecture.md b/docs/architecture.md index 1e20967..aacc3fd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,14 +4,15 @@ ## Current status -CMP currently consists of an initial C++23 module project. The package can be built and imported, -but the root module has no public declarations and no coroutine runtime has been implemented. +CMP is a C++23 module project whose first runtime primitive is implemented. The root module exports +a lazy, single-consumer `mcpplibs::cmp::Task` with a `Task` specialization. It does not yet +provide a scheduler or a root execution API. The repository contains: - one mcpp package manifest; -- the import-only root module `mcpplibs.cmp`; -- one gtest import test; +- the root module `mcpplibs.cmp` and its Task implementation; +- gtest contract, lifetime, exception, and symmetric-transfer tests; - one standalone path-dependency example; - Linux, macOS, and Windows CI workflows. @@ -29,8 +30,8 @@ repo = "https://github.com/mcpplibs/cmp" ``` The mcpp package identity is the pair `mcpplibs` and `cmp`. A consumer declares `cmp` under -`[dependencies.mcpplibs]` and imports the C++ module `mcpplibs.cmp`. The namespace -`mcpplibs::cmp` is reserved for future public C++ declarations. +`[dependencies.mcpplibs]` and imports the C++ module `mcpplibs.cmp`. Public C++ declarations use +the namespace `mcpplibs::cmp`. The root interface is `src/cmp.cppm`, which matches mcpp's default library-root naming rule. There is no `src/main.cpp`, so mcpp infers a library target named `cmp`; the manifest does not @@ -64,8 +65,9 @@ it is also mcpp's default. ## Build and tests `.xlings.json` pins the mcpp version used by the project. `mcpp build` builds the inferred library -target. `mcpp test` discovers `tests/cmp_test.cpp`, links the gtest entry point, and verifies that a -separate translation unit can import `mcpplibs.cmp`. +target. `mcpp test` discovers `tests/cmp_test.cpp`, links the gtest entry point, and verifies the +Task type contract, lazy execution, frame ownership, value and exception propagation, nested +composition, and stack-safe symmetric transfer. Each CI workflow installs the project tools, builds the library, runs the test suite, and runs `examples/basic`. The workflows are separate because tool installation and runner details differ @@ -96,21 +98,51 @@ cmp = { path = "../.." } Its program uses the same import path as an external package: ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -This example checks path dependency resolution and module consumption independently of the root -test target. +This example checks path dependency resolution, module consumption, and external coroutine +compilation independently of the root test target. Its example-private `InlineRunner` starts an +eager root coroutine, which awaits `print_answer()` and produces `Coroutine result: 42`. The helper +is limited to this synchronously completing, scheduler-free chain and is not a CMP public API. + +Any translation unit that defines a coroutine imports `std` itself so `std::coroutine_traits` and +the standard coroutine protocol types participate in compilation. The CMP module imports `std` +privately rather than re-exporting the entire standard library. ## Current constraints -The module currently provides no `task`, promise type, scheduler, timer, cancellation mechanism, -asynchronous I/O backend, or blocking-work pool. It also provides no compatibility alias for the -old scaffold module. +`Task` and `Task` have the following contract: + +- construction is lazy; the coroutine body starts when the Task is awaited; +- ownership is unique: Task is movable but not copyable or move-assignable; +- `operator co_await()` is rvalue-only and consumes the coroutine handle; +- one value or `std::exception_ptr` is stored in the coroutine frame; +- child completion transfers directly to its continuation, avoiding recursive `resume()` chains; +- an unconsumed Task destroys its frame, and the consuming awaiter destroys a completed frame; +- reference and array result types are rejected. + +A moved-from Task is empty and must not be awaited. The current implementation terminates on that +contract violation. There is no public `sync_wait`, detached execution, scheduler, thread-affinity +guarantee, timer, cancellation mechanism, asynchronous I/O backend, custom frame allocator, or +blocking-work pool. The module also provides no compatibility alias for the old scaffold module. + +Capturing coroutine lambdas require particular care: invoking a temporary capturing lambda can +leave the lazy coroutine referring to a destroyed closure. CMP does not yet provide a helper that +extends that closure's lifetime. The `C` in CMP echoes the naming role of Go runtime's `G`; it does not imply equivalent semantics. Standard C++ coroutines provide suspension and resumption mechanics, but they do not supply a @@ -121,12 +153,13 @@ scheduler and do not make a blocking operation asynchronous. The following areas may be considered in separate designs. They are not part of the current package contract: -1. coroutine task ownership, completion, and lifetime rules; -2. a single-thread scheduler and explicit scheduling awaiters; +1. a root runner and minimal single-thread scheduler with explicit scheduling awaiters; +2. structured task scopes and concurrent joins; 3. timers, wake-up paths, and cancellation; 4. multi-worker scheduling and work stealing; 5. asynchronous I/O integrations; -6. a dedicated pool for unavoidable blocking work. +6. a dedicated pool for unavoidable blocking work; +7. result adapters and optional coroutine-frame allocation strategies. Module partitions or implementation units can be added when an implemented API needs those boundaries. @@ -142,7 +175,9 @@ cd examples/basic mcpp run ``` -The expected result is a successful library build, one passing import test, and an example that -exits with status 0. The current Windows LLVM toolchain does not emit GNU depfiles. If a file +The expected result is a successful library build, eight passing Task tests, and an example that +exits with status 0. One test performs one million immediate Task completions to check that +symmetric transfer does not grow the native call stack. The current Windows LLVM toolchain does +not emit GNU depfiles. If a file included by a module interface changes, an incremental build can reuse an older BMI or object; `--cache=off` is used for a full local verification. diff --git a/docs/architecture.zh.hant.md b/docs/architecture.zh.hant.md index b8b9a25..78aaf8d 100644 --- a/docs/architecture.zh.hant.md +++ b/docs/architecture.zh.hant.md @@ -4,14 +4,14 @@ ## 目前狀態 -CMP 目前是一個初始的 C++23 模組專案。套件可以建置和匯入,但根模組還沒有公開宣告, -協程執行期也尚未實作。 +CMP 是一個 C++23 模組專案,已實作第一個執行期基礎型別。根模組匯出延遲啟動、單一消費者的 +`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供排程器或根任務執行 API。 儲存庫現有內容包括: - 一份 mcpp 套件清單; -- 只包含模組宣告的根模組 `mcpplibs.cmp`; -- 一個 gtest 匯入測試; +- 根模組 `mcpplibs.cmp` 及其 Task 實作; +- 涵蓋契約、生命週期、例外和對稱轉移的 gtest 測試; - 一個透過路徑相依使用根套件的獨立範例; - Linux、macOS 和 Windows 三套 CI 工作流程。 @@ -29,8 +29,7 @@ repo = "https://github.com/mcpplibs/cmp" ``` mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.mcpplibs]` 中宣告 -`cmp`,在 C++ 原始碼中匯入 `mcpplibs.cmp`。`mcpplibs::cmp` 保留給日後的公開 C++ -宣告使用。 +`cmp`,在 C++ 原始碼中匯入 `mcpplibs.cmp`。公開 C++ 宣告使用 `mcpplibs::cmp` 命名空間。 根模組介面位於 `src/cmp.cppm`,符合 mcpp 預設的函式庫根模組命名規則。儲存庫中沒有 `src/main.cpp`,因此 mcpp 會推斷出名為 `cmp` 的函式庫目標,不需要額外設定 `[lib]` 或 @@ -63,8 +62,8 @@ mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.m ## 建置與測試 `.xlings.json` 固定專案使用的 mcpp 版本。`mcpp build` 建置自動推斷的函式庫目標。 -`mcpp test` 會找到 `tests/cmp_test.cpp`,連結 gtest 進入點,並驗證另一個轉譯單元可以 -匯入 `mcpplibs.cmp`。 +`mcpp test` 會找到 `tests/cmp_test.cpp`,連結 gtest 進入點,並驗證 Task 型別契約、延遲執行、 +協程框架所有權、值與例外傳播、巢狀組合,以及不會增長呼叫堆疊的對稱轉移。 三套 CI 工作流程都會安裝專案工具、建置函式庫、執行測試並執行 `examples/basic`。不同 作業系統的工具安裝和執行環境不同,因此分別保留工作流程檔案。 @@ -93,19 +92,47 @@ cmp = { path = "../.." } 範例程式使用與外部專案相同的匯入路徑: ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -這個範例在根測試目標之外,單獨檢查路徑相依解析和模組使用。 +這個範例在根測試目標之外,單獨檢查路徑相依解析、模組使用和外部協程編譯。範例私有的 +`InlineRunner` 啟動一個 eager 根協程,等待 `print_answer()` 並輸出 `Coroutine result: 42`。 +這個輔助型別只適用於目前同步完成且沒有排程器的協程鏈,不屬於 CMP 公共 API。 + +任何定義協程的轉譯單元都要自行匯入 `std`,使 `std::coroutine_traits` 和標準協程協定型別 +參與編譯。CMP 模組私下匯入 `std`,而不是向使用端重新匯出整個標準函式庫。 ## 目前邊界 -根模組目前沒有提供 `task`、promise 型別、排程器、計時器、取消機制、非同步 I/O 後端或 -阻塞工作執行緒池,也沒有保留舊骨架模組的相容別名。 +`Task` 和 `Task` 遵循以下契約: + +- 建構時延遲啟動,協程本體在 Task 被等待時開始執行; +- 所有權唯一:Task 可以移動建構,但不能複製或移動賦值; +- `operator co_await()` 僅用於右值,並在等待時消費協程控制代碼; +- 協程框架保存一個值或 `std::exception_ptr`; +- 子協程完成後直接轉移到 continuation,避免遞迴呼叫 `resume()`; +- 未消費的 Task 銷毀自己的框架,消費它的 awaiter 銷毀已完成的框架; +- 拒絕參考和陣列結果型別。 + +被移動後的 Task 為空,不得再次等待;目前實作會在違反該契約時終止程序。目前沒有公開 +`sync_wait`、detached 執行、排程器、執行緒親和保證、計時器、取消機制、非同步 I/O 後端、 +自訂協程框架 allocator 或阻塞工作執行緒池,也沒有保留舊骨架模組的相容別名。 + +捕捉變數的協程 lambda 需要特別小心:立即呼叫一個暫時的捕捉 lambda,可能使延遲協程參考 +已經銷毀的閉包。CMP 尚未提供延長該閉包生命週期的輔助函式。 CMP 名稱中的 `C` 與 Go 執行期中的 `G` 相呼應,但這只說明命名來源,不表示兩者語意 等價。標準 C++ 協程提供暫停和恢復機制,本身不包含排程器,也不會把阻塞操作自動變成 @@ -115,12 +142,13 @@ CMP 名稱中的 `C` 與 Go 執行期中的 `G` 相呼應,但這只說明命 以下方向可以分別設計和審查,目前都不是套件的既有約定: -1. 協程工作的所有權、完成和生命週期規則; -2. 單執行緒排程器和明確的排程 awaiter; +1. 根任務驅動器、最小單執行緒排程器和明確的排程 awaiter; +2. 結構化任務作用域和並行匯合; 3. 計時器、喚醒路徑和取消; 4. 多工作執行緒排程和工作竊取; 5. 非同步 I/O 整合; -6. 處理無法避免之阻塞工作的專用執行緒池。 +6. 處理無法避免之阻塞工作的專用執行緒池; +7. 結果適配器和可選的協程框架配置策略。 只有已實作的 API 確實需要新邊界時,才增加模組分割區或實作單元。 @@ -135,6 +163,7 @@ cd examples/basic mcpp run ``` -預期結果是函式庫建置成功、一個匯入測試通過,而且範例以狀態 0 結束。目前 Windows -LLVM 工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能沿用舊的 -BMI 或目的檔。完整複驗時使用 `--cache=off`。 +預期結果是函式庫建置成功、八個 Task 測試通過,而且範例以狀態 0 結束。其中一個測試執行 +一百萬次立即完成的 Task,用於檢查對稱轉移不會增長原生呼叫堆疊。目前 Windows LLVM +工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能沿用舊的 BMI +或目的檔。完整複驗時使用 `--cache=off`。 diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 78b4bef..1c85b21 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -4,14 +4,14 @@ ## 当前状态 -CMP 目前是一个初始的 C++23 模块项目。包可以构建和导入,但根模块还没有公共声明, -协程运行时也尚未实现。 +CMP 是一个 C++23 模块项目,已经实现首个运行时基础类型。根模块导出懒启动、单消费者的 +`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供调度器或根任务执行 API。 仓库现有内容包括: - 一份 mcpp 包清单; -- 仅包含模块声明的根模块 `mcpplibs.cmp`; -- 一个 gtest 导入测试; +- 根模块 `mcpplibs.cmp` 及其 Task 实现; +- 覆盖契约、生命周期、异常和对称转移的 gtest 测试; - 一个通过路径依赖使用根包的独立示例; - Linux、macOS 和 Windows 三套 CI 工作流。 @@ -29,8 +29,7 @@ repo = "https://github.com/mcpplibs/cmp" ``` mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpplibs]` 中声明 -`cmp`,在 C++ 源码中导入 `mcpplibs.cmp`。`mcpplibs::cmp` 留作以后公共 C++ 声明 -使用。 +`cmp`,在 C++ 源码中导入 `mcpplibs.cmp`。公共 C++ 声明使用 `mcpplibs::cmp` 命名空间。 根模块接口位于 `src/cmp.cppm`,符合 mcpp 默认的库根模块命名规则。仓库中没有 `src/main.cpp`,因此 mcpp 会推断出名为 `cmp` 的库目标,不需要额外配置 `[lib]` 或 @@ -63,8 +62,8 @@ mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpp ## 构建与测试 `.xlings.json` 固定项目使用的 mcpp 版本。`mcpp build` 构建自动推断的库目标。 -`mcpp test` 发现 `tests/cmp_test.cpp`,链接 gtest 入口,并验证另一个翻译单元可以导入 -`mcpplibs.cmp`。 +`mcpp test` 发现 `tests/cmp_test.cpp`,链接 gtest 入口,并验证 Task 类型契约、懒执行、 +协程帧所有权、值与异常传播、嵌套组合以及不会增长调用栈的对称转移。 三套 CI 工作流都会安装项目工具、构建库、运行测试并执行 `examples/basic`。不同操作系统 的工具安装和运行环境不同,因此分别保留工作流文件。 @@ -93,19 +92,47 @@ cmp = { path = "../.." } 示例程序使用与外部项目相同的导入路径: ```cpp +import std; import mcpplibs.cmp; -int main() { - return 0; +using mcpplibs::cmp::Task; + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; } ``` -该示例在根测试目标之外,单独检查路径依赖解析和模块使用。 +该示例在根测试目标之外,单独检查路径依赖解析、模块使用和外部协程编译。示例私有的 +`InlineRunner` 启动一个 eager 根协程,等待 `print_answer()` 并输出 `Coroutine result: 42`。 +这个辅助类型只适用于当前同步完成且没有调度器的协程链,不属于 CMP 公共 API。 + +任何定义协程的翻译单元都要自行导入 `std`,使 `std::coroutine_traits` 和标准协程协议类型 +参与编译。CMP 模块私有导入 `std`,而不是向使用方重新导出整个标准库。 ## 当前边界 -根模块目前没有提供 `task`、promise 类型、调度器、定时器、取消机制、异步 I/O 后端或 -阻塞任务线程池,也没有保留旧脚手架模块的兼容别名。 +`Task` 和 `Task` 遵循以下契约: + +- 构造时懒启动,协程体在 Task 被等待时开始执行; +- 所有权唯一:Task 可以移动构造,但不能复制或移动赋值; +- `operator co_await()` 仅用于右值,并在等待时消费协程句柄; +- 协程帧保存一个值或 `std::exception_ptr`; +- 子协程完成后直接转移到 continuation,避免递归调用 `resume()`; +- 未消费的 Task 销毁自己的帧,消费它的 awaiter 销毁已经完成的帧; +- 拒绝引用和数组结果类型。 + +被移动后的 Task 为空,不得再次等待;当前实现会在违反该契约时终止进程。目前没有公共 +`sync_wait`、detached 执行、调度器、线程亲和保证、定时器、取消机制、异步 I/O 后端、 +自定义协程帧 allocator 或阻塞任务线程池,也没有保留旧脚手架模块的兼容别名。 + +捕获变量的协程 lambda 需要特别小心:立即调用一个临时的捕获 lambda,可能使懒协程引用 +已经销毁的闭包。CMP 尚未提供延长该闭包生命周期的辅助函数。 CMP 名称中的 `C` 与 Go 运行时中的 `G` 相呼应,但这只说明命名来源,不表示两者语义等价。 标准 C++ 协程提供挂起和恢复机制,本身不包含调度器,也不会把阻塞操作自动变成异步操作。 @@ -114,12 +141,13 @@ CMP 名称中的 `C` 与 Go 运行时中的 `G` 相呼应,但这只说明命 以下方向可以分别设计和评审,目前都不是包的既有约定: -1. 协程任务的所有权、完成和生命周期规则; -2. 单线程调度器和显式调度 awaiter; +1. 根任务驱动器、最小单线程调度器和显式调度 awaiter; +2. 结构化任务作用域和并发汇合; 3. 定时器、唤醒路径和取消; 4. 多工作线程调度和工作窃取; 5. 异步 I/O 集成; -6. 处理不可避免的阻塞工作的专用线程池。 +6. 处理不可避免的阻塞工作的专用线程池; +7. 结果适配器和可选的协程帧分配策略。 只有已实现的 API 确实需要新的边界时,才增加模块分区或实现单元。 @@ -134,6 +162,7 @@ cd examples/basic mcpp run ``` -预期结果是库构建成功、一个导入测试通过,并且示例以状态 0 退出。当前 Windows LLVM -工具链不会生成 GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI -或目标文件。完整复验时使用 `--cache=off`。 +预期结果是库构建成功、八个 Task 测试通过,并且示例以状态 0 退出。其中一个测试执行一百万次 +立即完成的 Task,用于检查对称转移不会增长原生调用栈。当前 Windows LLVM 工具链不会生成 +GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI 或目标文件。 +完整复验时使用 `--cache=off`。 diff --git a/examples/basic/src/main.cpp b/examples/basic/src/main.cpp index 191aa3c..b70cdc2 100644 --- a/examples/basic/src/main.cpp +++ b/examples/basic/src/main.cpp @@ -1,5 +1,49 @@ +import std; import mcpplibs.cmp; +using mcpplibs::cmp::Task; + +class InlineRunner { +public: + struct promise_type { + [[nodiscard]] InlineRunner get_return_object() const noexcept; + + [[nodiscard]] constexpr std::suspend_never initial_suspend() const noexcept { + return {}; + } + + [[nodiscard]] constexpr std::suspend_never final_suspend() const noexcept { + return {}; + } + + constexpr void return_void() const noexcept {} + + [[noreturn]] void unhandled_exception() const noexcept { + std::terminate(); + } + }; +}; + +InlineRunner InlineRunner::promise_type::get_return_object() const noexcept { + return {}; +} + +Task answer() { + co_return 42; +} + +Task print_answer() { + auto value = co_await answer(); + std::println("Coroutine result: {}", value); + co_return; +} + +InlineRunner run_inline(Task task) { + co_await std::move(task); + co_return; +} + int main() { + run_inline(print_answer()); return 0; } diff --git a/src/cmp.cppm b/src/cmp.cppm index c6912de..36aca08 100644 --- a/src/cmp.cppm +++ b/src/cmp.cppm @@ -1 +1,260 @@ export module mcpplibs.cmp; + +import std; + +export namespace mcpplibs::cmp { + +template +requires ( + std::same_as || + (std::is_object_v && !std::is_array_v) +) +class [[nodiscard]] Task { +public: + struct promise_type { + std::optional result_ {}; + std::exception_ptr exception_ {}; + std::coroutine_handle<> continuation_ { std::noop_coroutine() }; + + [[nodiscard]] Task get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + class FinalAwaiter { + public: + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle coroutine) const noexcept { + return coroutine.promise().continuation_; + } + + constexpr void await_resume() const noexcept {} + }; + + [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { + return {}; + } + + template + requires std::constructible_from + void return_value(U&& value) + noexcept(std::is_nothrow_constructible_v) { + result_.emplace(std::forward(value)); + } + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + class Awaiter { + private: + Handle coroutine_ {}; + + public: + explicit Awaiter(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + + Awaiter(const Awaiter&) = delete; + Awaiter& operator=(const Awaiter&) = delete; + + Awaiter(Awaiter&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Awaiter& operator=(Awaiter&&) = delete; + + ~Awaiter() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle<> continuation) noexcept { + coroutine_.promise().continuation_ = continuation; + return coroutine_; + } + + T await_resume() { + auto& promise = coroutine_.promise(); + + if (promise.exception_) { + std::rethrow_exception(promise.exception_); + } + + return std::move(*promise.result_); + } + }; + + Handle coroutine_ {}; + + explicit Task(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + Task() = delete; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task(Task&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Task& operator=(Task&&) = delete; + + ~Task() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] auto operator co_await() && noexcept { + if (!coroutine_) { + std::terminate(); + } + + return Awaiter { std::exchange(coroutine_, {}) }; + } +}; + +template +requires ( + std::same_as || + (std::is_object_v && !std::is_array_v) +) +Task Task::promise_type::get_return_object() noexcept { + return Task { + std::coroutine_handle::from_promise(*this) + }; +} + +template<> +class [[nodiscard]] Task { +public: + struct promise_type { + std::exception_ptr exception_ {}; + std::coroutine_handle<> continuation_ { std::noop_coroutine() }; + + [[nodiscard]] Task get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + class FinalAwaiter { + public: + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle coroutine) const noexcept { + return coroutine.promise().continuation_; + } + + constexpr void await_resume() const noexcept {} + }; + + [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { + return {}; + } + + constexpr void return_void() const noexcept {} + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + class Awaiter { + private: + Handle coroutine_ {}; + + public: + explicit Awaiter(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + + Awaiter(const Awaiter&) = delete; + Awaiter& operator=(const Awaiter&) = delete; + + Awaiter(Awaiter&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Awaiter& operator=(Awaiter&&) = delete; + + ~Awaiter() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle<> continuation) noexcept { + coroutine_.promise().continuation_ = continuation; + return coroutine_; + } + + void await_resume() { + auto& promise = coroutine_.promise(); + + if (promise.exception_) { + std::rethrow_exception(promise.exception_); + } + } + }; + + Handle coroutine_ {}; + + explicit Task(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + Task() = delete; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task(Task&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Task& operator=(Task&&) = delete; + + ~Task() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] auto operator co_await() && noexcept { + if (!coroutine_) { + std::terminate(); + } + + return Awaiter { std::exchange(coroutine_, {}) }; + } +}; + +inline Task Task::promise_type::get_return_object() noexcept { + return Task { + std::coroutine_handle::from_promise(*this) + }; +} + +} // namespace mcpplibs::cmp diff --git a/tests/cmp_test.cpp b/tests/cmp_test.cpp index 4354c83..d6208d6 100644 --- a/tests/cmp_test.cpp +++ b/tests/cmp_test.cpp @@ -1,7 +1,313 @@ #include +import std; import mcpplibs.cmp; -TEST(CmpModuleTest, Imports) { - SUCCEED(); +namespace { + +using mcpplibs::cmp::Task; + +template +concept SupportsTask = requires { + typename Task; +}; + +template +concept HasLvalueCoAwait = requires(T& task) { + task.operator co_await(); +}; + +template +concept HasRvalueCoAwait = requires(T&& task) { + std::move(task).operator co_await(); +}; + +static_assert(SupportsTask); +static_assert(SupportsTask); +static_assert(!SupportsTask); +static_assert(!SupportsTask); +static_assert(!SupportsTask); +static_assert(!SupportsTask); + +static_assert(!std::default_initializable>); +static_assert(!std::copy_constructible>); +static_assert(std::move_constructible>); +static_assert(!std::is_move_assignable_v>); +static_assert(!HasLvalueCoAwait>); +static_assert(HasRvalueCoAwait>); + +class TestOperation { +public: + struct promise_type { + std::exception_ptr exception_ {}; + + [[nodiscard]] TestOperation get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + [[nodiscard]] constexpr std::suspend_always final_suspend() const noexcept { + return {}; + } + + void return_void() const noexcept {} + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + Handle coroutine_ {}; + + explicit TestOperation(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + TestOperation(const TestOperation&) = delete; + TestOperation& operator=(const TestOperation&) = delete; + + TestOperation(TestOperation&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + TestOperation& operator=(TestOperation&&) = delete; + + ~TestOperation() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + void run(); +}; + +TestOperation TestOperation::promise_type::get_return_object() noexcept { + return TestOperation { TestOperation::Handle::from_promise(*this) }; +} + +void TestOperation::run() { + if (!coroutine_ || coroutine_.done()) { + throw std::logic_error { "test operation is not runnable" }; + } + + coroutine_.resume(); + + if (!coroutine_.done()) { + throw std::logic_error { "task unexpectedly suspended" }; + } + + if (coroutine_.promise().exception_) { + std::rethrow_exception(coroutine_.promise().exception_); + } +} + +template +TestOperation store_result(Task task, std::optional& result) { + result.emplace(co_await std::move(task)); +} + +TestOperation await_task(Task task) { + co_await std::move(task); +} + +Task make_value(bool& started) { + started = true; + co_return 42; +} + +Task increment(int& value) { + ++value; + co_return; +} + +Task make_nested_value() { + bool started { false }; + auto value = co_await make_value(started); + co_return value + 1; +} + +class LifetimeToken { +private: + int* liveCount_ {}; + +public: + explicit LifetimeToken(int& liveCount) noexcept + : liveCount_ { &liveCount } { + ++*liveCount_; + } + + LifetimeToken(const LifetimeToken&) = delete; + LifetimeToken& operator=(const LifetimeToken&) = delete; + + LifetimeToken(LifetimeToken&& other) noexcept + : liveCount_ { std::exchange(other.liveCount_, nullptr) } {} + + LifetimeToken& operator=(LifetimeToken&&) = delete; + + ~LifetimeToken() { + if (liveCount_) { + --*liveCount_; + } + } +}; + +Task hold_token(LifetimeToken token) { + static_cast(token); + co_return; +} + +Task throw_error(LifetimeToken token) { + static_cast(token); + throw std::runtime_error { "task failed" }; + co_return 0; +} + +class MoveOnlyValue { +private: + int* liveCount_ {}; + +public: + int value {}; + + MoveOnlyValue(int value, int& liveCount) noexcept + : liveCount_ { &liveCount }, value { value } { + ++*liveCount_; + } + + MoveOnlyValue(const MoveOnlyValue&) = delete; + MoveOnlyValue& operator=(const MoveOnlyValue&) = delete; + + MoveOnlyValue(MoveOnlyValue&& other) noexcept + : liveCount_ { std::exchange(other.liveCount_, nullptr) }, + value { other.value } {} + + MoveOnlyValue& operator=(MoveOnlyValue&&) = delete; + + ~MoveOnlyValue() { + if (liveCount_) { + --*liveCount_; + } + } +}; + +Task make_move_only_value(int& liveCount) { + co_return MoveOnlyValue { 7, liveCount }; +} + +Task complete_immediately() { + co_return; +} + +Task complete_many_times(int count) { + for (int index { 0 }; index < count; ++index) { + co_await complete_immediately(); + } + + co_return count; +} + +TEST(CmpTaskTest, IsLazyAndReturnsValue) { + bool started { false }; + auto task = make_value(started); + std::optional result {}; + + EXPECT_FALSE(started); + + auto operation = store_result(std::move(task), result); + EXPECT_FALSE(started); + + operation.run(); + + EXPECT_TRUE(started); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 42); +} + +TEST(CmpTaskTest, SupportsVoidResults) { + int value { 0 }; + auto operation = await_task(increment(value)); + + EXPECT_EQ(value, 0); + operation.run(); + EXPECT_EQ(value, 1); } + +TEST(CmpTaskTest, ComposesNestedTasks) { + std::optional result {}; + auto operation = store_result(make_nested_value(), result); + + operation.run(); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 43); +} + +TEST(CmpTaskTest, DestroysAnUnawaitedFrame) { + int liveCount { 0 }; + + { + auto task = hold_token(LifetimeToken { liveCount }); + EXPECT_EQ(liveCount, 1); + } + + EXPECT_EQ(liveCount, 0); +} + +TEST(CmpTaskTest, MovingTransfersFrameOwnershipOnce) { + int liveCount { 0 }; + + { + auto first = hold_token(LifetimeToken { liveCount }); + auto second = std::move(first); + static_cast(second); + EXPECT_EQ(liveCount, 1); + } + + EXPECT_EQ(liveCount, 0); +} + +TEST(CmpTaskTest, MovesResultBeforeDestroyingFrame) { + int liveCount { 0 }; + std::optional result {}; + auto operation = store_result(make_move_only_value(liveCount), result); + + operation.run(); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->value, 7); + EXPECT_EQ(liveCount, 1); + + result.reset(); + EXPECT_EQ(liveCount, 0); +} + +TEST(CmpTaskTest, PropagatesExceptionAndDestroysFrame) { + int liveCount { 0 }; + std::optional result {}; + auto operation = store_result( + throw_error(LifetimeToken { liveCount }), + result); + + EXPECT_EQ(liveCount, 1); + EXPECT_THROW(operation.run(), std::runtime_error); + EXPECT_EQ(liveCount, 0); + EXPECT_FALSE(result.has_value()); +} + +TEST(CmpTaskTest, SymmetricTransferDoesNotGrowTheStack) { + constexpr int COMPLETION_COUNT { 1'000'000 }; + std::optional result {}; + auto operation = store_result( + complete_many_times(COMPLETION_COUNT), + result); + + operation.run(); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, COMPLETION_COUNT); +} + +} // namespace From 47918bb0c3fb41338b13160a8e97fa23fecb9627 Mon Sep 17 00:00:00 2001 From: HikariTish Date: Wed, 12 Aug 2026 20:13:38 +0800 Subject: [PATCH 2/5] feat: implement coroutine RunLoop and Scheduler Add caller-thread FIFO scheduling, cross-thread wakeups, root result and exception handling, boundary tests, examples, documentation, and current mcpp configuration. --- .agents/skills/mcpp-index/SKILL.md | 21 +- .agents/skills/mcpp-style-ref/SKILL.md | 6 +- .agents/skills/mcpp/SKILL.md | 14 +- .agents/skills/more-details/SKILL.md | 9 +- .gitignore | 1 + .idea/.gitignore | 10 - .idea/cmp.iml | 2 - .idea/editor.xml | 350 ----------------- .idea/modules.xml | 8 - .idea/vcs.xml | 6 - .xlings.json | 2 +- README.md | 41 +- README.zh.hant.md | 38 +- README.zh.md | 38 +- docs/architecture.md | 103 +++-- docs/architecture.zh.hant.md | 90 +++-- docs/architecture.zh.md | 89 +++-- .../plans/2026-08-12-cmp-run-loop.md | 41 ++ .../specs/2026-08-12-cmp-run-loop-design.md | 121 ++++++ examples/basic/src/main.cpp | 37 +- mcpp.toml | 2 +- src/cmp.cppm | 260 +------------ src/run_loop.cppm | 333 ++++++++++++++++ src/task.cppm | 261 +++++++++++++ tests/cmp_test.cpp | 4 +- tests/run_loop_test.cpp | 366 ++++++++++++++++++ 26 files changed, 1437 insertions(+), 816 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/cmp.iml delete mode 100644 .idea/editor.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 docs/superpowers/plans/2026-08-12-cmp-run-loop.md create mode 100644 docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md create mode 100644 src/run_loop.cppm create mode 100644 src/task.cppm create mode 100644 tests/run_loop_test.cpp diff --git a/.agents/skills/mcpp-index/SKILL.md b/.agents/skills/mcpp-index/SKILL.md index 698aa78..3a9d303 100644 --- a/.agents/skills/mcpp-index/SKILL.md +++ b/.agents/skills/mcpp-index/SKILL.md @@ -29,13 +29,10 @@ mcpp index list|add|remove|update # manage registries Package identity is a pair: **`namespace` is a dotted hierarchical path, `name` is a single atomic segment** (`compat` + `zlib`, `mcpplibs.capi` + `lua` — never `mcpplibs` + `capi.lua`). -A **bare** dependency name resolves in exactly three places, in order: - -1. `mcpplibs` — the default namespace -2. `compat` — third-party C/C++ wrappers -3. packages that declare no namespace at all - -Anything else must be spelled out — there is no index-wide fuzzy search by short name: +A **bare** dependency name means the `mcpplibs` default namespace. The older fallback search +through `compat` and packages without a namespace is deprecated in mcpp 2026.8 and removed in +2026.9. Every other namespace must be spelled out; there is no index-wide fuzzy search by short +name: ```toml [dependencies] @@ -43,12 +40,14 @@ Anything else must be spelled out — there is no index-wide fuzzy search by sho [dependencies.chriskohlhoff] # or a namespace sub-table asio = "1.38.1" + +[dependencies.compat] # third-party wrapper +zlib = "1.3.1" ``` -When resolution fails: read the error (it lists the namespaces searched), then check the -spelling against the online index, and run `xlings update` — a release tarball bundles an -index snapshot frozen at build time, which is the usual reason a fresh version "does not -exist". +When resolution fails: read the error, check the exact namespace and spelling against the online +index, and run `xlings update` — a release tarball bundles an index snapshot frozen at build time, +which is the usual reason a fresh version "does not exist". Mirrors: `mcpp self config --mirror CN` switches to the GitCode mirror; `GLOBAL` (upstream) is the default. diff --git a/.agents/skills/mcpp-style-ref/SKILL.md b/.agents/skills/mcpp-style-ref/SKILL.md index 921858d..6bad24b 100644 --- a/.agents/skills/mcpp-style-ref/SKILL.md +++ b/.agents/skills/mcpp-style-ref/SKILL.md @@ -149,8 +149,10 @@ mcpp --version - `.xlings.json`:声明项目工具环境 - `mcpp.toml`:声明 `[package]` 与测试依赖;简单库目标可由 mcpp 从 `src/*.cppm` 自动推断 -- `src/cmp.cppm`:库主模块接口,声明 `export module mcpplibs.cmp;` -- `tests/cmp_test.cpp`:`mcpp test` 自动发现的 gtest 导入测试;不要定义 `main()` +- `src/cmp.cppm`:库主模块接口,导出 `:task` 与 `:run_loop` 分区 +- `src/task.cppm`:`Task` 与 `Task` 分区 +- `src/run_loop.cppm`:`RunLoop` 与 `Scheduler` 分区 +- `tests/cmp_test.cpp`、`tests/run_loop_test.cpp`:`mcpp test` 自动发现的 gtest 测试;不要定义 `main()` - `examples/basic/`:独立 mcpp consumer 包,通过 path 依赖引用根库 构建: diff --git a/.agents/skills/mcpp/SKILL.md b/.agents/skills/mcpp/SKILL.md index f26fbfa..3076702 100644 --- a/.agents/skills/mcpp/SKILL.md +++ b/.agents/skills/mcpp/SKILL.md @@ -9,7 +9,7 @@ The build and package tool this repository is built with. mcpp is module-first C `import std` works out of the box, toolchains install into an isolated sandbox, and dependencies resolve through a package index. -Verified against **mcpp 2026.8.1.1**. mcpp is pre-1.0 and moves fast — when this skill and +Verified against **mcpp 2026.8.11.2**. mcpp is pre-1.0 and moves fast — when this skill and the tool disagree, the tool wins. Check with `mcpp --help`, `mcpp --help`, and the [upstream docs](https://github.com/mcpp-community/mcpp/tree/main/docs). @@ -81,12 +81,12 @@ defines = ["FOO=1"] # bare names; reach every TU including module scan default-profile = "release" # project default when no --profile/--release is passed [dependencies] # runtime deps -cmdline = "0.0.2" # bare name: searched in mcpplibs, then compat, then no-namespace +cmdline = "0.0.2" # bare name: mcpplibs namespace [dependencies.mcpplibs] # namespace sub-table (preferred for several from one org) tinyhttps = "0.2.3" -[dev-dependencies] # test-only; `mcpp build` ignores these +[dev-dependencies.compat] # test-only; `mcpp build` ignores these gtest = "1.15.2" [toolchain] @@ -103,10 +103,10 @@ Dependency forms: `"1.2.3"` · `"^1.2"` · `"~1.2"` · `">=1.0, <2.0"` · `{ path = "../mylib" }` · `{ git = "...", tag = "v1.0.0" }` · `{ version = "0.0.3", features = ["docking"] }`. -**Namespace rule:** a bare name resolves in exactly three places, in order — `mcpplibs`, -`compat` (third-party C/C++ wrappers), then packages that declare no namespace. Any other -namespace must be written out: `"chriskohlhoff.asio" = "1.38.1"` or a -`[dependencies.chriskohlhoff]` sub-table. +**Namespace rule:** a bare name means `mcpplibs`. Compatibility fallback to `compat` is deprecated +in 2026.8 and removed in 2026.9, so third-party wrappers and every other namespace must be explicit: +`[dependencies.compat]`, `"chriskohlhoff.asio" = "1.38.1"`, or the corresponding namespace +sub-table. ## Modules diff --git a/.agents/skills/more-details/SKILL.md b/.agents/skills/more-details/SKILL.md index 6c62a04..da4ad14 100644 --- a/.agents/skills/more-details/SKILL.md +++ b/.agents/skills/more-details/SKILL.md @@ -31,8 +31,11 @@ docs, and actual command output. - `docs/architecture.md` (`.zh.md`, `.zh.hant.md`) — current structure, mcpp conventions, runtime boundaries, CI. - `.xlings.json` — the project tool environment (which mcpp version builds this). - `mcpp.toml` — package metadata, dependencies, dev-dependencies. -- `src/cmp.cppm` — the import-only CMP root module interface. -- `tests/cmp_test.cpp` — the gtest import smoke test. +- `src/cmp.cppm` — the CMP root module interface, re-exporting its public partitions. +- `src/task.cppm` — lazy single-consumer Task implementation. +- `src/run_loop.cppm` — caller-thread RunLoop, Scheduler, and root driver. +- `tests/cmp_test.cpp` — Task contract and lifetime tests. +- `tests/run_loop_test.cpp` — scheduling, threading, and invalid-use boundary tests. - `examples/basic/` — a standalone path-dependency consumer. - `.github/workflows/ci-{linux,macos,windows}.yml` — per-platform build, test, and example CI. @@ -87,6 +90,6 @@ mcpp --version - **Confirm an `mcpp.toml` field** → `mcpp.toml`, the [`mcpp`](../mcpp/SKILL.md) skill, then upstream `docs/05-mcpp-toml.md`. - **Add a dependency** → the [`mcpp-index`](../mcpp-index/SKILL.md) skill, then `mcpplibs/cmdline` for a real example. - **Add a module API** → `mcpp-style-ref`, then edit `src/*.cppm`. -- **Add a test** → follow `tests/cmp_test.cpp`, verify with `mcpp test`. +- **Add a test** → follow the focused file under `tests/`, verify with `mcpp test`. - **Add an example** → follow `examples/basic/` — its own `mcpp.toml` with a path dependency. - **Publish the library** → the [`mcpp-index`](../mcpp-index/SKILL.md) skill. diff --git a/.gitignore b/.gitignore index 0f41629..2367b14 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ compile_commands.json # Local tools and editor state .cache/ /.clice/ +/.idea/ /.rpiv/ /.vscode/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 7f050be..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# 已忽略包含查询文件的默认文件夹 -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/cmp.iml b/.idea/cmp.iml deleted file mode 100644 index 962e49f..0000000 --- a/.idea/cmp.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml deleted file mode 100644 index 4abd7d2..0000000 --- a/.idea/editor.xml +++ /dev/null @@ -1,350 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index c19669f..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index c8397c9..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.xlings.json b/.xlings.json index b08ef31..1fb0f9c 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.8.1.1" + "mcpp": "2026.8.11.2" } } diff --git a/README.md b/README.md index bf1e439..b791f4b 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP now provides its first coroutine primitive: a lazy, single-consumer `Task` / `Task`. -> Scheduling, cancellation, timers, asynchronous I/O, and a public root runner are not implemented. +> CMP provides a lazy, single-consumer `Task` / `Task` and a caller-thread `RunLoop` +> with explicit scheduling. Cancellation, timers, asynchronous I/O, and detached execution are +> not implemented. CMP is being built as a modern coroutine runtime and library on standard stackless C++ coroutines. The intended direction is an explicit `co_await` model that can grow, in small @@ -71,27 +72,34 @@ cd examples/basic mcpp run ``` -The example prints `Coroutine result: 42` from inside a `Task` coroutine and exits +The example prints `Coroutine result: 42` from inside a scheduled `Task` coroutine and exits successfully. It proves that an independent mcpp package can resolve the path dependency, import -`mcpplibs.cmp`, compose Tasks, and execute the synchronous chain. +`mcpplibs.cmp`, compose Tasks, and drive them through the public RunLoop. -## Current Task API +## Current API ```cpp import std; import mcpplibs.cmp; using mcpplibs::cmp::Task; +using mcpplibs::cmp::RunLoop; Task answer() { co_return 42; } -Task print_answer() { +Task print_answer(RunLoop::Scheduler scheduler) { + co_await scheduler.schedule(); auto value = co_await answer(); std::println("Coroutine result: {}", value); co_return; } + +int main() { + RunLoop loop {}; + loop.run(print_answer(loop.get_scheduler())); +} ``` `Task` is lazy: calling `answer()` creates a suspended coroutine. It starts when consumed by @@ -104,10 +112,16 @@ A translation unit that defines a coroutine must import `std` so the compiler ca coroutine protocol types. CMP imports `std` privately and does not re-export the whole standard library. -CMP does not yet provide `sync_wait` or a scheduler, so the current API is a composition primitive -rather than a complete application entry point. The standalone example therefore defines a small, -private root coroutine that is suitable only for its synchronously completing chain. A moved-from -Task must not be awaited. +`RunLoop::run()` consumes one root Task, executes ready coroutines on the calling thread, returns +its value, and rethrows its exception. `Scheduler::schedule()` always suspends and queues the +continuation. Scheduler handles are copyable, but remain tied to their originating RunLoop. +Sequential `run()` calls are supported; nested or concurrent calls are rejected. A moved-from Task +must not be awaited. + +RunLoop is not a background thread and does not make blocking code asynchronous. A Task that +suspends without arranging a future resume can leave `run()` waiting indefinitely. CMP does not +provide automatic thread affinity: after an external awaiter resumes on another thread, explicitly +await the desired Scheduler to return to its RunLoop. ## Repository Layout @@ -116,7 +130,10 @@ Task must not be awaited. ├── .xlings.json # pinned project tool environment ├── mcpp.toml # package identity and test dependency ├── src/cmp.cppm # root module interface +├── src/task.cppm # Task module partition +├── src/run_loop.cppm # RunLoop and Scheduler partition ├── tests/cmp_test.cpp # Task contract and lifetime tests +├── tests/run_loop_test.cpp # scheduler, boundary, and threading tests ├── examples/basic/ # standalone path-dependency consumer ├── docs/architecture.md # current structure, boundaries, and evolution └── .github/workflows/ # Linux, macOS, and Windows CI @@ -140,7 +157,7 @@ The mcpp version is pinned by `.xlings.json`; contributors should not rely on an global mcpp installation. CMP does not track `mcpp.lock`; `.gitignore` enforces that repository policy. Runtime dependencies -belong in `[dependencies]`; test-only dependencies belong in `[dev-dependencies]`. +belong in `[dependencies]`; gtest is declared explicitly under `[dev-dependencies.compat]`. ## Roadmap @@ -148,7 +165,7 @@ Runtime work is split into independently reviewable phases: 1. package identity and importable-module bootstrap — implemented; 2. coroutine task and lifetime semantics — initial `Task` implemented; -3. a minimal single-thread scheduler; +3. a root runner and minimal single-thread scheduler — initially implemented; 4. timers, cancellation, and structured wake-up paths; 5. multi-worker scheduling and work stealing; 6. asynchronous I/O integration and a blocking pool. diff --git a/README.zh.hant.md b/README.zh.hant.md index 97892c2..d400111 100644 --- a/README.zh.hant.md +++ b/README.zh.hant.md @@ -16,8 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP 已提供第一個協程基礎型別:延遲啟動、單一消費者的 `Task` / `Task`。 -> 排程、取消、計時器、非同步 I/O 和公開根任務驅動器尚未實作。 +> CMP 已提供延遲啟動、單一消費者的 `Task` / `Task`,以及在呼叫執行緒運行、 +> 支援明確排程的 `RunLoop`。取消、計時器、非同步 I/O 和 detached 執行尚未實作。 CMP 計畫以標準無堆疊 C++ 協程建構現代協程執行期與函式庫。專案將以明確的 `co_await` 為主線,透過經過驗證的小步驟逐步探索排程、計時器、非同步 I/O、取消,以及阻塞工作的安全隔離。 @@ -67,26 +67,34 @@ cd examples/basic mcpp run ``` -範例會從 `Task` 協程內部印出 `Coroutine result: 42`,然後以成功狀態結束。它負責 -證明獨立 mcpp 套件能夠解析路徑相依、匯入 `mcpplibs.cmp`、組合 Task 並執行同步協程鏈。 +範例會從經過排程的 `Task` 協程內部印出 `Coroutine result: 42`,然後以成功狀態結束。 +它負責證明獨立 mcpp 套件能夠解析路徑相依、匯入 `mcpplibs.cmp`、組合 Task 並透過公開 +RunLoop 驅動任務。 -## 目前 Task API +## 目前 API ```cpp import std; import mcpplibs.cmp; using mcpplibs::cmp::Task; +using mcpplibs::cmp::RunLoop; Task answer() { co_return 42; } -Task print_answer() { +Task print_answer(RunLoop::Scheduler scheduler) { + co_await scheduler.schedule(); auto value = co_await answer(); std::println("Coroutine result: {}", value); co_return; } + +int main() { + RunLoop loop {}; + loop.run(print_answer(loop.get_scheduler())); +} ``` `Task` 採延遲啟動:呼叫 `answer()` 只建立處於暫停狀態的協程,在被 `co_await` 消費時才 @@ -97,9 +105,14 @@ continuation 之間直接轉移,並透過 RAII 銷毀未消費的協程框架 定義協程的轉譯單元必須匯入 `std`,使編譯器能夠看到標準協程協定型別。CMP 私下匯入 `std`,不會向使用端重新匯出整個標準函式庫。 -CMP 尚未提供 `sync_wait` 或排程器,因此目前 API 是協程組合基礎,而不是完整的應用程式入口。 -獨立範例因此定義了一個很小的私有根協程,只適用於其中同步完成的協程鏈。已經被移動的 -Task 不得再次等待。 +`RunLoop::run()` 消費一個根 Task,在呼叫執行緒執行就緒協程,回傳結果並重新拋出例外。 +`Scheduler::schedule()` 始終暫停目前協程並把 continuation 放入佇列。Scheduler 可以複製, +但始終屬於建立它的 RunLoop。支援依序多次呼叫 `run()`,巢狀或並行呼叫會被拒絕。已經被 +移動的 Task 不得再次等待。 + +RunLoop 不是背景執行緒,也不會把阻塞程式碼自動變成非同步程式碼。如果 Task 暫停後沒有 +安排未來的恢復動作,`run()` 可能一直等待。CMP 不提供隱式執行緒親和:外部 awaiter 在其他 +執行緒恢復協程後,需要明確等待目標 Scheduler 才會返回對應 RunLoop。 ## 儲存庫結構 @@ -108,7 +121,10 @@ Task 不得再次等待。 ├── .xlings.json # 固定的專案工具環境 ├── mcpp.toml # 套件識別與測試相依 ├── src/cmp.cppm # 根模組介面 +├── src/task.cppm # Task 模組分割區 +├── src/run_loop.cppm # RunLoop 與 Scheduler 分割區 ├── tests/cmp_test.cpp # Task 契約和生命週期測試 +├── tests/run_loop_test.cpp # 排程、邊界和執行緒測試 ├── examples/basic/ # 獨立的路徑相依 consumer ├── docs/architecture.zh.hant.md # 目前結構、邊界與演進方向 └── .github/workflows/ # Linux、macOS 和 Windows CI @@ -130,7 +146,7 @@ CI 在 Linux、macOS 和 Windows 上執行等價的建構、測試與獨立範 `.xlings.json` 固定,不應依賴無關的全域 mcpp 安裝。 CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定。執行期相依放在 -`[dependencies]`,測試專用相依放在 `[dev-dependencies]`。 +`[dependencies]`,gtest 明確宣告在 `[dev-dependencies.compat]` 中。 ## 路線圖 @@ -138,7 +154,7 @@ CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定 1. 套件識別與可匯入模組 bootstrap——已完成; 2. 協程 task 與生命週期語意——已實作初始 `Task`; -3. 最小單執行緒排程器; +3. 根任務驅動器和最小單執行緒排程器——已完成初始實作; 4. 計時器、取消和結構化喚醒路徑; 5. 多 worker 排程與 work stealing; 6. 非同步 I/O 整合和 blocking pool。 diff --git a/README.zh.md b/README.zh.md index 4b93282..71a0335 100644 --- a/README.zh.md +++ b/README.zh.md @@ -16,8 +16,8 @@ [![ci-windows](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpplibs/cmp/actions/workflows/ci-windows.yml) > [!IMPORTANT] -> CMP 已经提供第一个协程基础类型:懒启动、单消费者的 `Task` / `Task`。 -> 调度、取消、定时器、异步 I/O 和公共根任务驱动器尚未实现。 +> CMP 已提供懒启动、单消费者的 `Task` / `Task`,以及在调用线程运行、支持显式 +> 调度的 `RunLoop`。取消、定时器、异步 I/O 和 detached 执行尚未实现。 CMP 计划基于标准无栈 C++ 协程构建现代协程运行时和库。项目将以显式 `co_await` 为主线, 通过经过验证的小步骤逐步探索调度、定时器、异步 I/O、取消以及阻塞工作的安全隔离。 @@ -67,26 +67,34 @@ cd examples/basic mcpp run ``` -示例会从 `Task` 协程内部打印 `Coroutine result: 42`,然后以成功状态退出。它负责 -证明独立 mcpp 包能够解析路径依赖、导入 `mcpplibs.cmp`、组合 Task 并执行同步协程链。 +示例会从经过调度的 `Task` 协程内部打印 `Coroutine result: 42`,然后以成功状态退出。 +它负责证明独立 mcpp 包能够解析路径依赖、导入 `mcpplibs.cmp`、组合 Task 并通过公共 +RunLoop 驱动任务。 -## 当前 Task API +## 当前 API ```cpp import std; import mcpplibs.cmp; using mcpplibs::cmp::Task; +using mcpplibs::cmp::RunLoop; Task answer() { co_return 42; } -Task print_answer() { +Task print_answer(RunLoop::Scheduler scheduler) { + co_await scheduler.schedule(); auto value = co_await answer(); std::println("Coroutine result: {}", value); co_return; } + +int main() { + RunLoop loop {}; + loop.run(print_answer(loop.get_scheduler())); +} ``` `Task` 采用懒启动:调用 `answer()` 只创建处于挂起状态的协程,在被 `co_await` 消费时才 @@ -97,9 +105,14 @@ continuation 之间直接转移,并通过 RAII 销毁未消费的协程帧。 定义协程的翻译单元必须导入 `std`,使编译器能够看到标准协程协议类型。CMP 私有导入 `std`,不会向使用方重新导出整个标准库。 -CMP 尚未提供 `sync_wait` 或调度器,因此当前 API 是协程组合基础,而不是完整的应用入口。 -独立示例因此定义了一个很小的私有根协程,只适用于其中同步完成的协程链。已经被移动的 -Task 不得再次等待。 +`RunLoop::run()` 消费一个根 Task,在调用线程执行就绪协程,返回结果并重新抛出异常。 +`Scheduler::schedule()` 始终挂起当前协程并把 continuation 放入队列。Scheduler 可以复制, +但始终属于创建它的 RunLoop。支持顺序多次调用 `run()`,嵌套或并发调用会被拒绝。已经被 +移动的 Task 不得再次等待。 + +RunLoop 不是后台线程,也不会把阻塞代码自动变成异步代码。如果 Task 挂起后没有安排未来的 +恢复动作,`run()` 可能一直等待。CMP 不提供隐式线程亲和:外部 awaiter 在其他线程恢复协程 +后,需要显式等待目标 Scheduler 才会返回对应 RunLoop。 ## 仓库结构 @@ -108,7 +121,10 @@ Task 不得再次等待。 ├── .xlings.json # 固定的项目工具环境 ├── mcpp.toml # 包身份和测试依赖 ├── src/cmp.cppm # 根模块接口 +├── src/task.cppm # Task 模块分区 +├── src/run_loop.cppm # RunLoop 与 Scheduler 分区 ├── tests/cmp_test.cpp # Task 契约和生命周期测试 +├── tests/run_loop_test.cpp # 调度、边界和线程测试 ├── examples/basic/ # 独立的路径依赖 consumer ├── docs/architecture.zh.md # 当前结构、边界和演进方向 └── .github/workflows/ # Linux、macOS 和 Windows CI @@ -130,7 +146,7 @@ CI 在 Linux、macOS 和 Windows 上执行等价的构建、测试和独立示 `.xlings.json` 固定,不应依赖无关的全局 mcpp 安装。 CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。运行时依赖放在 -`[dependencies]`,测试专用依赖放在 `[dev-dependencies]`。 +`[dependencies]`,gtest 明确声明在 `[dev-dependencies.compat]` 中。 ## 路线图 @@ -138,7 +154,7 @@ CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。 1. 包身份和可导入模块 bootstrap——已完成; 2. 协程 task 与生命周期语义——已实现初始 `Task`; -3. 最小单线程调度器; +3. 根任务驱动器和最小单线程调度器——已完成初始实现; 4. 定时器、取消和结构化唤醒路径; 5. 多 worker 调度与 work stealing; 6. 异步 I/O 集成和 blocking pool。 diff --git a/docs/architecture.md b/docs/architecture.md index aacc3fd..5ae155e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,15 +4,16 @@ ## Current status -CMP is a C++23 module project whose first runtime primitive is implemented. The root module exports -a lazy, single-consumer `mcpplibs::cmp::Task` with a `Task` specialization. It does not yet -provide a scheduler or a root execution API. +CMP is a C++23 module project with a small coroutine execution core. The root module exports a +lazy, single-consumer `mcpplibs::cmp::Task`, `RunLoop`, and its copyable `Scheduler` handle. +`RunLoop::run()` is the public root execution boundary, while `Scheduler::schedule()` explicitly +returns a suspended coroutine to that loop. The repository contains: - one mcpp package manifest; -- the root module `mcpplibs.cmp` and its Task implementation; -- gtest contract, lifetime, exception, and symmetric-transfer tests; +- the root module `mcpplibs.cmp` with Task and RunLoop partitions; +- gtest contract, lifetime, exception, scheduling, and threading tests; - one standalone path-dependency example; - Linux, macOS, and Windows CI workflows. @@ -38,8 +39,8 @@ There is no `src/main.cpp`, so mcpp infers a library target named `cmp`; the man need a `[lib]` or `[targets.cmp]` override. The C++23 baseline is written explicitly even though it is also mcpp's default. -`gtest = "1.15.2"` is a development dependency used by the test target. CMP does not track an -`mcpp.lock` file; it is excluded by `.gitignore`. +`compat.gtest = "1.15.2"` is an explicitly namespaced development dependency used by the test +targets. CMP does not track an `mcpp.lock` file; it is excluded by `.gitignore`. ## Repository layout @@ -57,17 +58,23 @@ it is also mcpp's default. ├── examples/basic/ │ ├── mcpp.toml │ └── src/main.cpp -├── src/cmp.cppm -├── tests/cmp_test.cpp +├── src/ +│ ├── cmp.cppm +│ ├── task.cppm +│ └── run_loop.cppm +├── tests/ +│ ├── cmp_test.cpp +│ └── run_loop_test.cpp └── mcpp.toml ``` ## Build and tests `.xlings.json` pins the mcpp version used by the project. `mcpp build` builds the inferred library -target. `mcpp test` discovers `tests/cmp_test.cpp`, links the gtest entry point, and verifies the -Task type contract, lazy execution, frame ownership, value and exception propagation, nested -composition, and stack-safe symmetric transfer. +target. `mcpp test` discovers both test files and links a gtest entry point for each. The tests +verify Task ownership and symmetric transfer together with root execution, scheduling, exception +propagation, cross-thread wake-up, invalid scheduler use, loop reuse, and stack-safe repeated +scheduling. Each CI workflow installs the project tools, builds the library, runs the test suite, and runs `examples/basic`. The workflows are separate because tool installation and runner details differ @@ -102,22 +109,29 @@ import std; import mcpplibs.cmp; using mcpplibs::cmp::Task; +using mcpplibs::cmp::RunLoop; Task answer() { co_return 42; } -Task print_answer() { +Task print_answer(RunLoop::Scheduler scheduler) { + co_await scheduler.schedule(); auto value = co_await answer(); std::println("Coroutine result: {}", value); co_return; } + +int main() { + RunLoop loop {}; + loop.run(print_answer(loop.get_scheduler())); +} ``` -This example checks path dependency resolution, module consumption, and external coroutine -compilation independently of the root test target. Its example-private `InlineRunner` starts an -eager root coroutine, which awaits `print_answer()` and produces `Coroutine result: 42`. The helper -is limited to this synchronously completing, scheduler-free chain and is not a CMP public API. +This example checks path dependency resolution, module consumption, external coroutine +compilation, and the public root runner independently of the root test targets. The RunLoop drives +`print_answer()` on the main thread; the explicit scheduling point is reached before the coroutine +prints `Coroutine result: 42`. Any translation unit that defines a coroutine imports `std` itself so `std::coroutine_traits` and the standard coroutine protocol types participate in compilation. The CMP module imports `std` @@ -136,9 +150,31 @@ privately rather than re-exporting the entire standard library. - reference and array result types are rejected. A moved-from Task is empty and must not be awaited. The current implementation terminates on that -contract violation. There is no public `sync_wait`, detached execution, scheduler, thread-affinity -guarantee, timer, cancellation mechanism, asynchronous I/O backend, custom frame allocator, or -blocking-work pool. The module also provides no compatibility alias for the old scaffold module. +contract violation. + +`RunLoop` and `Scheduler` have the following contract: + +- RunLoop is neither copyable nor movable; its identity anchors every Scheduler it creates; +- `run(Task)` consumes one root Task and runs ready continuations on the calling thread; +- a root value, including a move-only value, is returned; a root exception is rethrown; +- a RunLoop can be reused sequentially, but nested and concurrent `run()` calls throw + `std::logic_error`; +- `schedule()` always suspends and appends its continuation to a thread-safe FIFO ready queue; +- producers may enqueue from other threads, but only the thread inside `run()` consumes the queue; +- using a Scheduler after its RunLoop is destroyed, or while its own RunLoop is not active, throws + `std::logic_error` from the await expression; +- root completion with separately queued work is rejected because detached ownership is not part + of this phase. + +RunLoop does not own a worker thread and supplies no automatic thread affinity. An external +awaiter may resume a Task on another thread; awaiting the original Scheduler explicitly returns +the continuation to its RunLoop. A Task that suspends without arranging another thread or event +source to resume it can leave `run()` blocked indefinitely. Blocking functions still block the +thread on which the coroutine currently executes. + +There is no public free-standing `sync_wait`, detached execution, timer, cancellation mechanism, +asynchronous I/O backend, custom frame allocator, or blocking-work pool. The module also provides +no compatibility alias for the old scaffold module. Capturing coroutine lambdas require particular care: invoking a temporary capturing lambda can leave the lazy coroutine referring to a destroyed closure. CMP does not yet provide a helper that @@ -153,16 +189,16 @@ scheduler and do not make a blocking operation asynchronous. The following areas may be considered in separate designs. They are not part of the current package contract: -1. a root runner and minimal single-thread scheduler with explicit scheduling awaiters; -2. structured task scopes and concurrent joins; -3. timers, wake-up paths, and cancellation; -4. multi-worker scheduling and work stealing; -5. asynchronous I/O integrations; -6. a dedicated pool for unavoidable blocking work; -7. result adapters and optional coroutine-frame allocation strategies. +1. structured task scopes and concurrent joins; +2. timers, wake-up paths, and cancellation; +3. multi-worker scheduling and work stealing; +4. asynchronous I/O integrations; +5. a dedicated pool for unavoidable blocking work; +6. result adapters and optional coroutine-frame allocation strategies. -Module partitions or implementation units can be added when an implemented API needs those -boundaries. +Task and RunLoop now occupy separate module partitions because they are implemented public +boundaries. Further partitions or implementation units are added only when another implemented API +needs them. ## Verification @@ -175,9 +211,10 @@ cd examples/basic mcpp run ``` -The expected result is a successful library build, eight passing Task tests, and an example that -exits with status 0. One test performs one million immediate Task completions to check that -symmetric transfer does not grow the native call stack. The current Windows LLVM toolchain does -not emit GNU depfiles. If a file +The expected result is a successful library build, 22 passing tests across two binaries, and an +example that prints `Coroutine result: 42` and exits with status 0. One test performs one million +immediate Task completions; another performs 100,000 explicit scheduling operations. These check +that neither symmetric transfer nor queued scheduling grows the native call stack. The current +Windows LLVM toolchain does not emit GNU depfiles. If a file included by a module interface changes, an incremental build can reuse an older BMI or object; `--cache=off` is used for a full local verification. diff --git a/docs/architecture.zh.hant.md b/docs/architecture.zh.hant.md index 78aaf8d..8da5795 100644 --- a/docs/architecture.zh.hant.md +++ b/docs/architecture.zh.hant.md @@ -4,14 +4,15 @@ ## 目前狀態 -CMP 是一個 C++23 模組專案,已實作第一個執行期基礎型別。根模組匯出延遲啟動、單一消費者的 -`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供排程器或根任務執行 API。 +CMP 是一個具備小型協程執行核心的 C++23 模組專案。根模組匯出延遲啟動、單一消費者的 +`mcpplibs::cmp::Task`、`RunLoop` 及其可複製的 `Scheduler` 控制代碼。`RunLoop::run()` +是公開根任務執行邊界,`Scheduler::schedule()` 用於明確地把暫停協程送回對應執行迴圈。 儲存庫現有內容包括: - 一份 mcpp 套件清單; -- 根模組 `mcpplibs.cmp` 及其 Task 實作; -- 涵蓋契約、生命週期、例外和對稱轉移的 gtest 測試; +- 根模組 `mcpplibs.cmp` 及 Task、RunLoop 模組分割區; +- 涵蓋契約、生命週期、例外、排程和執行緒行為的 gtest 測試; - 一個透過路徑相依使用根套件的獨立範例; - Linux、macOS 和 Windows 三套 CI 工作流程。 @@ -35,8 +36,8 @@ mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.m `src/main.cpp`,因此 mcpp 會推斷出名為 `cmp` 的函式庫目標,不需要額外設定 `[lib]` 或 `[targets.cmp]`。雖然 C++23 也是 mcpp 的預設標準,清單中仍明確寫出這項基線。 -`gtest = "1.15.2"` 是測試使用的開發相依。CMP 目前不追蹤 `mcpp.lock`,該檔案由 -`.gitignore` 排除。 +`compat.gtest = "1.15.2"` 是測試使用的明確命名空間開發相依。CMP 目前不追蹤 +`mcpp.lock`,該檔案由 `.gitignore` 排除。 ## 儲存庫結構 @@ -54,16 +55,22 @@ mcpp 套件由 `mcpplibs` 和 `cmp` 共同識別。使用端在 `[dependencies.m ├── examples/basic/ │ ├── mcpp.toml │ └── src/main.cpp -├── src/cmp.cppm -├── tests/cmp_test.cpp +├── src/ +│ ├── cmp.cppm +│ ├── task.cppm +│ └── run_loop.cppm +├── tests/ +│ ├── cmp_test.cpp +│ └── run_loop_test.cpp └── mcpp.toml ``` ## 建置與測試 `.xlings.json` 固定專案使用的 mcpp 版本。`mcpp build` 建置自動推斷的函式庫目標。 -`mcpp test` 會找到 `tests/cmp_test.cpp`,連結 gtest 進入點,並驗證 Task 型別契約、延遲執行、 -協程框架所有權、值與例外傳播、巢狀組合,以及不會增長呼叫堆疊的對稱轉移。 +`mcpp test` 會找到兩個測試檔案,並為每個檔案連結 gtest 進入點。測試同時驗證 Task 所有權 +和對稱轉移,以及根任務執行、排程、例外傳播、跨執行緒喚醒、無效 Scheduler、RunLoop +重複使用和不會增長呼叫堆疊的重複排程。 三套 CI 工作流程都會安裝專案工具、建置函式庫、執行測試並執行 `examples/basic`。不同 作業系統的工具安裝和執行環境不同,因此分別保留工作流程檔案。 @@ -96,21 +103,28 @@ import std; import mcpplibs.cmp; using mcpplibs::cmp::Task; +using mcpplibs::cmp::RunLoop; Task answer() { co_return 42; } -Task print_answer() { +Task print_answer(RunLoop::Scheduler scheduler) { + co_await scheduler.schedule(); auto value = co_await answer(); std::println("Coroutine result: {}", value); co_return; } + +int main() { + RunLoop loop {}; + loop.run(print_answer(loop.get_scheduler())); +} ``` -這個範例在根測試目標之外,單獨檢查路徑相依解析、模組使用和外部協程編譯。範例私有的 -`InlineRunner` 啟動一個 eager 根協程,等待 `print_answer()` 並輸出 `Coroutine result: 42`。 -這個輔助型別只適用於目前同步完成且沒有排程器的協程鏈,不屬於 CMP 公共 API。 +這個範例在根測試目標之外,單獨檢查路徑相依解析、模組使用、外部協程編譯和公開根任務 +驅動器。RunLoop 在主執行緒驅動 `print_answer()`;協程經過明確排程點後輸出 +`Coroutine result: 42`。 任何定義協程的轉譯單元都要自行匯入 `std`,使 `std::coroutine_traits` 和標準協程協定型別 參與編譯。CMP 模組私下匯入 `std`,而不是向使用端重新匯出整個標準函式庫。 @@ -127,8 +141,27 @@ Task print_answer() { - 未消費的 Task 銷毀自己的框架,消費它的 awaiter 銷毀已完成的框架; - 拒絕參考和陣列結果型別。 -被移動後的 Task 為空,不得再次等待;目前實作會在違反該契約時終止程序。目前沒有公開 -`sync_wait`、detached 執行、排程器、執行緒親和保證、計時器、取消機制、非同步 I/O 後端、 +被移動後的 Task 為空,不得再次等待;目前實作會在違反該契約時終止程序。 + +`RunLoop` 和 `Scheduler` 遵循以下契約: + +- RunLoop 既不能複製也不能移動;它的身分是所有關聯 Scheduler 的生命週期錨點; +- `run(Task)` 消費一個根 Task,並在呼叫執行緒執行就緒 continuation; +- 回傳根任務結果,包括 move-only 結果;根任務例外會重新拋出; +- RunLoop 可以依序重複使用,但巢狀或並行呼叫 `run()` 會拋出 `std::logic_error`; +- `schedule()` 始終暫停,並把 continuation 追加到執行緒安全的 FIFO 就緒佇列; +- 其他執行緒可以入列,但只有正在執行 `run()` 的執行緒會消費佇列; +- RunLoop 銷毀後繼續使用其 Scheduler,或在其所屬 RunLoop 未運行時使用 Scheduler,皆會 + 從 await 運算式拋出 `std::logic_error`; +- 根任務完成時若仍有單獨排隊的工作,RunLoop 會拒絕退出,因為本階段不支援 detached + 所有權。 + +RunLoop 不擁有工作執行緒,也不提供自動執行緒親和。外部 awaiter 可以在其他執行緒恢復 +Task;明確等待原 Scheduler 才會把 continuation 送回對應 RunLoop。如果 Task 暫停後沒有 +安排其他執行緒或事件來源恢復它,`run()` 可能無限等待。阻塞函式仍會阻塞協程目前所在的 +執行緒。 + +目前沒有公開自由函式 `sync_wait`、detached 執行、計時器、取消機制、非同步 I/O 後端、 自訂協程框架 allocator 或阻塞工作執行緒池,也沒有保留舊骨架模組的相容別名。 捕捉變數的協程 lambda 需要特別小心:立即呼叫一個暫時的捕捉 lambda,可能使延遲協程參考 @@ -142,15 +175,15 @@ CMP 名稱中的 `C` 與 Go 執行期中的 `G` 相呼應,但這只說明命 以下方向可以分別設計和審查,目前都不是套件的既有約定: -1. 根任務驅動器、最小單執行緒排程器和明確的排程 awaiter; -2. 結構化任務作用域和並行匯合; -3. 計時器、喚醒路徑和取消; -4. 多工作執行緒排程和工作竊取; -5. 非同步 I/O 整合; -6. 處理無法避免之阻塞工作的專用執行緒池; -7. 結果適配器和可選的協程框架配置策略。 +1. 結構化任務作用域和並行匯合; +2. 計時器、喚醒路徑和取消; +3. 多工作執行緒排程和工作竊取; +4. 非同步 I/O 整合; +5. 處理無法避免之阻塞工作的專用執行緒池; +6. 結果適配器和可選的協程框架配置策略。 -只有已實作的 API 確實需要新邊界時,才增加模組分割區或實作單元。 +Task 與 RunLoop 已形成真實的公開邊界,因此分別位於模組分割區中。只有其他已實作 API +確實需要新邊界時,才繼續增加模組分割區或實作單元。 ## 驗證 @@ -163,7 +196,8 @@ cd examples/basic mcpp run ``` -預期結果是函式庫建置成功、八個 Task 測試通過,而且範例以狀態 0 結束。其中一個測試執行 -一百萬次立即完成的 Task,用於檢查對稱轉移不會增長原生呼叫堆疊。目前 Windows LLVM -工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能沿用舊的 BMI -或目的檔。完整複驗時使用 `--cache=off`。 +預期結果是函式庫建置成功、兩個二進位檔中的 22 項測試全部通過,而且範例輸出 +`Coroutine result: 42` 後以狀態 0 結束。一個測試執行一百萬次立即完成的 Task,另一個 +測試執行十萬次明確排程,用於檢查對稱轉移和佇列排程都不會增長原生呼叫堆疊。目前 +Windows LLVM 工具鏈不會產生 GNU depfile;如果模組介面包含的檔案發生變更,增量建置可能 +沿用舊的 BMI 或目的檔。完整複驗時使用 `--cache=off`。 diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 1c85b21..85c3071 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -4,14 +4,15 @@ ## 当前状态 -CMP 是一个 C++23 模块项目,已经实现首个运行时基础类型。根模块导出懒启动、单消费者的 -`mcpplibs::cmp::Task` 及其 `Task` 特化,但尚未提供调度器或根任务执行 API。 +CMP 是一个具备小型协程执行核心的 C++23 模块项目。根模块导出懒启动、单消费者的 +`mcpplibs::cmp::Task`、`RunLoop` 及其可复制的 `Scheduler` 句柄。`RunLoop::run()` 是 +公共根任务执行边界,`Scheduler::schedule()` 用于显式地把挂起协程送回对应运行循环。 仓库现有内容包括: - 一份 mcpp 包清单; -- 根模块 `mcpplibs.cmp` 及其 Task 实现; -- 覆盖契约、生命周期、异常和对称转移的 gtest 测试; +- 根模块 `mcpplibs.cmp` 及 Task、RunLoop 模块分区; +- 覆盖契约、生命周期、异常、调度和线程行为的 gtest 测试; - 一个通过路径依赖使用根包的独立示例; - Linux、macOS 和 Windows 三套 CI 工作流。 @@ -35,8 +36,8 @@ mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpp `src/main.cpp`,因此 mcpp 会推断出名为 `cmp` 的库目标,不需要额外配置 `[lib]` 或 `[targets.cmp]`。虽然 C++23 也是 mcpp 的默认标准,清单中仍然明确写出这一基线。 -`gtest = "1.15.2"` 是测试使用的开发依赖。CMP 当前不跟踪 `mcpp.lock`,该文件由 -`.gitignore` 排除。 +`compat.gtest = "1.15.2"` 是测试使用的显式命名空间开发依赖。CMP 当前不跟踪 +`mcpp.lock`,该文件由 `.gitignore` 排除。 ## 仓库结构 @@ -54,16 +55,22 @@ mcpp 包由 `mcpplibs` 和 `cmp` 共同标识。使用方在 `[dependencies.mcpp ├── examples/basic/ │ ├── mcpp.toml │ └── src/main.cpp -├── src/cmp.cppm -├── tests/cmp_test.cpp +├── src/ +│ ├── cmp.cppm +│ ├── task.cppm +│ └── run_loop.cppm +├── tests/ +│ ├── cmp_test.cpp +│ └── run_loop_test.cpp └── mcpp.toml ``` ## 构建与测试 `.xlings.json` 固定项目使用的 mcpp 版本。`mcpp build` 构建自动推断的库目标。 -`mcpp test` 发现 `tests/cmp_test.cpp`,链接 gtest 入口,并验证 Task 类型契约、懒执行、 -协程帧所有权、值与异常传播、嵌套组合以及不会增长调用栈的对称转移。 +`mcpp test` 发现两个测试文件,并为每个文件链接 gtest 入口。测试同时验证 Task 所有权和 +对称转移,以及根任务执行、调度、异常传播、跨线程唤醒、无效 Scheduler、RunLoop 复用和 +不会增长调用栈的重复调度。 三套 CI 工作流都会安装项目工具、构建库、运行测试并执行 `examples/basic`。不同操作系统 的工具安装和运行环境不同,因此分别保留工作流文件。 @@ -96,21 +103,28 @@ import std; import mcpplibs.cmp; using mcpplibs::cmp::Task; +using mcpplibs::cmp::RunLoop; Task answer() { co_return 42; } -Task print_answer() { +Task print_answer(RunLoop::Scheduler scheduler) { + co_await scheduler.schedule(); auto value = co_await answer(); std::println("Coroutine result: {}", value); co_return; } + +int main() { + RunLoop loop {}; + loop.run(print_answer(loop.get_scheduler())); +} ``` -该示例在根测试目标之外,单独检查路径依赖解析、模块使用和外部协程编译。示例私有的 -`InlineRunner` 启动一个 eager 根协程,等待 `print_answer()` 并输出 `Coroutine result: 42`。 -这个辅助类型只适用于当前同步完成且没有调度器的协程链,不属于 CMP 公共 API。 +该示例在根测试目标之外,单独检查路径依赖解析、模块使用、外部协程编译和公共根任务驱动器。 +RunLoop 在主线程驱动 `print_answer()`;协程经过显式调度点后输出 +`Coroutine result: 42`。 任何定义协程的翻译单元都要自行导入 `std`,使 `std::coroutine_traits` 和标准协程协议类型 参与编译。CMP 模块私有导入 `std`,而不是向使用方重新导出整个标准库。 @@ -127,8 +141,26 @@ Task print_answer() { - 未消费的 Task 销毁自己的帧,消费它的 awaiter 销毁已经完成的帧; - 拒绝引用和数组结果类型。 -被移动后的 Task 为空,不得再次等待;当前实现会在违反该契约时终止进程。目前没有公共 -`sync_wait`、detached 执行、调度器、线程亲和保证、定时器、取消机制、异步 I/O 后端、 +被移动后的 Task 为空,不得再次等待;当前实现会在违反该契约时终止进程。 + +`RunLoop` 和 `Scheduler` 遵循以下契约: + +- RunLoop 既不能复制也不能移动;它的身份是所有关联 Scheduler 的生命周期锚点; +- `run(Task)` 消费一个根 Task,并在调用线程执行就绪 continuation; +- 返回根任务结果,包括 move-only 结果;根任务异常会重新抛出; +- RunLoop 可以顺序复用,但嵌套或并发调用 `run()` 会抛出 `std::logic_error`; +- `schedule()` 始终挂起,并把 continuation 追加到线程安全的 FIFO 就绪队列; +- 其他线程可以入队,但只有正在执行 `run()` 的线程会消费队列; +- RunLoop 销毁后继续使用其 Scheduler,或在其所属 RunLoop 未运行时使用 Scheduler,都会 + 从 await 表达式抛出 `std::logic_error`; +- 根任务完成时如果仍存在单独排队的工作,RunLoop 会拒绝退出,因为本阶段不支持 detached + 所有权。 + +RunLoop 不拥有工作线程,也不提供自动线程亲和。外部 awaiter 可以在其他线程恢复 Task; +显式等待原 Scheduler 才会把 continuation 送回对应 RunLoop。如果 Task 挂起后没有安排 +其他线程或事件源恢复它,`run()` 可能无限等待。阻塞函数仍会阻塞协程当前所在的线程。 + +目前没有公共自由函数 `sync_wait`、detached 执行、定时器、取消机制、异步 I/O 后端、 自定义协程帧 allocator 或阻塞任务线程池,也没有保留旧脚手架模块的兼容别名。 捕获变量的协程 lambda 需要特别小心:立即调用一个临时的捕获 lambda,可能使懒协程引用 @@ -141,15 +173,15 @@ CMP 名称中的 `C` 与 Go 运行时中的 `G` 相呼应,但这只说明命 以下方向可以分别设计和评审,目前都不是包的既有约定: -1. 根任务驱动器、最小单线程调度器和显式调度 awaiter; -2. 结构化任务作用域和并发汇合; -3. 定时器、唤醒路径和取消; -4. 多工作线程调度和工作窃取; -5. 异步 I/O 集成; -6. 处理不可避免的阻塞工作的专用线程池; -7. 结果适配器和可选的协程帧分配策略。 +1. 结构化任务作用域和并发汇合; +2. 定时器、唤醒路径和取消; +3. 多工作线程调度和工作窃取; +4. 异步 I/O 集成; +5. 处理不可避免的阻塞工作的专用线程池; +6. 结果适配器和可选的协程帧分配策略。 -只有已实现的 API 确实需要新的边界时,才增加模块分区或实现单元。 +Task 与 RunLoop 已经形成真实的公共边界,因此分别位于模块分区中。只有其他已实现 API +确实需要新边界时,才继续增加模块分区或实现单元。 ## 验证 @@ -162,7 +194,8 @@ cd examples/basic mcpp run ``` -预期结果是库构建成功、八个 Task 测试通过,并且示例以状态 0 退出。其中一个测试执行一百万次 -立即完成的 Task,用于检查对称转移不会增长原生调用栈。当前 Windows LLVM 工具链不会生成 -GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI 或目标文件。 -完整复验时使用 `--cache=off`。 +预期结果是库构建成功、两个二进制中的 22 项测试全部通过,并且示例输出 +`Coroutine result: 42` 后以状态 0 退出。一个测试执行一百万次立即完成的 Task,另一个测试 +执行十万次显式调度,用于检查对称转移和队列调度都不会增长原生调用栈。当前 Windows LLVM +工具链不会生成 GNU depfile;如果模块接口包含的文件发生变化,增量构建可能复用旧的 BMI +或目标文件。完整复验时使用 `--cache=off`。 diff --git a/docs/superpowers/plans/2026-08-12-cmp-run-loop.md b/docs/superpowers/plans/2026-08-12-cmp-run-loop.md new file mode 100644 index 0000000..7be25bd --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-cmp-run-loop.md @@ -0,0 +1,41 @@ +# CMP RunLoop v1 Implementation Plan + +**Goal:** Add the smallest scheduler-aware root execution API that makes Task convenient to use +while keeping lifetime, synchronization, and failure handling inside CMP. + +**Toolchain:** C++23 Modules, mcpp/xlings 2026.8.11.2, LLVM 22.1.8, compat.gtest 1.15.2. + +**Git constraint:** Work on `feature/run-loop-v1`; do not stage, commit, or push. The user performs +the commit. + +## Tasks + +- [x] Clean the inherited project state, ignore `.idea/`, pin mcpp 2026.8.11.2, and make the gtest + namespace explicit for current mcpp dependency rules. +- [x] Move the unchanged Task implementation into `mcpplibs.cmp:task` and keep the root module as + the public re-export point. +- [x] Implement `RunLoop`, `RunLoop::Scheduler`, the root coroutine bridge, FIFO ready queue, + condition-variable wake-up, exception propagation, and RAII run-state recovery. +- [x] Add focused tests for values, void, move-only results, exceptions, scheduling, cross-thread + wake-up, direct external-thread completion, sequential reuse, nested/concurrent runs, invalid + Scheduler lifetime, outstanding work, and repeated scheduling. +- [x] Replace the example-private root coroutine with the public RunLoop and print from inside the + scheduled Task. +- [x] Update English, Simplified Chinese, and Traditional Chinese README/architecture documents + and repository-local development skills. +- [x] Run strict clean build, all tests, release build, standalone consumer, diff validation, and + final repository status review. + +## Required Verification + +```bash +mcpp --version +mcpp build --strict --cache=off +mcpp test --strict --cache=off +mcpp build --release --strict --cache=off +cd examples/basic +mcpp run +git diff --check +``` + +The RunLoop test binary is also repeated 100 times to exercise concurrency and wake-up paths. diff --git a/docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md b/docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md new file mode 100644 index 0000000..9dab970 --- /dev/null +++ b/docs/superpowers/specs/2026-08-12-cmp-run-loop-design.md @@ -0,0 +1,121 @@ +# CMP RunLoop v1 Design + +**Date:** 2026-08-12 +**Status:** Implemented and verified, pending user commit +**Base:** `feature/task-v1` at `7e24ee0` + +## Purpose + +Turn `Task` from a composition-only primitive into a usable application root without adding a +thread pool, cancellation model, or detached ownership. Developers should only need to create a +RunLoop, pass its Scheduler where an explicit scheduling point is needed, and call `run()`. + +## Public API + +```cpp +RunLoop loop {}; +auto scheduler = loop.get_scheduler(); +auto result = loop.run(operation(scheduler)); +``` + +The root module exports: + +- non-copyable, non-movable `RunLoop`; +- copyable `RunLoop::Scheduler` handles with identity comparison; +- `Scheduler::schedule()`, an awaitable that always suspends and enqueues the continuation; +- `RunLoop::run(Task)`, returning `T`, supporting `void` and move-only values, and rethrowing + the root exception. + +There is no free-standing `sync_wait()` in this phase. A RunLoop member keeps the blocking root +boundary and the Scheduler that can make progress explicitly connected. + +## Execution Model + +RunLoop owns a shared internal state with a mutex, condition variable, and FIFO deque of coroutine +handles. Scheduler contains only a weak reference to that state. This gives invalid lifetime use a +defined exception instead of leaving a dangling RunLoop pointer. + +`run()` performs these steps: + +1. atomically claims the RunLoop for one active root; +2. creates an internal lazy root coroutine that awaits the supplied Task; +3. enqueues that root and consumes ready handles on the calling thread; +4. waits on the condition variable when no handle is ready; +5. publishes the root value or exception under the same synchronization boundary; +6. releases the RunLoop for sequential reuse and destroys the root frame through RAII. + +Task-to-Task completion continues to use the existing symmetric-transfer contract. Explicit +`schedule()` always goes through the queue, including on the RunLoop thread, so it is a real yield +point and does not grow the native stack. + +## Threading and Lifetime Boundaries + +- One thread at a time may execute `run()` for a RunLoop. +- Other threads may enqueue a suspended continuation through its Scheduler. +- Only the thread executing `run()` consumes the ready queue. +- A root may complete on an external thread; completion publication wakes the RunLoop thread. +- RunLoop supplies no hidden thread affinity. Code explicitly awaits a Scheduler to return to it. +- A Scheduler is valid only while its RunLoop exists and while that RunLoop has an active root. +- Destroying or externally resuming a coroutine after publishing its handle remains the owner's + responsibility; RunLoop stores scheduling references, not frame ownership. + +## Error Policy + +The root Task's own exception is rethrown by `run()`. Detectable contract violations throw +`std::logic_error`: + +- nested or concurrent `run()`; +- scheduling on a RunLoop that is not active; +- scheduling after the RunLoop was destroyed; +- completing the root while separately queued work remains; +- encountering an empty, completed, or otherwise non-runnable queued handle. + +Queue allocation failure propagates normally. RAII resets the active-run flag and drops queued +references before the exception leaves `run()`. + +A Task that suspends without arranging a future resume can keep `run()` blocked indefinitely. +This is expected event-loop behavior; without timers, cancellation, or a work registry, CMP cannot +distinguish a valid external wait from a deadlock. + +## Module Structure + +```text +src/cmp.cppm root module, re-exports public partitions +src/task.cppm existing Task implementation +src/run_loop.cppm RunLoop, Scheduler, root bridge, and queue state +``` + +The implementation imports `std` privately. Consumers defining coroutine functions still import +`std` themselves for the standard coroutine protocol. + +## Deliberately Excluded + +- detached tasks, spawn, and public queue submission; +- `finish()`, `poll()`, `run_one()`, or early stop; +- a dedicated background thread or thread pool; +- timers, cancellation, I/O, and blocking-work isolation; +- automatic Scheduler propagation or thread-local current Scheduler; +- custom allocators and lock-free queues. + +These require ownership or shutdown semantics that do not exist yet. + +## Reference Direction + +The separation between a run loop and a cheap Scheduler follows the proven shape used by +[stdexec](https://github.com/NVIDIA/stdexec/blob/main/include/stdexec/__detail/__run_loop.hpp) and +[libunifex](https://github.com/facebookexperimental/libunifex/blob/main/include/unifex/manual_event_loop.hpp). +The root coroutine/result bridge follows the core technique used by +[cppcoro](https://github.com/lewissbaker/cppcoro/blob/master/include/cppcoro/detail/sync_wait_task.hpp), +while driving the loop during the blocking boundary follows +[Folly](https://github.com/facebook/folly/blob/main/folly/coro/BlockingWait.h). CMP keeps only the +parts required by its current Task contract. + +## Acceptance Criteria + +- Task value, void, move-only result, and exception roots work. +- Scheduling on and back to the calling thread works. +- Cross-thread enqueue and external-thread root completion wake the loop safely. +- Nested, concurrent, expired, wrong-loop, and outstanding-work cases are rejected. +- A RunLoop remains reusable after successful runs and handled failures. +- Repeated scheduling does not grow the stack. +- The standalone consumer prints from inside a scheduled coroutine using only public CMP APIs. diff --git a/examples/basic/src/main.cpp b/examples/basic/src/main.cpp index b70cdc2..fe2ffdc 100644 --- a/examples/basic/src/main.cpp +++ b/examples/basic/src/main.cpp @@ -2,48 +2,21 @@ import std; import mcpplibs.cmp; using mcpplibs::cmp::Task; - -class InlineRunner { -public: - struct promise_type { - [[nodiscard]] InlineRunner get_return_object() const noexcept; - - [[nodiscard]] constexpr std::suspend_never initial_suspend() const noexcept { - return {}; - } - - [[nodiscard]] constexpr std::suspend_never final_suspend() const noexcept { - return {}; - } - - constexpr void return_void() const noexcept {} - - [[noreturn]] void unhandled_exception() const noexcept { - std::terminate(); - } - }; -}; - -InlineRunner InlineRunner::promise_type::get_return_object() const noexcept { - return {}; -} +using mcpplibs::cmp::RunLoop; Task answer() { co_return 42; } -Task print_answer() { +Task print_answer(RunLoop::Scheduler scheduler) { + co_await scheduler.schedule(); auto value = co_await answer(); std::println("Coroutine result: {}", value); co_return; } -InlineRunner run_inline(Task task) { - co_await std::move(task); - co_return; -} - int main() { - run_inline(print_answer()); + RunLoop loop {}; + loop.run(print_answer(loop.get_scheduler())); return 0; } diff --git a/mcpp.toml b/mcpp.toml index 47ad837..d58eccc 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -8,5 +8,5 @@ license = "Apache-2.0" authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/cmp" -[dev-dependencies] +[dev-dependencies.compat] gtest = "1.15.2" diff --git a/src/cmp.cppm b/src/cmp.cppm index 36aca08..b1f1a85 100644 --- a/src/cmp.cppm +++ b/src/cmp.cppm @@ -1,260 +1,4 @@ export module mcpplibs.cmp; -import std; - -export namespace mcpplibs::cmp { - -template -requires ( - std::same_as || - (std::is_object_v && !std::is_array_v) -) -class [[nodiscard]] Task { -public: - struct promise_type { - std::optional result_ {}; - std::exception_ptr exception_ {}; - std::coroutine_handle<> continuation_ { std::noop_coroutine() }; - - [[nodiscard]] Task get_return_object() noexcept; - - [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { - return {}; - } - - class FinalAwaiter { - public: - [[nodiscard]] constexpr bool await_ready() const noexcept { - return false; - } - - [[nodiscard]] std::coroutine_handle<> await_suspend( - std::coroutine_handle coroutine) const noexcept { - return coroutine.promise().continuation_; - } - - constexpr void await_resume() const noexcept {} - }; - - [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { - return {}; - } - - template - requires std::constructible_from - void return_value(U&& value) - noexcept(std::is_nothrow_constructible_v) { - result_.emplace(std::forward(value)); - } - - void unhandled_exception() noexcept { - exception_ = std::current_exception(); - } - }; - -private: - using Handle = std::coroutine_handle; - - class Awaiter { - private: - Handle coroutine_ {}; - - public: - explicit Awaiter(Handle coroutine) noexcept - : coroutine_ { coroutine } {} - - Awaiter(const Awaiter&) = delete; - Awaiter& operator=(const Awaiter&) = delete; - - Awaiter(Awaiter&& other) noexcept - : coroutine_ { std::exchange(other.coroutine_, {}) } {} - - Awaiter& operator=(Awaiter&&) = delete; - - ~Awaiter() { - if (coroutine_) { - coroutine_.destroy(); - } - } - - [[nodiscard]] constexpr bool await_ready() const noexcept { - return false; - } - - [[nodiscard]] std::coroutine_handle<> await_suspend( - std::coroutine_handle<> continuation) noexcept { - coroutine_.promise().continuation_ = continuation; - return coroutine_; - } - - T await_resume() { - auto& promise = coroutine_.promise(); - - if (promise.exception_) { - std::rethrow_exception(promise.exception_); - } - - return std::move(*promise.result_); - } - }; - - Handle coroutine_ {}; - - explicit Task(Handle coroutine) noexcept - : coroutine_ { coroutine } {} - -public: - Task() = delete; - Task(const Task&) = delete; - Task& operator=(const Task&) = delete; - - Task(Task&& other) noexcept - : coroutine_ { std::exchange(other.coroutine_, {}) } {} - - Task& operator=(Task&&) = delete; - - ~Task() { - if (coroutine_) { - coroutine_.destroy(); - } - } - - [[nodiscard]] auto operator co_await() && noexcept { - if (!coroutine_) { - std::terminate(); - } - - return Awaiter { std::exchange(coroutine_, {}) }; - } -}; - -template -requires ( - std::same_as || - (std::is_object_v && !std::is_array_v) -) -Task Task::promise_type::get_return_object() noexcept { - return Task { - std::coroutine_handle::from_promise(*this) - }; -} - -template<> -class [[nodiscard]] Task { -public: - struct promise_type { - std::exception_ptr exception_ {}; - std::coroutine_handle<> continuation_ { std::noop_coroutine() }; - - [[nodiscard]] Task get_return_object() noexcept; - - [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { - return {}; - } - - class FinalAwaiter { - public: - [[nodiscard]] constexpr bool await_ready() const noexcept { - return false; - } - - [[nodiscard]] std::coroutine_handle<> await_suspend( - std::coroutine_handle coroutine) const noexcept { - return coroutine.promise().continuation_; - } - - constexpr void await_resume() const noexcept {} - }; - - [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { - return {}; - } - - constexpr void return_void() const noexcept {} - - void unhandled_exception() noexcept { - exception_ = std::current_exception(); - } - }; - -private: - using Handle = std::coroutine_handle; - - class Awaiter { - private: - Handle coroutine_ {}; - - public: - explicit Awaiter(Handle coroutine) noexcept - : coroutine_ { coroutine } {} - - Awaiter(const Awaiter&) = delete; - Awaiter& operator=(const Awaiter&) = delete; - - Awaiter(Awaiter&& other) noexcept - : coroutine_ { std::exchange(other.coroutine_, {}) } {} - - Awaiter& operator=(Awaiter&&) = delete; - - ~Awaiter() { - if (coroutine_) { - coroutine_.destroy(); - } - } - - [[nodiscard]] constexpr bool await_ready() const noexcept { - return false; - } - - [[nodiscard]] std::coroutine_handle<> await_suspend( - std::coroutine_handle<> continuation) noexcept { - coroutine_.promise().continuation_ = continuation; - return coroutine_; - } - - void await_resume() { - auto& promise = coroutine_.promise(); - - if (promise.exception_) { - std::rethrow_exception(promise.exception_); - } - } - }; - - Handle coroutine_ {}; - - explicit Task(Handle coroutine) noexcept - : coroutine_ { coroutine } {} - -public: - Task() = delete; - Task(const Task&) = delete; - Task& operator=(const Task&) = delete; - - Task(Task&& other) noexcept - : coroutine_ { std::exchange(other.coroutine_, {}) } {} - - Task& operator=(Task&&) = delete; - - ~Task() { - if (coroutine_) { - coroutine_.destroy(); - } - } - - [[nodiscard]] auto operator co_await() && noexcept { - if (!coroutine_) { - std::terminate(); - } - - return Awaiter { std::exchange(coroutine_, {}) }; - } -}; - -inline Task Task::promise_type::get_return_object() noexcept { - return Task { - std::coroutine_handle::from_promise(*this) - }; -} - -} // namespace mcpplibs::cmp +export import :task; +export import :run_loop; diff --git a/src/run_loop.cppm b/src/run_loop.cppm new file mode 100644 index 0000000..5dbe462 --- /dev/null +++ b/src/run_loop.cppm @@ -0,0 +1,333 @@ +export module mcpplibs.cmp:run_loop; + +import std; +import :task; + +namespace mcpplibs::cmp::detail { + +struct RootCompletionBase { + bool completed_ { false }; + std::exception_ptr exception_ {}; +}; + +template +struct RootCompletion final : RootCompletionBase { + std::optional result_ {}; +}; + +template<> +struct RootCompletion final : RootCompletionBase {}; + +class RunLoopState final { +private: + std::mutex mutex_ {}; + std::condition_variable condition_ {}; + std::deque> ready_ {}; + bool running_ { false }; + +public: + void begin_run() { + const std::lock_guard lock { mutex_ }; + + if (running_) { + throw std::logic_error { "run loop is already running" }; + } + + if (!ready_.empty()) { + throw std::logic_error { "run loop contains abandoned work" }; + } + + running_ = true; + } + + void enqueue(std::coroutine_handle<> coroutine) { + if (!coroutine) { + throw std::invalid_argument { "cannot schedule an empty coroutine" }; + } + + { + const std::lock_guard lock { mutex_ }; + + if (!running_) { + throw std::logic_error { "scheduler has no active run" }; + } + + ready_.push_back(coroutine); + } + + condition_.notify_one(); + } + + void complete(RootCompletionBase& completion) noexcept { + // 通过互斥量发布完成状态,以及此前写入的结果或异常 + { + const std::lock_guard lock { mutex_ }; + completion.completed_ = true; + } + + condition_.notify_one(); + } + + void drive(RootCompletionBase& completion) { + while (true) { + std::coroutine_handle<> coroutine {}; + + { + std::unique_lock lock { mutex_ }; + condition_.wait(lock, [&] { + return completion.completed_ || !ready_.empty(); + }); + + if (completion.completed_) { + if (!ready_.empty()) { + throw std::logic_error { + "root task completed with outstanding work" + }; + } + + running_ = false; + return; + } + + coroutine = ready_.front(); + ready_.pop_front(); + } + + if (!coroutine || coroutine.done()) { + throw std::logic_error { "run loop contains a non-runnable coroutine" }; + } + + // 锁外恢复,协程再次入队时不会与 RunLoop 自锁 + coroutine.resume(); + } + } + + void abort_run() noexcept { + const std::lock_guard lock { mutex_ }; + ready_.clear(); + running_ = false; + } +}; + +class RunGuard final { +private: + std::shared_ptr state_ {}; + bool active_ { true }; + +public: + explicit RunGuard(std::shared_ptr state) + : state_ { std::move(state) } { + state_->begin_run(); + } + + RunGuard(const RunGuard&) = delete; + RunGuard& operator=(const RunGuard&) = delete; + RunGuard(RunGuard&&) = delete; + RunGuard& operator=(RunGuard&&) = delete; + + ~RunGuard() { + if (active_) { + state_->abort_run(); + } + } + + void release() noexcept { + active_ = false; + } +}; + +class RootOperation final { +public: + struct promise_type { + RunLoopState* state_ {}; + RootCompletionBase* completion_ {}; + + [[nodiscard]] RootOperation get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + class FinalAwaiter final { + public: + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + void await_suspend( + std::coroutine_handle coroutine) const noexcept { + auto* const state = coroutine.promise().state_; + auto* const completion = coroutine.promise().completion_; + state->complete(*completion); + } + + constexpr void await_resume() const noexcept {} + }; + + [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { + return {}; + } + + constexpr void return_void() const noexcept {} + + void unhandled_exception() noexcept { + completion_->exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + Handle coroutine_ {}; + + explicit RootOperation(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + RootOperation(const RootOperation&) = delete; + RootOperation& operator=(const RootOperation&) = delete; + + RootOperation(RootOperation&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + RootOperation& operator=(RootOperation&&) = delete; + + ~RootOperation() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + void bind(RunLoopState& state, RootCompletionBase& completion) noexcept { + coroutine_.promise().state_ = &state; + coroutine_.promise().completion_ = &completion; + } + + [[nodiscard]] std::coroutine_handle<> handle() const noexcept { + return coroutine_; + } +}; + +inline RootOperation RootOperation::promise_type::get_return_object() noexcept { + return RootOperation { + std::coroutine_handle::from_promise(*this) + }; +} + +template +RootOperation make_root_operation(Task task, RootCompletion& completion) { + if constexpr (std::same_as) { + co_await std::move(task); + } else { + completion.result_.emplace(co_await std::move(task)); + } +} + +} // namespace mcpplibs::cmp::detail + +export namespace mcpplibs::cmp { + +class RunLoop final { +public: + class Scheduler final { + private: + class ScheduleAwaiter final { + private: + std::weak_ptr state_ {}; + + public: + explicit ScheduleAwaiter( + std::weak_ptr state) noexcept + : state_ { std::move(state) } {} + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + void await_suspend(std::coroutine_handle<> continuation) { + // 入队后协程可能立即恢复,因此先把状态保存到当前线程的栈上。 + const auto state = state_.lock(); + + if (!state) { + throw std::logic_error { "scheduler's run loop no longer exists" }; + } + + state->enqueue(continuation); + } + + constexpr void await_resume() const noexcept {} + }; + + std::weak_ptr state_ {}; + + explicit Scheduler( + const std::shared_ptr& state) noexcept + : state_ { state } {} + + friend class RunLoop; + + public: + Scheduler() = delete; + Scheduler(const Scheduler&) = default; + Scheduler& operator=(const Scheduler&) = default; + Scheduler(Scheduler&&) noexcept = default; + Scheduler& operator=(Scheduler&&) noexcept = default; + ~Scheduler() = default; + + [[nodiscard]] auto schedule() const noexcept { + return ScheduleAwaiter { state_ }; + } + + friend bool operator==( + const Scheduler& left, + const Scheduler& right) noexcept { + return !left.state_.owner_before(right.state_) && + !right.state_.owner_before(left.state_); + } + }; + +private: + std::shared_ptr state_ { + std::make_shared() + }; + +public: + RunLoop() = default; + RunLoop(const RunLoop&) = delete; + RunLoop& operator=(const RunLoop&) = delete; + RunLoop(RunLoop&&) = delete; + RunLoop& operator=(RunLoop&&) = delete; + ~RunLoop() = default; + + [[nodiscard]] Scheduler get_scheduler() const noexcept { + return Scheduler { state_ }; + } + + template + T run(Task task) { + detail::RootCompletion completion {}; + detail::RunGuard guard { state_ }; + auto operation = detail::make_root_operation( + std::move(task), + completion); + + operation.bind(*state_, completion); + state_->enqueue(operation.handle()); + state_->drive(completion); + guard.release(); + + if (completion.exception_) { + std::rethrow_exception(completion.exception_); + } + + if constexpr (!std::same_as) { + if (!completion.result_) { + throw std::logic_error { "root task completed without a result" }; + } + + return std::move(*completion.result_); + } + } +}; + +} // namespace mcpplibs::cmp diff --git a/src/task.cppm b/src/task.cppm new file mode 100644 index 0000000..f2df590 --- /dev/null +++ b/src/task.cppm @@ -0,0 +1,261 @@ +export module mcpplibs.cmp:task; + +import std; + +export namespace mcpplibs::cmp { + +// 完成时对称转移到等待方,避免嵌套 Task 持续增长原生调用栈 +template +requires ( + std::same_as || + (std::is_object_v && !std::is_array_v) +) +class [[nodiscard]] Task { +public: + struct promise_type { + std::optional result_ {}; + std::exception_ptr exception_ {}; + std::coroutine_handle<> continuation_ { std::noop_coroutine() }; + + [[nodiscard]] Task get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + class FinalAwaiter { + public: + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle coroutine) const noexcept { + return coroutine.promise().continuation_; + } + + constexpr void await_resume() const noexcept {} + }; + + [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { + return {}; + } + + template + requires std::constructible_from + void return_value(U&& value) + noexcept(std::is_nothrow_constructible_v) { + result_.emplace(std::forward(value)); + } + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + class Awaiter { + private: + Handle coroutine_ {}; + + public: + explicit Awaiter(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + + Awaiter(const Awaiter&) = delete; + Awaiter& operator=(const Awaiter&) = delete; + + Awaiter(Awaiter&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Awaiter& operator=(Awaiter&&) = delete; + + ~Awaiter() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle<> continuation) noexcept { + coroutine_.promise().continuation_ = continuation; + return coroutine_; + } + + T await_resume() { + auto& promise = coroutine_.promise(); + + if (promise.exception_) { + std::rethrow_exception(promise.exception_); + } + + return std::move(*promise.result_); + } + }; + + Handle coroutine_ {}; + + explicit Task(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + Task() = delete; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task(Task&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Task& operator=(Task&&) = delete; + + ~Task() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] auto operator co_await() && noexcept { + if (!coroutine_) { + std::terminate(); + } + + return Awaiter { std::exchange(coroutine_, {}) }; + } +}; + +template +requires ( + std::same_as || + (std::is_object_v && !std::is_array_v) +) +Task Task::promise_type::get_return_object() noexcept { + return Task { + std::coroutine_handle::from_promise(*this) + }; +} + +template<> +class [[nodiscard]] Task { +public: + struct promise_type { + std::exception_ptr exception_ {}; + std::coroutine_handle<> continuation_ { std::noop_coroutine() }; + + [[nodiscard]] Task get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_always initial_suspend() const noexcept { + return {}; + } + + class FinalAwaiter { + public: + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle coroutine) const noexcept { + return coroutine.promise().continuation_; + } + + constexpr void await_resume() const noexcept {} + }; + + [[nodiscard]] constexpr FinalAwaiter final_suspend() const noexcept { + return {}; + } + + constexpr void return_void() const noexcept {} + + void unhandled_exception() noexcept { + exception_ = std::current_exception(); + } + }; + +private: + using Handle = std::coroutine_handle; + + class Awaiter { + private: + Handle coroutine_ {}; + + public: + explicit Awaiter(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + + Awaiter(const Awaiter&) = delete; + Awaiter& operator=(const Awaiter&) = delete; + + Awaiter(Awaiter&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Awaiter& operator=(Awaiter&&) = delete; + + ~Awaiter() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + [[nodiscard]] std::coroutine_handle<> await_suspend( + std::coroutine_handle<> continuation) noexcept { + coroutine_.promise().continuation_ = continuation; + return coroutine_; + } + + void await_resume() { + auto& promise = coroutine_.promise(); + + if (promise.exception_) { + std::rethrow_exception(promise.exception_); + } + } + }; + + Handle coroutine_ {}; + + explicit Task(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + Task() = delete; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task(Task&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + Task& operator=(Task&&) = delete; + + ~Task() { + if (coroutine_) { + coroutine_.destroy(); + } + } + + [[nodiscard]] auto operator co_await() && noexcept { + if (!coroutine_) { + std::terminate(); + } + + return Awaiter { std::exchange(coroutine_, {}) }; + } +}; + +inline Task Task::promise_type::get_return_object() noexcept { + return Task { + std::coroutine_handle::from_promise(*this) + }; +} + +} // namespace mcpplibs::cmp diff --git a/tests/cmp_test.cpp b/tests/cmp_test.cpp index d6208d6..8a6681d 100644 --- a/tests/cmp_test.cpp +++ b/tests/cmp_test.cpp @@ -1,8 +1,8 @@ -#include - import std; import mcpplibs.cmp; +#include + namespace { using mcpplibs::cmp::Task; diff --git a/tests/run_loop_test.cpp b/tests/run_loop_test.cpp new file mode 100644 index 0000000..91b6e48 --- /dev/null +++ b/tests/run_loop_test.cpp @@ -0,0 +1,366 @@ +import std; +import mcpplibs.cmp; + +#include + +namespace { + +using mcpplibs::cmp::RunLoop; +using mcpplibs::cmp::Task; + +using Scheduler = RunLoop::Scheduler; + +static_assert(std::default_initializable); +static_assert(!std::copy_constructible); +static_assert(!std::move_constructible); +static_assert(!std::is_copy_assignable_v); +static_assert(!std::is_move_assignable_v); + +static_assert(!std::default_initializable); +static_assert(std::copy_constructible); +static_assert(std::move_constructible); +static_assert(std::equality_comparable); + +Task lazy_value(bool& started) { + started = true; + co_return 42; +} + +Task increment(int& value) { + ++value; + co_return; +} + +Task> make_move_only_value() { + co_return std::make_unique(7); +} + +Task do_nothing() { + co_return; +} + +Task schedule_once(Scheduler scheduler) { + co_await scheduler.schedule(); + co_return std::this_thread::get_id(); +} + +Task schedule_many_times(Scheduler scheduler, int count) { + for (int index { 0 }; index < count; ++index) { + co_await scheduler.schedule(); + } + + co_return count; +} + +Task fail_after_scheduling(Scheduler scheduler) { + co_await scheduler.schedule(); + throw std::runtime_error { "scheduled task failed" }; +} + +Task run_nested(RunLoop& loop) { + loop.run(do_nothing()); + co_return; +} + +class ResumeOnNewThread final { +private: + std::jthread* worker_ {}; + +public: + explicit ResumeOnNewThread(std::jthread& worker) noexcept + : worker_ { &worker } {} + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + void await_suspend(std::coroutine_handle<> continuation) const { + // 新线程可能立即恢复协程,因此启动后不再读取 awaiter 成员。 + auto* const worker = worker_; + *worker = std::jthread { [continuation] { + continuation.resume(); + } }; + } + + constexpr void await_resume() const noexcept {} +}; + +Task> leave_and_return( + Scheduler scheduler, + std::jthread& worker) { + co_await ResumeOnNewThread { worker }; + const auto workerThread = std::this_thread::get_id(); + + co_await scheduler.schedule(); + co_return std::pair { workerThread, std::this_thread::get_id() }; +} + +Task finish_on_new_thread(std::jthread& worker) { + co_await ResumeOnNewThread { worker }; + co_return std::this_thread::get_id(); +} + +class ManualSuspend final { +private: + std::coroutine_handle<>* continuation_ {}; + std::latch* suspended_ {}; + +public: + ManualSuspend( + std::coroutine_handle<>& continuation, + std::latch& suspended) noexcept + : continuation_ { &continuation }, suspended_ { &suspended } {} + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + void await_suspend(std::coroutine_handle<> continuation) const noexcept { + *continuation_ = continuation; + suspended_->count_down(); + } + + constexpr void await_resume() const noexcept {} +}; + +Task suspend_manually( + std::coroutine_handle<>& continuation, + std::latch& suspended) { + co_await ManualSuspend { continuation, suspended }; +} + +class EagerOperation final { +public: + struct promise_type { + [[nodiscard]] EagerOperation get_return_object() noexcept; + + [[nodiscard]] constexpr std::suspend_never initial_suspend() const noexcept { + return {}; + } + + [[nodiscard]] constexpr std::suspend_always final_suspend() const noexcept { + return {}; + } + + constexpr void return_void() const noexcept {} + + [[noreturn]] void unhandled_exception() const noexcept { + std::terminate(); + } + }; + +private: + using Handle = std::coroutine_handle; + + Handle coroutine_ {}; + + explicit EagerOperation(Handle coroutine) noexcept + : coroutine_ { coroutine } {} + +public: + EagerOperation(const EagerOperation&) = delete; + EagerOperation& operator=(const EagerOperation&) = delete; + + EagerOperation(EagerOperation&& other) noexcept + : coroutine_ { std::exchange(other.coroutine_, {}) } {} + + EagerOperation& operator=(EagerOperation&&) = delete; + + ~EagerOperation() { + if (coroutine_) { + coroutine_.destroy(); + } + } +}; + +EagerOperation EagerOperation::promise_type::get_return_object() noexcept { + return EagerOperation { + EagerOperation::Handle::from_promise(*this) + }; +} + +EagerOperation enqueue_external_work(Scheduler scheduler) { + co_await scheduler.schedule(); +} + +Task leave_outstanding_work( + Scheduler scheduler, + std::optional& operation) { + operation.emplace(enqueue_external_work(std::move(scheduler))); + co_return; +} + +Scheduler make_expired_scheduler() { + RunLoop loop {}; + return loop.get_scheduler(); +} + +TEST(CmpRunLoopTest, RunsLazyValueAndVoidTasks) { + RunLoop loop {}; + bool started { false }; + int value { 0 }; + auto task = lazy_value(started); + + EXPECT_FALSE(started); + EXPECT_EQ(loop.run(std::move(task)), 42); + EXPECT_TRUE(started); + + loop.run(increment(value)); + EXPECT_EQ(value, 1); +} + +TEST(CmpRunLoopTest, ReturnsMoveOnlyValues) { + RunLoop loop {}; + auto value = loop.run(make_move_only_value()); + + ASSERT_NE(value, nullptr); + EXPECT_EQ(*value, 7); +} + +TEST(CmpRunLoopTest, SchedulerIdentityMatchesItsRunLoop) { + RunLoop first {}; + RunLoop second {}; + + EXPECT_EQ(first.get_scheduler(), first.get_scheduler()); + EXPECT_NE(first.get_scheduler(), second.get_scheduler()); +} + +TEST(CmpRunLoopTest, SchedulingResumesOnTheCallingThread) { + RunLoop loop {}; + const auto callingThread = std::this_thread::get_id(); + auto awaiter = loop.get_scheduler().schedule(); + + EXPECT_FALSE(awaiter.await_ready()); + EXPECT_EQ(loop.run(schedule_once(loop.get_scheduler())), callingThread); +} + +TEST(CmpRunLoopTest, SchedulingReturnsFromAnotherThread) { + RunLoop loop {}; + std::jthread worker {}; + const auto callingThread = std::this_thread::get_id(); + + const auto [workerThread, resumedThread] = loop.run( + leave_and_return(loop.get_scheduler(), worker)); + + if (worker.joinable()) { + worker.join(); + } + + EXPECT_NE(workerThread, callingThread); + EXPECT_EQ(resumedThread, callingThread); +} + +TEST(CmpRunLoopTest, RootCompletionWakesTheCallingThread) { + RunLoop loop {}; + std::jthread worker {}; + const auto callingThread = std::this_thread::get_id(); + + const auto completionThread = loop.run(finish_on_new_thread(worker)); + + if (worker.joinable()) { + worker.join(); + } + + EXPECT_NE(completionThread, callingThread); +} + +TEST(CmpRunLoopTest, PropagatesExceptionAfterScheduling) { + RunLoop loop {}; + bool started { false }; + + EXPECT_THROW( + loop.run(fail_after_scheduling(loop.get_scheduler())), + std::runtime_error); + + EXPECT_EQ(loop.run(lazy_value(started)), 42); + EXPECT_TRUE(started); +} + +TEST(CmpRunLoopTest, ReusesTheLoopSequentially) { + RunLoop loop {}; + bool firstStarted { false }; + bool secondStarted { false }; + + EXPECT_EQ(loop.run(lazy_value(firstStarted)), 42); + EXPECT_EQ(loop.run(lazy_value(secondStarted)), 42); + EXPECT_TRUE(firstStarted); + EXPECT_TRUE(secondStarted); +} + +TEST(CmpRunLoopTest, RejectsNestedRunAndRemainsReusable) { + RunLoop loop {}; + bool started { false }; + + EXPECT_THROW(loop.run(run_nested(loop)), std::logic_error); + EXPECT_EQ(loop.run(lazy_value(started)), 42); + EXPECT_TRUE(started); +} + +TEST(CmpRunLoopTest, RejectsConcurrentRunAndRemainsReusable) { + RunLoop loop {}; + std::coroutine_handle<> continuation {}; + std::latch suspended { 1 }; + std::exception_ptr driverException {}; + + std::jthread driver { [&] { + try { + loop.run(suspend_manually(continuation, suspended)); + } catch (...) { + driverException = std::current_exception(); + } + } }; + + suspended.wait(); + ASSERT_TRUE(continuation); + EXPECT_THROW(loop.run(do_nothing()), std::logic_error); + + continuation.resume(); + driver.join(); + + EXPECT_FALSE(driverException); + loop.run(do_nothing()); +} + +TEST(CmpRunLoopTest, RejectsSchedulerWithoutItsActiveRun) { + RunLoop owner {}; + RunLoop driver {}; + + EXPECT_THROW( + driver.run(schedule_once(owner.get_scheduler())), + std::logic_error); +} + +TEST(CmpRunLoopTest, RejectsSchedulerAfterRunLoopDestruction) { + RunLoop driver {}; + + EXPECT_THROW( + driver.run(schedule_once(make_expired_scheduler())), + std::logic_error); +} + +TEST(CmpRunLoopTest, RejectsOutstandingWorkAndRemainsReusable) { + RunLoop loop {}; + std::optional operation {}; + + EXPECT_THROW( + loop.run(leave_outstanding_work( + loop.get_scheduler(), + operation)), + std::logic_error); + + operation.reset(); + loop.run(do_nothing()); +} + +TEST(CmpRunLoopTest, RepeatedSchedulingDoesNotGrowTheStack) { + constexpr int SCHEDULE_COUNT { 100'000 }; + RunLoop loop {}; + + EXPECT_EQ( + loop.run(schedule_many_times( + loop.get_scheduler(), + SCHEDULE_COUNT)), + SCHEDULE_COUNT); +} + +} // namespace From c1fa32d776ba783eef1a566f9b1329a67fa75320 Mon Sep 17 00:00:00 2001 From: HikariTish Date: Wed, 12 Aug 2026 20:38:18 +0800 Subject: [PATCH 3/5] ci: align xlings version with project toolchain --- .github/workflows/ci-linux.yml | 2 +- .github/workflows/ci-macos.yml | 2 +- .github/workflows/ci-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 3a811a3..0f8fc66 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -25,7 +25,7 @@ env: # Pinned rather than "newest": a bootstrap that floats turns an upstream # release into a red build on an unrelated PR. Floor is xlings 0.4.69 # (the index keys by (namespace, name) from there on) — never pin below it. - XLINGS_VERSION: v2026.7.28.4 + XLINGS_VERSION: v2026.8.11.2 XLINGS_NON_INTERACTIVE: '1' jobs: diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index a33d338..6575740 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: true env: - XLINGS_VERSION: v2026.7.28.4 + XLINGS_VERSION: v2026.8.11.2 XLINGS_NON_INTERACTIVE: '1' jobs: diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index e4cb52f..8f5ca3a 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -22,7 +22,7 @@ concurrency: env: # Both installers read this: the shell one takes it as an argument or an env # var, the PowerShell one defaults its -Version parameter to it. - XLINGS_VERSION: v2026.7.28.4 + XLINGS_VERSION: v2026.8.11.2 XLINGS_NON_INTERACTIVE: '1' jobs: From 0aca4b23f53c400551409fd842c9b1adaf626248 Mon Sep 17 00:00:00 2001 From: HikariTish Date: Wed, 12 Aug 2026 20:44:57 +0800 Subject: [PATCH 4/5] test: fix std module compatibility --- tests/cmp_test.cpp | 4 ++-- tests/run_loop_test.cpp | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/cmp_test.cpp b/tests/cmp_test.cpp index 8a6681d..d6208d6 100644 --- a/tests/cmp_test.cpp +++ b/tests/cmp_test.cpp @@ -1,8 +1,8 @@ +#include + import std; import mcpplibs.cmp; -#include - namespace { using mcpplibs::cmp::Task; diff --git a/tests/run_loop_test.cpp b/tests/run_loop_test.cpp index 91b6e48..0dec5ef 100644 --- a/tests/run_loop_test.cpp +++ b/tests/run_loop_test.cpp @@ -1,8 +1,8 @@ +#include + import std; import mcpplibs.cmp; -#include - namespace { using mcpplibs::cmp::RunLoop; @@ -64,10 +64,10 @@ Task run_nested(RunLoop& loop) { class ResumeOnNewThread final { private: - std::jthread* worker_ {}; + std::thread* worker_ {}; public: - explicit ResumeOnNewThread(std::jthread& worker) noexcept + explicit ResumeOnNewThread(std::thread& worker) noexcept : worker_ { &worker } {} [[nodiscard]] constexpr bool await_ready() const noexcept { @@ -77,7 +77,7 @@ class ResumeOnNewThread final { void await_suspend(std::coroutine_handle<> continuation) const { // 新线程可能立即恢复协程,因此启动后不再读取 awaiter 成员。 auto* const worker = worker_; - *worker = std::jthread { [continuation] { + *worker = std::thread { [continuation] { continuation.resume(); } }; } @@ -87,7 +87,7 @@ class ResumeOnNewThread final { Task> leave_and_return( Scheduler scheduler, - std::jthread& worker) { + std::thread& worker) { co_await ResumeOnNewThread { worker }; const auto workerThread = std::this_thread::get_id(); @@ -95,7 +95,7 @@ Task> leave_and_return( co_return std::pair { workerThread, std::this_thread::get_id() }; } -Task finish_on_new_thread(std::jthread& worker) { +Task finish_on_new_thread(std::thread& worker) { co_await ResumeOnNewThread { worker }; co_return std::this_thread::get_id(); } @@ -236,7 +236,7 @@ TEST(CmpRunLoopTest, SchedulingResumesOnTheCallingThread) { TEST(CmpRunLoopTest, SchedulingReturnsFromAnotherThread) { RunLoop loop {}; - std::jthread worker {}; + std::thread worker {}; const auto callingThread = std::this_thread::get_id(); const auto [workerThread, resumedThread] = loop.run( @@ -252,7 +252,7 @@ TEST(CmpRunLoopTest, SchedulingReturnsFromAnotherThread) { TEST(CmpRunLoopTest, RootCompletionWakesTheCallingThread) { RunLoop loop {}; - std::jthread worker {}; + std::thread worker {}; const auto callingThread = std::this_thread::get_id(); const auto completionThread = loop.run(finish_on_new_thread(worker)); @@ -302,7 +302,7 @@ TEST(CmpRunLoopTest, RejectsConcurrentRunAndRemainsReusable) { std::latch suspended { 1 }; std::exception_ptr driverException {}; - std::jthread driver { [&] { + std::thread driver { [&] { try { loop.run(suspend_manually(continuation, suspended)); } catch (...) { From bb9028dbb620f681a1562f2e4c7e3692e7222c6e Mon Sep 17 00:00:00 2001 From: HikariTish Date: Wed, 12 Aug 2026 20:55:22 +0800 Subject: [PATCH 5/5] build: pin LLVM 22.1.8 toolchain --- mcpp.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mcpp.toml b/mcpp.toml index d58eccc..4474dd3 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -8,5 +8,8 @@ license = "Apache-2.0" authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/cmp" +[toolchain] +default = "llvm@22.1.8" + [dev-dependencies.compat] gtest = "1.15.2"