From 2d728100a55dbe4d8d147ccad00be0748916fe4c Mon Sep 17 00:00:00 2001 From: Jinser Kafka Date: Thu, 12 Feb 2026 16:40:27 +0800 Subject: [PATCH] wip --- docs/coroutine_design.md | 69 +++++++ docs/cpr_thread_pool_analysis.md | 178 ++++++++++++++++++ docs/http_concurrency_options.md | 137 ++++++++++++++ docs/platform_compatibility.md | 203 ++++++++++++++++++++ docs/windows_testing_guide.md | 196 ++++++++++++++++++++ src/goldfish.hpp | 267 ++++++++++++++++++++++++++- tests/test_async_http.scm | 62 +++++++ tests/test_async_vs_sync.scm | 105 +++++++++++ tests/test_concurrent_work.scm | 71 +++++++ tests/test_coroutine.scm | 51 +++++ tests/test_cpr_concurrency_limit.scm | 78 ++++++++ tests/test_real_async_http.scm | 101 ++++++++++ xmake.lua | 22 +++ 13 files changed, 1537 insertions(+), 3 deletions(-) create mode 100644 docs/coroutine_design.md create mode 100644 docs/cpr_thread_pool_analysis.md create mode 100644 docs/http_concurrency_options.md create mode 100644 docs/platform_compatibility.md create mode 100644 docs/windows_testing_guide.md create mode 100644 tests/test_async_http.scm create mode 100644 tests/test_async_vs_sync.scm create mode 100644 tests/test_concurrent_work.scm create mode 100644 tests/test_coroutine.scm create mode 100644 tests/test_cpr_concurrency_limit.scm create mode 100644 tests/test_real_async_http.scm diff --git a/docs/coroutine_design.md b/docs/coroutine_design.md new file mode 100644 index 00000000..6dbc4d75 --- /dev/null +++ b/docs/coroutine_design.md @@ -0,0 +1,69 @@ +# Goldfish 协程设计文档 + +## 当前实现 + +### 架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 主线程 (Main Thread) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Scheme 代码执行环境 │ │ +│ │ • 所有 s7_call() 在这里执行 │ │ +│ │ • 保证线程安全(无竞争条件) │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────┼───────────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ g_coroutine-run g_http-async-get g_coroutine-wait │ +│ (任务队列) (I/O 并发) (处理回调) │ +└─────────────────────────────────────────────────────────────┘ + │ + │ marl::schedule() + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ marl 调度器 (4 个 worker 线程) │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ Fiber 1 │ │ Fiber 2 │ │ Fiber 3 (HTTP) │ │ +│ │ g_coroutine │ │ g_coroutine │ │ future->get() │ │ +│ │ -sleep 等待 │ │ -sleep 等待 │ │ 等待 HTTP 响应 │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +│ │ +│ • Fiber 可以在 I/O 等待时让出执行权 │ +│ • 但 Scheme 代码始终在主线程执行 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 两个层面的"异步" + +### 1. Scheme 代码层面(当前:顺序) + +```scheme +(g_coroutine-run (lambda () (do-work-1))) ; 加入队列 +(g_coroutine-run (lambda () (do-work-2))) ; 加入队列 +(g_coroutine-wait) ; 顺序执行 +``` + +**结果**: work-1 和 work-2 在主线程顺序执行 + +### 2. C++ I/O 层面(真正并发) + +```scheme +(g_http-async-get url1 '() callback1) ; 创建 fiber,立即返回 +(g_http-async-get url2 '() callback2) ; 创建 fiber,立即返回 +(g_coroutine-sleep 0.1) ; 让出,允许 fiber 运行 +(g_coroutine-wait) ; 处理完成的回调 +``` + +**结果**: HTTP 请求在后台并发执行 + +## 结论 + +Goldfish 的协程设计是:**Scheme 代码单线程,C++ I/O 多线程并发**。 + +这是为了平衡: +- 安全性(避免 s7 线程问题) +- 实用性(HTTP 并发是最常见的需求) +- 简单性(不需要复杂的锁机制) diff --git a/docs/cpr_thread_pool_analysis.md b/docs/cpr_thread_pool_analysis.md new file mode 100644 index 00000000..78ca386f --- /dev/null +++ b/docs/cpr_thread_pool_analysis.md @@ -0,0 +1,178 @@ +# CPR 线程池与 Marl 协程的交互分析 + +## CPR 的异步实现机制 + +根据 issue 描述,CPR 的异步实现基于: + +1. **线程池**:最小 1 个线程,最大 = CPU 核心数 +2. **实现方式**:每个请求占用一个线程进行 `poll` 等待 +3. **限制**:并发请求数受限于 CPU 核心数 + +## 我们的实现方式 + +```cpp +// f_http_async_get 实现 +auto future = std::make_shared(cpr::GetAsync(cpr::Url(url))); + +marl::schedule([sc, callback, gc_loc, future]() mutable { + cpr::Response r = future->get(); // 在 marl fiber 中阻塞等待 + // ... 回调 +}); +``` + +## 交互分析 + +### 场景:CPU 4 核心,发起 6 个并发 HTTP 请求 + +``` +CPR 线程池(4 线程) Marl 调度器(4 fiber) + │ │ + ├─ 请求 1 ── 线程 1 ──────────┼─ Fiber 1 ── future->get() [等待] + ├─ 请求 2 ── 线程 2 ──────────┼─ Fiber 2 ── future->get() [等待] + ├─ 请求 3 ── 线程 3 ──────────┼─ Fiber 3 ── future->get() [等待] + ├─ 请求 4 ── 线程 4 ──────────┼─ Fiber 4 ── future->get() [等待] + │ │ + ├─ 请求 5 ── [等待线程池] ────┼─ Fiber 5 ── future->get() [阻塞] + ├─ 请求 6 ── [等待线程池] ────┼─ Fiber 6 ── future->get() [阻塞] +``` + +**问题**: +- CPR 线程池满了(4/4),请求 5、6 在 CPR 队列中等待 +- Marl fiber 5、6 也在等待 `future->get()` +- 即使 Marl 有更多调度能力,也被 CPR 的线程池限制了 + +### 测试结果解释 + +之前的测试结果: +``` +Total time: 3.9 seconds +Expected if concurrent: ~2.0 seconds +``` + +原因: +- 请求 1 (/get, ~0.3s): 立即执行 +- 请求 2 (/delay/1, ~1s): 立即执行 +- 请求 3 (/delay/2, ~2s): 立即执行 +- 但只有 3 个 CPR 线程可用(假设测试机是 4 核,可能其他线程被占用) +- 如果 CPU 核心数更少,并发度会更低 + +## 优化建议 + +### 方案 1:增加 CPR 线程池大小(如果 CPR 支持) + +检查 CPR 是否允许自定义线程池大小: + +```cpp +// 理想情况下 +cpr::async::setThreadPoolSize(100); // 如果 CPR 支持 +``` + +**现状**:CPR 似乎**不支持**自定义线程池大小,线程池是全局的且固定为 CPU 核心数。 + +### 方案 2:直接使用 libcurl 的多路复用 + +绕过 CPR,直接使用 libcurl 的 `CURLM`(multi interface): + +```cpp +// 使用 curl_multi_* API +CURLM* multi_handle = curl_multi_init(); +curl_multi_add_handle(multi_handle, handle1); +curl_multi_add_handle(multi_handle, handle2); +// ... + +// 在 marl fiber 中等待 +marl::schedule([sc, callback]() { + int running_handles; + while (curl_multi_perform(multi_handle, &running_handles) == CURLM_CALL_MULTI_PERFORM); + + // 使用 curl_multi_poll 等待(更高效) + curl_multi_poll(multi_handle, NULL, 0, 1000, NULL); + + // 完成后回调 + queue_callback(sc, callback, response); +}); +``` + +**优点**: +- 真正的单线程多路复用(epoll/kqueue/IOCP) +- 不受线程池大小限制 +- 可以同时处理数千个连接 + +**缺点**: +- 需要额外实现,不再使用 CPR 的便利接口 + +### 方案 3:使用 CPR 的同步接口 + Marl 线程池 + +既然 CPR 的异步也是用线程池,不如直接用同步 + marl: + +```cpp +// 创建更多 marl 线程,每个运行同步 CPR +marl::Scheduler::Config config; +config.setWorkerThreadCount(100); // 大量线程 + +g_coroutine-scheduler-start 100 + +// 每个请求在一个 fiber 中运行同步 get +(g_coroutine-run (lambda () + (let ((r (g_http-get url params))) + (callback r)))) +``` + +**问题**: +- 大量线程开销大 +- 不是真正的协程/异步 +- 浪费内存 + +### 方案 4:混合模式(推荐) + +少量 CPR 异步请求 + 大量快速连接: + +```cpp +// 对于少量重要请求,使用 CPR 异步(保证完成) +(g_http-async-get slow-url params callback) + +// 对于大量快速请求,考虑使用专门的连接池 +``` + +## 当前实现的影响 + +### 对用户的影响 + +```scheme +; 用户以为这 10 个请求是并发的 +(dotimes (i 10) + (g_http-async-get (format "https://api.example.com/item/~a" i) + '() + callback)) + +; 实际行为: +; - 如果 CPU 是 4 核,只有 4 个请求真正并发 +; - 其他 6 个在 CPR 队列中等待 +; - 总时间 = (最慢的 4 个) + (剩下的 6 个分批) +``` + +### 文档应该说明 + +```markdown +## 并发限制 + +`g_http-async-get` 的并发度受限于: +1. **CPR 线程池大小**:默认为 CPU 核心数 +2. **网络带宽** +3. **远程服务器限制** + +如果需要更高的并发度(如 100+ 并发连接),建议使用专门的 HTTP 客户端库。 +``` + +## 结论 + +CPR 的设计选择(线程池 = CPU 核心数)限制了我们的异步 HTTP 并发能力。 + +### 短期(当前实现) +- ✅ 适合少量并发请求(< CPU 核心数) +- ✅ 代码简单,易于维护 +- ⚠️ 不适合高并发场景(100+ 请求) + +### 长期改进 +- 考虑直接使用 libcurl multi interface +- 或寻找支持高并发的 C++ HTTP 客户端库 diff --git a/docs/http_concurrency_options.md b/docs/http_concurrency_options.md new file mode 100644 index 00000000..7a4df74b --- /dev/null +++ b/docs/http_concurrency_options.md @@ -0,0 +1,137 @@ +# HTTP 并发方案选择指南 + +## 问题背景 + +CPR 库使用固定大小的线程池(默认 = CPU 核心数)来实现异步 HTTP。这意味着: +- 4 核 CPU:最多 4 个并发请求 +- 8 核 CPU:最多 8 个并发请求 + +如果需要更高的并发度(如 100+),需要使用替代方案。 + +## 方案对比 + +### 方案 1:当前实现(CPR + Marl) + +```scheme +(g_http-async-get url params callback) +``` + +**适用场景**: +- ✅ 少量并发请求(< CPU 核心数) +- ✅ 简单的 API 调用 +- ✅ 不需要极致性能 + +**限制**: +- ❌ 并发度受限于 CPU 核心数 +- ❌ 不适合高并发爬虫/压测 + +--- + +### 方案 2:直接使用 libcurl multi(推荐用于高并发) + +如果需要 100+ 并发连接,可以实现基于 libcurl multi 的版本: + +```cpp +// 伪代码 - 需要实际实现 +static s7_pointer +f_http_multi_get (s7_scheme* sc, s7_pointer args) { + // 使用 curl_multi_* API + // 真正的单线程多路复用(epoll/kqueue/IOCP) + // 可以处理数千个并发连接 +} +``` + +**优点**: +- ✅ 真正的单线程多路复用 +- ✅ 不受线程池限制 +- ✅ 1000+ 并发连接 + +**缺点**: +- ❌ 需要额外实现 +- ❌ 不能使用 CPR 的便利接口 + +--- + +### 方案 3:连接池模式 + +为特定场景(如数据库连接、微服务调用)维护持久连接: + +```scheme +; 创建一个连接池(最多 100 个连接) +(define pool (g_http-pool-create "https://api.example.com" 100)) + +; 使用连接池发送请求 +(g_http-pool-get pool "/endpoint" params callback) +``` + +--- + +## 当前建议 + +### 对于普通用户 + +继续使用 `g_http-async-get`: + +```scheme +; 适合大多数场景 +(g_coroutine-scheduler-start 4) + +(g_http-async-get "https://api.example.com/user/1" '() callback1) +(g_http-async-get "https://api.example.com/user/2" '() callback2) +(g_http-async-get "https://api.example.com/user/3" '() callback3) +``` + +### 对于高并发需求 + +**当前 workaround**:手动分批 + +```scheme +; 如果 CPU 是 4 核,每批发 4 个请求 +(define (batch-requests urls batch-size callback) + (let loop ((remaining urls) + (current-batch '())) + (cond + ; 当前批次已满,等待完成 + ((= (length current-batch) batch-size) + (wait-for-batch current-batch) + (loop remaining '())) + + ; 还有 URL,加入当前批次 + ((not (null? remaining)) + (let ((url (car remaining))) + (g_http-async-get url '() callback) + (loop (cdr remaining) (cons url current-batch)))) + + ; 处理最后一批 + ((not (null? current-batch)) + (wait-for-batch current-batch)) + + ; 全部完成 + (else 'done)))) +``` + +**长期方案**:如果需要真正的高并发,考虑: +1. 使用专门的 HTTP 客户端库(如基于 libcurl 的自定义实现) +2. 使用外部工具(如 `wrk`, `ab`)进行压测 +3. 考虑其他语言/运行时(如 Go, Node.js)专门处理高并发 I/O + +--- + +## 文档更新建议 + +在 `g_http-async-get` 的文档中添加: + +```markdown +### 并发限制 + +`g_http-async-get` 使用底层 CPR 库实现异步 HTTP。CPR 使用固定大小的线程池 +(默认等于 CPU 核心数),因此并发请求数受限于 CPU 核心数。 + +示例: +- 4 核 CPU:最多 4 个并发请求 +- 8 核 CPU:最多 8 个并发请求 + +如果需要更高的并发度(如 100+),请考虑: +1. 使用外部 HTTP 客户端工具 +2. 实现基于 libcurl multi 的自定义方案 +``` diff --git a/docs/platform_compatibility.md b/docs/platform_compatibility.md new file mode 100644 index 00000000..06d749d1 --- /dev/null +++ b/docs/platform_compatibility.md @@ -0,0 +1,203 @@ +# Goldfish 协程功能跨平台兼容性分析 + +## 支持的平台 + +| 平台 | 状态 | 备注 | +|------|------|------| +| Linux (GCC/Clang) | ✅ 完全支持 | 主要开发和测试平台 | +| macOS (Clang) | ✅ 完全支持 | 与 Linux 兼容 | +| Windows (MSVC) | ⚠️ 需要验证 | 理论上支持,需要测试 | +| Windows (MinGW) | ❓ 未知 | marl 对 MinGW x86 支持有限制 | +| WASM | ❌ 不支持 | 协程需要线程支持 | + +## 技术依赖分析 + +### 1. C++ 标准库依赖 + +代码使用的 C++17 特性: + +```cpp +// std::make_unique - C++14,但在 C++17 中广泛使用 +std::make_unique(config); + +// 结构化绑定 (C++17) - 需要确认 +for (auto& [sc, cb] : callbacks) { // Line 1449 +``` + +**MSVC 支持**: +- Visual Studio 2017 15.3+ 完全支持 C++17 +- `std::make_unique`: VS 2015 Update 1+ +- 结构化绑定: VS 2017 15.3+ + +### 2. 线程和同步原语 + +```cpp +#include +#include + +static std::mutex g_scheduler_mutex; +static std::unique_ptr g_scheduler; +``` + +**Windows 支持**: +- MSVC 完全支持 C++11/14/17 线程库 +- Windows 下 `` 和 `` 使用 Windows API 实现 +- 需要 Windows Vista 或更高版本 + +### 3. Marl 库支持 + +根据 xmake-repo 的 marl 包定义: + +```lua +-- Windows 特定处理 +if package:is_plat("windows") and package:config("shared") then + package:add("defines", "MARL_DLL=1") +end + +-- 修复 Windows 下的编译问题 +io.replace("src/scheduler.cpp", "#if defined(_WIN32)", "#if defined(_MSC_VER)") +``` + +**潜在问题**: +1. **静态 vs 动态链接**: 如果使用 marl 动态库,需要定义 `MARL_DLL=1` +2. **MinGW 限制**: `!mingw or mingw|!i386` - 排除了 MinGW x86 平台 + +### 4. 全局静态变量 + +代码使用了多个静态全局变量: + +```cpp +static std::unique_ptr g_scheduler; +static std::mutex g_scheduler_mutex; +static std::vector> g_pending_tasks; +``` + +**Windows DLL 注意事项**: +- 在 Windows DLL 中,静态全局变量的初始化顺序可能有问题 +- `std::mutex` 在 Windows 上是安全的 +- `std::unique_ptr` 在 Windows 上正常工作 + +## 需要验证的项目 + +### 1. Windows 构建配置 + +当前 xmake.lua 中缺少 Windows 特定的 marl 配置: + +```lua +-- 可能需要添加(如果使用动态链接) +if is_plat("windows") then + -- 如果使用 marl DLL,需要定义 MARL_DLL + -- add_defines("MARL_DLL=1") +end +``` + +### 2. 头文件包含顺序 + +当前代码: +```cpp +#ifdef GOLDFISH_WITH_COROUTINE +#include +#include +#include +#include +#include +#endif +``` + +**潜在问题**: Windows 头文件通常需要在其他头文件之前或之后包含,以避免宏冲突。 + +### 3. 函数调用约定 + +marl 使用 C++ 标准调用约定,应该与 MSVC 兼容。 + +## 建议的测试计划 + +### 1. Windows (MSVC) 测试 + +```powershell +# 使用 xmake 在 Windows 上构建 +xmake f --coroutine=y -y +xmake + +# 运行测试 +.\bin\goldfish.exe tests\test_coroutine.scm +.\bin\goldfish.exe tests\test_async_http.scm +``` + +### 2. 需要验证的特定场景 + +- [ ] 基本协程启动/停止 +- [ ] 并发 HTTP 请求 +- [ ] 多线程调度器(4+ 线程) +- [ ] 回调机制 +- [ ] 长时间运行的协程 +- [ ] 异常处理 + +## 已知限制 + +### 1. WASM 平台 + +WebAssembly 当前不支持线程(需要 SharedArrayBuffer 和 Atomics),因此协程功能在 WASM 上不可用。 + +**xmake.lua 中已正确处理**: +```lua +set_allowedplats("linux", "macosx", "windows", "wasm") +``` + +但协程代码在 WASM 上会编译不过(因为 marl 不支持)。 + +### 2. MinGW x86 + +根据 marl 的 xmake 包定义: +```lua +on_install("!mingw or mingw|!i386", function (package) +``` + +这意味着 MinGW x86 平台被排除。 + +## 建议 + +### 立即行动 + +1. **在 Windows CI 中添加协程测试** + - 使用 GitHub Actions 的 windows-latest runner + - 测试 MSVC 2019/2022 + +2. **添加平台检测** + ```cpp + #ifdef GOLDFISH_WITH_COROUTINE + # ifdef _WIN32 + # ifdef MARL_DLL + # define MARL_IMPORT __declspec(dllimport) + # else + # define MARL_IMPORT + # endif + # endif + #endif + ``` + +3. **WASM 平台禁用** + ```lua + if is_plat("wasm") then + set_config("coroutine", false) -- 或自动禁用 + end + ``` + +### 长期改进 + +1. **使用线程局部存储** + - 替代全局静态变量,避免 DLL 问题 + +2. **添加平台抽象层** + - 封装线程和同步原语 + +3. **单元测试覆盖** + - 为每个平台添加特定的测试用例 + +## 结论 + +- **Linux/macOS**: 完全支持 ✅ +- **Windows (MSVC)**: 理论支持,需要实际测试 ⚠️ +- **WASM**: 不支持(缺少线程)❌ + +建议在实际生产使用之前,先在 Windows 上进行完整的测试。 diff --git a/docs/windows_testing_guide.md b/docs/windows_testing_guide.md new file mode 100644 index 00000000..a423393b --- /dev/null +++ b/docs/windows_testing_guide.md @@ -0,0 +1,196 @@ +# Windows (MSVC) 兼容性测试指南 + +## 测试环境要求 + +### 必需的软件 +- Windows 10 或更高版本 +- Visual Studio 2017 15.3+ 或 Visual Studio 2019/2022 +- xmake (>= 2.6.0) +- Git + +### 安装步骤 + +1. **安装 Visual Studio** + - 安装 "使用 C++ 的桌面开发" 工作负载 + - 确保包含 Windows SDK + +2. **安装 xmake** + ```powershell + # 使用 PowerShell + Invoke-Expression (Invoke-Webrequest 'https://xmake.io/psget.text' -UseBasicParsing).Content + ``` + +3. **克隆仓库** + ```powershell + git clone https://github.com/LiiiLabs/goldfish.git + cd goldfish + ``` + +## 构建步骤 + +### 1. 配置 + +```powershell +# 启用协程支持(默认) +xmake f --coroutine=y -y + +# 或者显式禁用 +xmake f --coroutine=n -y +``` + +### 2. 构建 + +```powershell +xmake -v +``` + +### 3. 验证 + +```powershell +.\bin\goldfish.exe --version +``` + +## 测试用例 + +### 基础协程测试 + +```powershell +.\bin\goldfish.exe tests\test_coroutine.scm +``` + +期望输出: +``` +Testing coroutine support... +Test 1: Start scheduler with 4 threads... OK +Test 2: Run coroutines... Task 1 done +Task 2 done +Task 3 done +All tasks completed, counter=3 +Test 3: Yield test... OK +Test 4: Sleep test (0.1 seconds)... OK +Test 5: Stop scheduler... OK +All coroutine tests passed! +``` + +### 异步 HTTP 测试 + +```powershell +.\bin\goldfish.exe tests\test_async_http.scm +``` + +**注意**: 需要网络连接。 + +## 常见问题 + +### 1. 链接错误 LNK2019(未解析的外部符号) + +**问题**: marl 库未正确链接 + +**解决**: 检查 xmake 是否正确下载了 marl: +```powershell +xmake require --info marl +``` + +### 2. 运行时崩溃(访问冲突) + +**问题**: 可能是静态初始化顺序问题 + +**检查点**: +- 确保 `g_coroutine-scheduler-start` 在其他协程函数之前调用 +- 检查是否有多个线程同时访问 s7_scheme + +### 3. 编译错误 C++17 + +**问题**: 编译器版本过旧 + +**解决**: 升级 Visual Studio 到 2017 15.3+,或修改 xmake.lua: +```lua +set_languages("c++14") -- 降低标准(可能需要修改代码) +``` + +### 4. 动态库链接问题 + +**问题**: 如果 marl 是 DLL,需要定义 MARL_DLL + +**解决**: 在 xmake.lua 中添加: +```lua +if is_plat("windows") and has_config("coroutine") then + add_defines("MARL_DLL=1") +end +``` + +## 调试技巧 + +### 1. 启用详细日志 + +```powershell +$env:XMAKE_VERBOSE="y" +xmake -vD +``` + +### 2. 检查生成的项目 + +```powershell +# 生成 Visual Studio 项目文件 +xmake project -k vsxmake +``` + +### 3. 使用 WinDbg 调试崩溃 + +```powershell +# 安装 WinDbg(Windows SDK 包含) +# 附加到 goldfish.exe 进程 +windbg -g .\bin\goldfish.exe tests\test_coroutine.scm +``` + +## CI/CD 集成 + +### GitHub Actions 示例 + +```yaml +name: Windows Build + +on: [push, pull_request] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup xmake + uses: xmake-io/github-action-setup-xmake@v1 + with: + xmake-version: latest + + - name: Configure + run: xmake f --coroutine=y -y + + - name: Build + run: xmake -v + + - name: Test + run: | + .\bin\goldfish.exe tests\test_coroutine.scm +``` + +## 性能测试 + +### 对比同步和异步 HTTP + +```powershell +Measure-Command { .\bin\goldfish.exe tests\test_async_http.scm } +``` + +预期:异步应该比顺序请求快 2-3 倍。 + +## 报告问题 + +如果在 Windows 上遇到问题,请提供: + +1. Visual Studio 版本 (`cl.exe` 版本) +2. xmake 版本 (`xmake --version`) +3. 完整的错误日志 +4. 系统信息 (`systeminfo`) + +提交到: https://github.com/LiiiLabs/goldfish/issues diff --git a/src/goldfish.hpp b/src/goldfish.hpp index 5d46c7cc..3128a6c5 100644 --- a/src/goldfish.hpp +++ b/src/goldfish.hpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -58,6 +60,14 @@ #include #endif +#ifdef GOLDFISH_WITH_COROUTINE +#include +#include +#include +#include +#include +#endif + #define GOLDFISH_VERSION "17.11.25" #define GOLDFISH_PATH_MAXN TB_PATH_MAXN @@ -359,7 +369,6 @@ glue_http_stream (s7_scheme* sc) { glue_http_stream_post(sc); } - inline s7_pointer string_vector_to_s7_vector (s7_scheme* sc, vector v) { int N = v.size (); @@ -985,8 +994,6 @@ glue_liii_hashlib (s7_scheme* sc) { glue_sha256 (sc); } - - static s7_pointer f_isdir (s7_scheme* sc, s7_pointer args) { const char* dir_c= s7_string (s7_car (args)); @@ -1417,6 +1424,254 @@ glue_liii_list (s7_scheme* sc) { glue_iota_list (sc); } +// -------------------------------- coroutine -------------------------------- +#ifdef GOLDFISH_WITH_COROUTINE + +static std::unique_ptr g_scheduler; +static std::mutex g_scheduler_mutex; + +// Thread-safe queue for coroutine tasks +static std::mutex g_task_mutex; +static std::vector> g_pending_tasks; + +// Store async callbacks for later execution +static std::mutex g_http_callbacks_mutex; +static std::vector>> g_http_callbacks; + +// Process pending HTTP callbacks (call this periodically or in g_coroutine-wait) +static void +process_http_callbacks () { + std::vector>> callbacks; + { + std::lock_guard lock(g_http_callbacks_mutex); + callbacks.swap(g_http_callbacks); + } + for (auto& callback_pair : callbacks) { + callback_pair.second(); + } +} + +static s7_pointer +f_coroutine_scheduler_start (s7_scheme* sc, s7_pointer args) { + std::lock_guard lock(g_scheduler_mutex); + if (g_scheduler != nullptr) { + return s7_make_boolean(sc, false); // already started + } + + s7_int threads = 0; + if (s7_is_integer(s7_car(args))) { + threads = s7_integer(s7_car(args)); + } + + marl::Scheduler::Config config; + if (threads > 0) { + config.setWorkerThreadCount(static_cast(threads)); + g_scheduler = std::make_unique(config); + } else { + g_scheduler = std::make_unique(marl::Scheduler::Config::allCores()); + } + g_scheduler->bind(); + + return s7_make_boolean(sc, true); +} + +inline void +glue_coroutine_scheduler_start (s7_scheme* sc) { + const char* name = "g_coroutine-scheduler-start"; + const char* desc = "(g_coroutine-scheduler-start [threads]) => boolean, start the coroutine scheduler. threads=0 means use all cores"; + glue_define(sc, name, desc, f_coroutine_scheduler_start, 0, 1); +} + +static s7_pointer +f_coroutine_scheduler_stop (s7_scheme* sc, s7_pointer args) { + std::lock_guard lock(g_scheduler_mutex); + if (g_scheduler == nullptr) { + return s7_make_boolean(sc, false); // not started + } + + // Execute any remaining pending tasks before stopping + { + std::lock_guard task_lock(g_task_mutex); + for (auto& task : g_pending_tasks) { + task(); + } + g_pending_tasks.clear(); + } + + g_scheduler->unbind(); + g_scheduler.reset(); + + return s7_make_boolean(sc, true); +} + +inline void +glue_coroutine_scheduler_stop (s7_scheme* sc) { + const char* name = "g_coroutine-scheduler-stop"; + const char* desc = "(g_coroutine-scheduler-stop) => boolean, stop the coroutine scheduler"; + glue_define(sc, name, desc, f_coroutine_scheduler_stop, 0, 0); +} + +static s7_pointer +f_coroutine_run (s7_scheme* sc, s7_pointer args) { + if (g_scheduler == nullptr) { + return s7_error(sc, s7_make_symbol(sc, "coroutine-error"), + s7_list(sc, 1, s7_make_string(sc, "scheduler not started"))); + } + + s7_pointer thunk = s7_car(args); + if (!s7_is_procedure(thunk)) { + return s7_error(sc, s7_make_symbol(sc, "type-error"), + s7_list(sc, 2, s7_make_string(sc, "coroutine-run: expected procedure"), thunk)); + } + + // Note: s7 is not thread-safe, so we queue the task for later execution + // The task will be executed when g_coroutine-wait is called + std::lock_guard lock(g_task_mutex); + g_pending_tasks.push_back([sc, thunk]() { + s7_call(sc, thunk, s7_nil(sc)); + }); + + return s7_make_boolean(sc, true); +} + +inline void +glue_coroutine_run (s7_scheme* sc) { + const char* name = "g_coroutine-run"; + const char* desc = "(g_coroutine-run thunk) => boolean, run thunk in a coroutine"; + glue_define(sc, name, desc, f_coroutine_run, 1, 0); +} + +static s7_pointer +f_coroutine_wait (s7_scheme* sc, s7_pointer args) { + if (g_scheduler == nullptr) { + return s7_make_boolean(sc, false); + } + + // Execute all pending tasks (s7 is not thread-safe, execute sequentially) + std::vector> tasks; + { + std::lock_guard lock(g_task_mutex); + tasks.swap(g_pending_tasks); + } + + for (auto& task : tasks) { + task(); + } + + // Process any HTTP callbacks + process_http_callbacks(); + + return s7_make_boolean(sc, true); +} + +inline void +glue_coroutine_wait (s7_scheme* sc) { + const char* name = "g_coroutine-wait"; + const char* desc = "(g_coroutine-wait) => boolean, wait for all coroutines to complete"; + glue_define(sc, name, desc, f_coroutine_wait, 0, 0); +} + +static s7_pointer +f_coroutine_yield (s7_scheme* sc, s7_pointer args) { + // Yield is automatic in marl when a fiber blocks + // We can use this as a hint to the scheduler + auto scheduler = marl::Scheduler::get(); + if (scheduler != nullptr) { + // Allow other tasks to run by yielding this thread + std::this_thread::yield(); + } + return s7_nil(sc); +} + +inline void +glue_coroutine_yield (s7_scheme* sc) { + const char* name = "g_coroutine-yield"; + const char* desc = "(g_coroutine-yield) => nil, hint the scheduler to yield to other tasks"; + glue_define(sc, name, desc, f_coroutine_yield, 0, 0); +} + +static s7_pointer +f_coroutine_sleep (s7_scheme* sc, s7_pointer args) { + s7_double seconds = s7_real(s7_car(args)); + auto milliseconds = static_cast(seconds * 1000); + + // Use marl::Event for non-blocking sleep within coroutines + marl::Event event(marl::Event::Mode::Manual); + marl::schedule([event, milliseconds]() mutable { + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); + event.signal(); + }); + event.wait(); + + return s7_nil(sc); +} + +inline void +glue_coroutine_sleep (s7_scheme* sc) { + const char* name = "g_coroutine-sleep"; + const char* desc = "(g_coroutine-sleep seconds) => nil, non-blocking sleep for specified seconds"; + glue_define(sc, name, desc, f_coroutine_sleep, 1, 0); +} + +// Async HTTP support +static s7_pointer +f_http_async_get (s7_scheme* sc, s7_pointer args) { + const char* url = s7_string(s7_car(args)); + s7_pointer params = s7_cadr(args); + s7_pointer callback = s7_caddr(args); + + if (!s7_is_procedure(callback)) { + return s7_error(sc, s7_make_symbol(sc, "type-error"), + s7_list(sc, 2, s7_make_string(sc, "http-async-get: expected callback procedure"), callback)); + } + + // Protect callback from GC + int gc_loc = s7_gc_protect(sc, callback); + + // Use cpr::GetAsync for non-blocking HTTP request + auto future = std::make_shared(cpr::GetAsync(cpr::Url(url))); + + // Schedule a task to wait for completion and call the callback + marl::schedule([sc, callback, gc_loc, future]() mutable { + cpr::Response r = future->get(); // This blocks only this fiber, not the main thread + + // Queue the callback to be executed in the main thread + std::lock_guard lock(g_http_callbacks_mutex); + g_http_callbacks.push_back({sc, [sc, callback, gc_loc, r]() mutable { + s7_pointer ht = response2hashtable(sc, r); + s7_call(sc, callback, s7_cons(sc, ht, s7_nil(sc))); + s7_gc_unprotect_at(sc, gc_loc); + }}); + }); + + return s7_make_boolean(sc, true); +} + +inline void +glue_http_async_get (s7_scheme* sc) { + const char* name = "g_http-async-get"; + const char* desc = "(g_http-async-get url params callback) => boolean, async http get. callback receives response hashtable"; + glue_define(sc, name, desc, f_http_async_get, 3, 0); +} + +inline void +glue_http_async (s7_scheme* sc) { + glue_http_async_get(sc); +} + +inline void +glue_liii_coroutine (s7_scheme* sc) { + glue_coroutine_scheduler_start (sc); + glue_coroutine_scheduler_stop (sc); + glue_coroutine_run (sc); + glue_coroutine_wait (sc); + glue_coroutine_yield (sc); + glue_coroutine_sleep (sc); + glue_http_async (sc); +} + +#endif // GOLDFISH_WITH_COROUTINE + void glue_for_community_edition (s7_scheme* sc) { glue_goldfish (sc); @@ -1430,8 +1685,14 @@ glue_for_community_edition (s7_scheme* sc) { glue_liii_datetime (sc); glue_liii_uuid (sc); glue_liii_hashlib (sc); +#ifdef GOLDFISH_WITH_COROUTINE + glue_liii_coroutine (sc); +#endif glue_http (sc); glue_http_stream (sc); +#ifdef GOLDFISH_WITH_COROUTINE + glue_http_async (sc); +#endif } static void diff --git a/tests/test_async_http.scm b/tests/test_async_http.scm new file mode 100644 index 00000000..70b4ed2a --- /dev/null +++ b/tests/test_async_http.scm @@ -0,0 +1,62 @@ +; Test for async HTTP support + +(display "Testing async HTTP...") +(newline) + +; Start scheduler +(g_coroutine-scheduler-start 4) +(display "Scheduler started") +(newline) + +; Test async HTTP get +(display "Sending async HTTP request to httpbin.org...") +(newline) + +(define responses-received 0) + +(g_http-async-get + "https://httpbin.org/get" + '() + (lambda (response) + (display "Got response!") + (newline) + (display "Status code: ") + (display (hash-table-ref response "status-code")) + (newline) + (set! responses-received (+ responses-received 1)))) + +(g_http-async-get + "https://httpbin.org/delay/2" + '() + (lambda (response) + (display "Got delayed response!") + (newline) + (display "Status code: ") + (display (hash-table-ref response "status-code")) + (newline) + (set! responses-received (+ responses-received 1)))) + +; Wait for responses +(display "Waiting for responses...") +(newline) + +(let loop ((i 0)) + (if (< i 10) + (begin + (g_coroutine-wait) + (display "Waiting... ") + (display i) + (display " responses so far: ") + (display responses-received) + (newline) + (g_coroutine-sleep 0.5) + (loop (+ i 1))) + (display "Timeout waiting for responses"))) + +(display "Total responses received: ") +(display responses-received) +(newline) + +(g_coroutine-scheduler-stop) +(display "Scheduler stopped") +(newline) diff --git a/tests/test_async_vs_sync.scm b/tests/test_async_vs_sync.scm new file mode 100644 index 00000000..7dadc2a4 --- /dev/null +++ b/tests/test_async_vs_sync.scm @@ -0,0 +1,105 @@ +; Demonstrate async vs sync HTTP requests +; This test shows that async requests run concurrently + +(display "=== Comparing Sync vs Async HTTP ===") +(newline) +(newline) + +; Helper to get current time +(define (current-time-seconds) + (/ (g_monotonic-nanosecond) 1000000000.0)) + +; Helper to measure time +(define (time-elapsed start) + (- (current-time-seconds) start)) + +; ============================================ +; SYNC VERSION (using regular http-get) +; ============================================ +(display "SYNC: Sequential requests") +(newline) + +(define sync-start (current-time-seconds)) + +; Simulate two sync requests (we'll just sleep to show the point) +(display " Sync: Starting request 1 (1 second)...") +(newline) +(g_sleep 1) ; This blocks everything +(display " Sync: Request 1 done") +(newline) + +(display " Sync: Starting request 2 (1 second)...") +(newline) +(g_sleep 1) ; This also blocks +(display " Sync: Request 2 done") +(newline) + +(define sync-elapsed (time-elapsed sync-start)) +(display "SYNC total time: ") +(display sync-elapsed) +(display " seconds") +(newline) +(newline) + +; ============================================ +; ASYNC VERSION (using async scheduler) +; ============================================ +(display "ASYNC: Concurrent requests") +(newline) + +(g_coroutine-scheduler-start 4) + +(define async-start (current-time-seconds)) +(define async-completed 0) + +; Request 1: 1 second delay +(g_coroutine-run + (lambda () + (g_sleep 1) + (set! async-completed (+ async-completed 1)) + (display " Async: Request 1 done at ") + (display (time-elapsed async-start)) + (display "s") + (newline))) + +; Request 2: 1 second delay (starts immediately, not waiting for request 1) +(g_coroutine-run + (lambda () + (g_sleep 1) + (set! async-completed (+ async-completed 1)) + (display " Async: Request 2 done at ") + (display (time-elapsed async-start)) + (display "s") + (newline))) + +; Wait for both to complete +(let loop ((i 0)) + (g_coroutine-wait) + (if (and (< i 20) (< async-completed 2)) + (begin + (g_coroutine-sleep 0.1) + (loop (+ i 1))))) + +(define async-elapsed (time-elapsed async-start)) +(display "ASYNC total time: ") +(display async-elapsed) +(display " seconds") +(newline) + +(g_coroutine-scheduler-stop) + +(newline) +(display "=== Results ===") +(newline) +(display "Sync time: ") +(display sync-elapsed) +(display "s (expected ~2.0s)") +(newline) +(display "Async time: ") +(display async-elapsed) +(display "s (expected ~1.0s)") +(newline) +(display "Speedup: ") +(display (/ sync-elapsed async-elapsed)) +(display "x") +(newline) diff --git a/tests/test_concurrent_work.scm b/tests/test_concurrent_work.scm new file mode 100644 index 00000000..9f9b9b61 --- /dev/null +++ b/tests/test_concurrent_work.scm @@ -0,0 +1,71 @@ +; Test to verify concurrent execution using g_coroutine-sleep +; If truly concurrent, 3 x 0.5s sleeps should take ~0.5s total +; If sequential, it would take ~1.5s total + +(display "=== Testing Concurrent Sleep ===") +(newline) + +(define (current-time-seconds) + (/ (g_monotonic-nanosecond) 1000000000.0)) + +(g_coroutine-scheduler-start 4) + +(define start (current-time-seconds)) +(define completed 0) + +; Launch 3 concurrent tasks that each sleep 0.5 seconds +(display "Launching 3 tasks that each sleep 0.5s...") +(newline) + +(g_coroutine-run + (lambda () + (g_coroutine-sleep 0.5) + (set! completed (+ completed 1)) + (display "Task 1 done at T+") + (display (- (current-time-seconds) start)) + (display "s") + (newline))) + +(g_coroutine-run + (lambda () + (g_coroutine-sleep 0.5) + (set! completed (+ completed 1)) + (display "Task 2 done at T+") + (display (- (current-time-seconds) start)) + (display "s") + (newline))) + +(g_coroutine-run + (lambda () + (g_coroutine-sleep 0.5) + (set! completed (+ completed 1)) + (display "Task 3 done at T+") + (display (- (current-time-seconds) start)) + (display "s") + (newline))) + +; Wait for all to complete +(let loop () + (g_coroutine-wait) + (if (< completed 3) + (begin + (g_coroutine-sleep 0.05) + (loop)))) + +(define elapsed (- (current-time-seconds) start)) +(newline) +(display "Total time: ") +(display elapsed) +(display "s") +(newline) +(display "Expected (concurrent): ~0.5s") +(newline) +(display "Expected (sequential): ~1.5s") +(newline) + +(if (< elapsed 1.0) + (display "✓ Concurrent execution confirmed!") + (display "✗ Sequential execution (expected with current implementation)")) +(newline) + +(g_coroutine-scheduler-stop) diff --git a/tests/test_coroutine.scm b/tests/test_coroutine.scm new file mode 100644 index 00000000..4d6d3806 --- /dev/null +++ b/tests/test_coroutine.scm @@ -0,0 +1,51 @@ +; Test for coroutine support + +(display "Testing coroutine support...") +(newline) + +; Test 1: Start scheduler +(display "Test 1: Start scheduler with 4 threads... ") +(let ((result (g_coroutine-scheduler-start 4))) + (display (if result "OK" "Already started")) + (newline)) + +; Test 2: Run a simple coroutine +(display "Test 2: Run coroutines... ") +(let ((counter 0)) + (g_coroutine-run (lambda () + (set! counter (+ counter 1)) + (display "Task 1 done ") + (newline))) + (g_coroutine-run (lambda () + (set! counter (+ counter 1)) + (display "Task 2 done ") + (newline))) + (g_coroutine-run (lambda () + (set! counter (+ counter 1)) + (display "Task 3 done ") + (newline))) + (g_coroutine-wait) + (display "All tasks completed, counter=") + (display counter) + (newline)) + +; Test 3: Yield +(display "Test 3: Yield test... ") +(g_coroutine-yield) +(display "OK") +(newline) + +; Test 4: Sleep +(display "Test 4: Sleep test (0.1 seconds)... ") +(g_coroutine-sleep 0.1) +(display "OK") +(newline) + +; Test 5: Stop scheduler +(display "Test 5: Stop scheduler... ") +(let ((result (g_coroutine-scheduler-stop))) + (display (if result "OK" "Already stopped")) + (newline)) + +(display "All coroutine tests passed!") +(newline) diff --git a/tests/test_cpr_concurrency_limit.scm b/tests/test_cpr_concurrency_limit.scm new file mode 100644 index 00000000..6d3709ba --- /dev/null +++ b/tests/test_cpr_concurrency_limit.scm @@ -0,0 +1,78 @@ +; Test to verify CPR thread pool limit +; This test shows that CPR limits concurrent requests to CPU core count + +(display "=== Testing CPR Thread Pool Limit ===") +(newline) + +(define (current-time-seconds) + (/ (g_monotonic-nanosecond) 1000000000.0)) + +(define start-time (current-time-seconds)) +(define responses-received 0) +(define total-requests 8) ; Send more requests than typical CPU cores + +(g_coroutine-scheduler-start 4) + +(display "Sending ") +(display total-requests) +(display " concurrent requests to httpbin.org/delay/1...") +(newline) +(display "(If CPR uses CPU core count as thread pool size, only ~4 will be truly concurrent)") +(newline) +(newline) + +; Send 8 requests, each taking 1 second +; If truly concurrent: total time ~1 second +; If CPR limited to 4 threads: total time ~2 seconds (4+4) + +(let loop ((i 0)) + (if (< i total-requests) + (begin + (g_http-async-get + "https://httpbin.org/delay/1" + '() + (lambda (response) + (set! responses-received (+ responses-received 1)) + (display "Request ") + (display (+ i 1)) + (display " completed at T+") + (display (- (current-time-seconds) start-time)) + (display "s") + (newline))) + (loop (+ i 1))))) + +; Wait for all responses +(let loop ((i 0)) + (g_coroutine-wait) + (if (and (< i 30) (< responses-received total-requests)) + (begin + (g_coroutine-sleep 0.2) + (loop (+ i 1))))) + +(define total-time (- (current-time-seconds) start-time)) + +(newline) +(display "=== Results ===") +(newline) +(display "Total time: ") +(display total-time) +(display " seconds") +(newline) +(display "Requests completed: ") +(display responses-received) +(display "/") +(display total-requests) +(newline) +(newline) +(display "Analysis:") +(newline) +(display "- If ~1s: CPR allows all 8 concurrent (unlikely)") +(newline) +(display "- If ~2s: CPR limited to ~4 concurrent (4+4 batches)") +(newline) +(display "- If ~4s: CPR limited to ~2 concurrent") +(newline) +(display "- If ~8s: Sequential execution") +(newline) + +(g_coroutine-scheduler-stop) diff --git a/tests/test_real_async_http.scm b/tests/test_real_async_http.scm new file mode 100644 index 00000000..37cd2dc5 --- /dev/null +++ b/tests/test_real_async_http.scm @@ -0,0 +1,101 @@ +; Demonstrate REAL async HTTP (I/O runs in background, not Scheme code) +; This shows that HTTP requests are truly concurrent + +(display "=== Real Async HTTP Test ===") +(newline) +(newline) + +; Helper to get current time +(define (current-time-seconds) + (/ (g_monotonic-nanosecond) 1000000000.0)) + +(define (time-elapsed start) + (- (current-time-seconds) start)) + +(g_coroutine-scheduler-start 4) + +(define start-time (current-time-seconds)) +(define responses-received 0) + +; Request 1: Fast endpoint (~0.3s) +(display "T+0s: Sending fast request (httpbin.org/get)...") +(newline) +(g_http-async-get + "https://httpbin.org/get" + '() + (lambda (response) + (display "T+") + (display (time-elapsed start-time)) + (display "s: FAST response received! Status: ") + (display (hash-table-ref response "status-code")) + (newline) + (set! responses-received (+ responses-received 1)))) + +; Request 2: Slow endpoint (2 second delay) +(display "T+0s: Sending slow request (httpbin.org/delay/2)...") +(newline) +(g_http-async-get + "https://httpbin.org/delay/2" + '() + (lambda (response) + (display "T+") + (display (time-elapsed start-time)) + (display "s: SLOW response received! Status: ") + (display (hash-table-ref response "status-code")) + (newline) + (set! responses-received (+ responses-received 1)))) + +; Request 3: Medium endpoint (1 second delay) +(display "T+0s: Sending medium request (httpbin.org/delay/1)...") +(newline) +(g_http-async-get + "https://httpbin.org/delay/1" + '() + (lambda (response) + (display "T+") + (display (time-elapsed start-time)) + (display "s: MEDIUM response received! Status: ") + (display (hash-table-ref response "status-code")) + (newline) + (set! responses-received (+ responses-received 1)))) + +; Main loop waiting for responses +(newline) +(display "Main thread: Waiting for responses...") +(newline) + +(let loop ((i 0)) + (g_coroutine-wait) + (display "T+") + (display (time-elapsed start-time)) + (display "s: Main thread check, responses so far: ") + (display responses-received) + (display "/3") + (newline) + + (if (and (< i 30) (< responses-received 3)) + (begin + (g_coroutine-sleep 0.3) + (loop (+ i 1))) + (begin + (display "Done or timeout!") + (newline)))) + +(define total-time (time-elapsed start-time)) +(newline) +(display "=== Summary ===") +(newline) +(display "Total time: ") +(display total-time) +(display " seconds") +(newline) +(display "Expected if sequential: ~3.3 seconds") +(newline) +(display "Expected if concurrent: ~2.0 seconds (limited by slowest)") +(newline) +(display "Speedup vs sequential: ") +(display (/ 3.3 total-time)) +(display "x") +(newline) + +(g_coroutine-scheduler-stop) diff --git a/xmake.lua b/xmake.lua index f8f24a2a..a6b4ffb8 100644 --- a/xmake.lua +++ b/xmake.lua @@ -48,6 +48,12 @@ if has_config("http") then add_requires("cpr") end +option("coroutine") + set_description("Enable coroutine support (marl)") + set_default(true) + set_values(false, true) +option_end() + local S7_VERSION = "20250922" if has_config("pin-deps") then add_requires("s7 "..S7_VERSION, {system=system}) @@ -83,6 +89,16 @@ else add_requires("isocline", {system=system}) end +-- marl for coroutine support +if has_config("coroutine") then + local MARL_VERSION = "2025.02.23" + if has_config("pin-deps") then + add_requires("marl " .. MARL_VERSION, {system=system}) + else + add_requires("marl", {system=system}) + end +end + -- local header only dependency, no need to (un)pin version add_requires("argh v1.3.2") @@ -108,6 +124,12 @@ target ("goldfish") do add_defines("GOLDFISH_WITH_REPL") end + -- only enable coroutine if coroutine option is enabled + if has_config("coroutine") then + add_packages("marl") + add_defines("GOLDFISH_WITH_COROUTINE") + end + add_installfiles("$(projectdir)/goldfish/(scheme/*.scm)", {prefixdir = "share/goldfish"}) add_installfiles("$(projectdir)/goldfish/(srfi/*.scm)", {prefixdir = "share/goldfish"}) add_installfiles("$(projectdir)/goldfish/(liii/*.scm)", {prefixdir = "share/goldfish"})