From 2d3ffc8da0d2b906f6d31e1e59ca177a9bbc3ece Mon Sep 17 00:00:00 2001 From: Andrey Kutejko Date: Mon, 13 Jul 2026 13:50:24 +0200 Subject: [PATCH 1/2] Introduce coroutines for modeling asynchronous UI flow --- .github/workflows/c-cpp.yml | 2 +- common/coroutines.h | 30 + common/meson.build | 2 +- common/rinstallprogress.cc | 14 +- common/rinstallprogress.h | 14 +- common/rpackagelister.cc | 16 +- common/rpackagelister.h | 3 +- common/ruserdialog.cc | 10 +- common/ruserdialog.h | 26 +- debian/control | 2 +- gtk/gsynaptic.cc | 89 +- gtk/rgcacheprogress.cc | 40 +- gtk/rgcacheprogress.h | 1 + gtk/rgcdscanner.cc | 2 +- gtk/rgchangelogdialog.cc | 4 +- gtk/rgchangelogdialog.h | 4 +- gtk/rgdebinstallprogress.cc | 104 +- gtk/rgdebinstallprogress.h | 12 +- gtk/rgdummyinstallprogress.cc | 17 +- gtk/rgdummyinstallprogress.h | 6 +- gtk/rgfetchprogress.cc | 60 +- gtk/rgfetchprogress.h | 2 +- gtk/rgfiltermanager.cc | 4 +- gtk/rggtkbuilderwindow.cc | 3 +- gtk/rginstallprogress.cc | 45 +- gtk/rginstallprogress.h | 9 +- gtk/rglogview.cc | 2 +- gtk/rgmainwindow.cc | 1914 ++++++++------- gtk/rgmainwindow.h | 38 +- gtk/rgpkgcdrom.cc | 32 +- gtk/rgpkgcdrom.h | 3 +- gtk/rgpkgdetails.cc | 61 +- gtk/rgpkgdetails.h | 3 +- gtk/rgpreferenceswindow.cc | 197 +- gtk/rgpreferenceswindow.h | 14 +- gtk/rgrepositorywin.cc | 177 +- gtk/rgrepositorywin.h | 13 +- gtk/rgsummarywindow.cc | 11 +- gtk/rgsummarywindow.h | 3 +- gtk/rgtaskswin.cc | 85 +- gtk/rgterminstallprogress.cc | 64 +- gtk/rgterminstallprogress.h | 10 +- gtk/rguserdialog.cc | 32 +- gtk/rguserdialog.h | 7 +- gtk/rgutils.cc | 61 +- gtk/rgutils.h | 166 +- gtk/rgwindow.cc | 20 - gtk/rgwindow.h | 3 - meson.build | 15 +- subprojects/.wraplock | 0 subprojects/task.wrap | 9 + subprojects/task/.clang-format | 251 ++ subprojects/task/.clang-format-ignore | 1 + subprojects/task/.clang-tidy | 55 + subprojects/task/.codespellignore | 7 + subprojects/task/.codespellrc | 6 + subprojects/task/.devcontainer/Dockerfile | 19 + .../task/.devcontainer/devcontainer.json | 15 + subprojects/task/.devcontainer/postcreate.sh | 4 + subprojects/task/.github/CODEOWNERS | 14 + .../task/.github/workflows/ci_tests.yml | 113 + .../.github/workflows/pre-commit-check.yml | 13 + .../.github/workflows/pre-commit-update.yml | 15 + .../task/.github/workflows/pre-commit.yml | 13 + subprojects/task/.gitignore | 5 + .../task/.meson-subproject-wrap-hash.txt | 1 + subprojects/task/.pre-commit-config.yaml | 46 + subprojects/task/CMakeLists.txt | 63 + subprojects/task/CMakePresets.json | 413 ++++ subprojects/task/LICENSE | 219 ++ subprojects/task/Makefile | 74 + subprojects/task/README.md | 124 + subprojects/task/docs/Makefile | 18 + subprojects/task/docs/P3552-task.md | 2108 +++++++++++++++++ subprojects/task/docs/P3796-task-issues.md | 1560 ++++++++++++ subprojects/task/docs/P3941-affinity.md | 1401 +++++++++++ subprojects/task/docs/P3980-allocation.md | 388 +++ subprojects/task/docs/c++now.md | 46 + subprojects/task/docs/compiler-explorer.ico | Bin 0 -> 15406 bytes subprojects/task/docs/contributing.md | 36 + subprojects/task/docs/examples.md | 246 ++ subprojects/task/docs/issues-overview.md | 274 +++ subprojects/task/docs/resources.md | 12 + subprojects/task/docs/task-issues.md | 28 + subprojects/task/examples/CMakeLists.txt | 79 + subprojects/task/examples/affinity.cpp | 63 + .../task/examples/aggregate-return.cpp | 20 + subprojects/task/examples/alloc-1.cpp | 124 + subprojects/task/examples/alloc-2.cpp | 82 + subprojects/task/examples/alloc.cpp | 118 + subprojects/task/examples/async-lock.cpp | 116 + subprojects/task/examples/bulk.cpp | 27 + subprojects/task/examples/c++now-affinity.cpp | 56 + .../task/examples/c++now-allocator.cpp | 28 + subprojects/task/examples/c++now-basic.cpp | 34 + subprojects/task/examples/c++now-cancel.cpp | 15 + subprojects/task/examples/c++now-errors.cpp | 84 + subprojects/task/examples/c++now-query.cpp | 66 + .../task/examples/c++now-result-types.cpp | 20 + subprojects/task/examples/c++now-return.cpp | 45 + .../task/examples/c++now-stop_token.cpp | 39 + .../task/examples/c++now-with_error.cpp | 63 + subprojects/task/examples/co_await-result.cpp | 40 + subprojects/task/examples/co_await-task.cpp | 24 + subprojects/task/examples/container.cpp | 26 + subprojects/task/examples/customize.cpp | 31 + .../task/examples/dangling-references.cpp | 34 + subprojects/task/examples/demo-scope.hpp | 91 + .../task/examples/demo-thread_loop.hpp | 30 + .../task/examples/demo-tls_scheduler.hpp | 171 ++ subprojects/task/examples/environment.cpp | 159 ++ subprojects/task/examples/error.cpp | 64 + .../task/examples/escaped-exception.cpp | 30 + subprojects/task/examples/friendly.cpp | 17 + subprojects/task/examples/hello.cpp | 16 + subprojects/task/examples/http-server.cpp | 37 + subprojects/task/examples/into_optional.cpp | 76 + subprojects/task/examples/issue-affine_on.cpp | 21 + .../task/examples/issue-frame-allocator.cpp | 26 + .../task/examples/issue-start-reschedules.cpp | 36 + .../examples/issue-symmetric-transfer.cpp | 29 + subprojects/task/examples/loop.cpp | 97 + subprojects/task/examples/odd-completions.cpp | 28 + subprojects/task/examples/odd-return.cpp | 20 + subprojects/task/examples/query.cpp | 64 + subprojects/task/examples/result_example.cpp | 17 + subprojects/task/examples/rvalue-task.cpp | 38 + subprojects/task/examples/stop.cpp | 53 + subprojects/task/examples/task-sender.cpp | 160 ++ subprojects/task/examples/task_scheduler.cpp | 21 + subprojects/task/examples/tls-scheduler.cpp | 60 + .../task/include/beman/execution/task.hpp | 13 + subprojects/task/include/beman/lazy/lazy.hpp | 24 + .../beman/task/detail/allocator_of.hpp | 38 + .../beman/task/detail/allocator_support.hpp | 87 + .../include/beman/task/detail/awaiter.hpp | 116 + .../detail/change_coroutine_scheduler.hpp | 37 + .../include/beman/task/detail/completion.hpp | 28 + .../beman/task/detail/error_types_of.hpp | 28 + .../beman/task/detail/final_awaiter.hpp | 27 + .../beman/task/detail/find_allocator.hpp | 46 + .../task/include/beman/task/detail/handle.hpp | 48 + .../task/detail/infallible_scheduler.hpp | 33 + .../beman/task/detail/into_optional.hpp | 100 + .../task/include/beman/task/detail/logger.hpp | 40 + .../task/include/beman/task/detail/poly.hpp | 78 + .../beman/task/detail/promise_base.hpp | 61 + .../include/beman/task/detail/promise_env.hpp | 40 + .../beman/task/detail/promise_type.hpp | 122 + .../include/beman/task/detail/result_type.hpp | 219 ++ .../beman/task/detail/scheduler_of.hpp | 34 + .../task/detail/single_thread_context.hpp | 33 + .../task/include/beman/task/detail/state.hpp | 77 + .../include/beman/task/detail/state_base.hpp | 62 + .../include/beman/task/detail/state_rep.hpp | 48 + .../include/beman/task/detail/stop_source.hpp | 27 + .../include/beman/task/detail/sub_visit.hpp | 44 + .../task/include/beman/task/detail/task.hpp | 93 + .../beman/task/detail/task_scheduler.hpp | 203 ++ .../include/beman/task/detail/with_error.hpp | 29 + subprojects/task/include/beman/task/task.hpp | 54 + subprojects/task/infra/.beman_submodule | 3 + subprojects/task/infra/.github/CODEOWNERS | 1 + .../infra/.github/workflows/pre-commit.yml | 78 + ...reusable-beman-create-issue-when-fault.yml | 28 + subprojects/task/infra/.gitignore | 59 + .../task/infra/.pre-commit-config.yaml | 21 + subprojects/task/infra/LICENSE | 219 ++ subprojects/task/infra/README.md | 54 + subprojects/task/infra/cmake/Config.cmake.in | 12 + .../infra/cmake/appleclang-toolchain.cmake | 44 + .../infra/cmake/beman-install-library.cmake | 323 +++ .../task/infra/cmake/gnu-toolchain.cmake | 41 + .../infra/cmake/llvm-libc++-toolchain.cmake | 20 + .../task/infra/cmake/llvm-toolchain.cmake | 41 + .../task/infra/cmake/msvc-toolchain.cmake | 41 + .../task/infra/cmake/use-fetch-content.cmake | 187 ++ subprojects/task/lockfile.json | 3 + .../task/src/beman/task/CMakeLists.txt | 53 + subprojects/task/src/beman/task/task.cpp | 4 + .../task/tests/beman/task/CMakeLists.txt | 36 + .../tests/beman/task/allocator_of.test.cpp | 33 + .../beman/task/allocator_support.test.cpp | 63 + .../task/tests/beman/task/completion.test.cpp | 18 + .../tests/beman/task/error_types_of.test.cpp | 28 + .../tests/beman/task/final_awaiter.test.cpp | 45 + .../tests/beman/task/find_allocator.test.cpp | 46 + .../task/tests/beman/task/handle.test.cpp | 93 + .../task/tests/beman/task/lazy.test.cpp | 22 + .../tests/beman/task/msvc-asan-issue.test.cpp | 24 + .../task/tests/beman/task/poly.test.cpp | 299 +++ .../tests/beman/task/promise_base.test.cpp | 103 + .../tests/beman/task/promise_type.test.cpp | 206 ++ .../tests/beman/task/result_type.test.cpp | 109 + .../tests/beman/task/scheduler_of.test.cpp | 33 + .../beman/task/single_thread_context.test.cpp | 25 + .../task/tests/beman/task/state_base.test.cpp | 59 + .../task/tests/beman/task/sub_visit.test.cpp | 37 + .../task/tests/beman/task/task.test.cpp | 60 + .../tests/beman/task/task_scheduler.test.cpp | 265 +++ .../task/tests/beman/task/with_error.test.cpp | 17 + 201 files changed, 16870 insertions(+), 1516 deletions(-) create mode 100644 common/coroutines.h create mode 100644 subprojects/.wraplock create mode 100644 subprojects/task.wrap create mode 100644 subprojects/task/.clang-format create mode 100644 subprojects/task/.clang-format-ignore create mode 100644 subprojects/task/.clang-tidy create mode 100644 subprojects/task/.codespellignore create mode 100644 subprojects/task/.codespellrc create mode 100644 subprojects/task/.devcontainer/Dockerfile create mode 100644 subprojects/task/.devcontainer/devcontainer.json create mode 100644 subprojects/task/.devcontainer/postcreate.sh create mode 100644 subprojects/task/.github/CODEOWNERS create mode 100644 subprojects/task/.github/workflows/ci_tests.yml create mode 100644 subprojects/task/.github/workflows/pre-commit-check.yml create mode 100644 subprojects/task/.github/workflows/pre-commit-update.yml create mode 100644 subprojects/task/.github/workflows/pre-commit.yml create mode 100644 subprojects/task/.gitignore create mode 100644 subprojects/task/.meson-subproject-wrap-hash.txt create mode 100644 subprojects/task/.pre-commit-config.yaml create mode 100644 subprojects/task/CMakeLists.txt create mode 100644 subprojects/task/CMakePresets.json create mode 100644 subprojects/task/LICENSE create mode 100644 subprojects/task/Makefile create mode 100644 subprojects/task/README.md create mode 100644 subprojects/task/docs/Makefile create mode 100644 subprojects/task/docs/P3552-task.md create mode 100644 subprojects/task/docs/P3796-task-issues.md create mode 100644 subprojects/task/docs/P3941-affinity.md create mode 100644 subprojects/task/docs/P3980-allocation.md create mode 100644 subprojects/task/docs/c++now.md create mode 100644 subprojects/task/docs/compiler-explorer.ico create mode 100644 subprojects/task/docs/contributing.md create mode 100644 subprojects/task/docs/examples.md create mode 100644 subprojects/task/docs/issues-overview.md create mode 100644 subprojects/task/docs/resources.md create mode 100644 subprojects/task/docs/task-issues.md create mode 100644 subprojects/task/examples/CMakeLists.txt create mode 100644 subprojects/task/examples/affinity.cpp create mode 100644 subprojects/task/examples/aggregate-return.cpp create mode 100644 subprojects/task/examples/alloc-1.cpp create mode 100644 subprojects/task/examples/alloc-2.cpp create mode 100644 subprojects/task/examples/alloc.cpp create mode 100644 subprojects/task/examples/async-lock.cpp create mode 100644 subprojects/task/examples/bulk.cpp create mode 100644 subprojects/task/examples/c++now-affinity.cpp create mode 100644 subprojects/task/examples/c++now-allocator.cpp create mode 100644 subprojects/task/examples/c++now-basic.cpp create mode 100644 subprojects/task/examples/c++now-cancel.cpp create mode 100644 subprojects/task/examples/c++now-errors.cpp create mode 100644 subprojects/task/examples/c++now-query.cpp create mode 100644 subprojects/task/examples/c++now-result-types.cpp create mode 100644 subprojects/task/examples/c++now-return.cpp create mode 100644 subprojects/task/examples/c++now-stop_token.cpp create mode 100644 subprojects/task/examples/c++now-with_error.cpp create mode 100644 subprojects/task/examples/co_await-result.cpp create mode 100644 subprojects/task/examples/co_await-task.cpp create mode 100644 subprojects/task/examples/container.cpp create mode 100644 subprojects/task/examples/customize.cpp create mode 100644 subprojects/task/examples/dangling-references.cpp create mode 100644 subprojects/task/examples/demo-scope.hpp create mode 100644 subprojects/task/examples/demo-thread_loop.hpp create mode 100644 subprojects/task/examples/demo-tls_scheduler.hpp create mode 100644 subprojects/task/examples/environment.cpp create mode 100644 subprojects/task/examples/error.cpp create mode 100644 subprojects/task/examples/escaped-exception.cpp create mode 100644 subprojects/task/examples/friendly.cpp create mode 100644 subprojects/task/examples/hello.cpp create mode 100644 subprojects/task/examples/http-server.cpp create mode 100644 subprojects/task/examples/into_optional.cpp create mode 100644 subprojects/task/examples/issue-affine_on.cpp create mode 100644 subprojects/task/examples/issue-frame-allocator.cpp create mode 100644 subprojects/task/examples/issue-start-reschedules.cpp create mode 100644 subprojects/task/examples/issue-symmetric-transfer.cpp create mode 100644 subprojects/task/examples/loop.cpp create mode 100644 subprojects/task/examples/odd-completions.cpp create mode 100644 subprojects/task/examples/odd-return.cpp create mode 100644 subprojects/task/examples/query.cpp create mode 100644 subprojects/task/examples/result_example.cpp create mode 100644 subprojects/task/examples/rvalue-task.cpp create mode 100644 subprojects/task/examples/stop.cpp create mode 100644 subprojects/task/examples/task-sender.cpp create mode 100644 subprojects/task/examples/task_scheduler.cpp create mode 100644 subprojects/task/examples/tls-scheduler.cpp create mode 100644 subprojects/task/include/beman/execution/task.hpp create mode 100644 subprojects/task/include/beman/lazy/lazy.hpp create mode 100644 subprojects/task/include/beman/task/detail/allocator_of.hpp create mode 100644 subprojects/task/include/beman/task/detail/allocator_support.hpp create mode 100644 subprojects/task/include/beman/task/detail/awaiter.hpp create mode 100644 subprojects/task/include/beman/task/detail/change_coroutine_scheduler.hpp create mode 100644 subprojects/task/include/beman/task/detail/completion.hpp create mode 100644 subprojects/task/include/beman/task/detail/error_types_of.hpp create mode 100644 subprojects/task/include/beman/task/detail/final_awaiter.hpp create mode 100644 subprojects/task/include/beman/task/detail/find_allocator.hpp create mode 100644 subprojects/task/include/beman/task/detail/handle.hpp create mode 100644 subprojects/task/include/beman/task/detail/infallible_scheduler.hpp create mode 100644 subprojects/task/include/beman/task/detail/into_optional.hpp create mode 100644 subprojects/task/include/beman/task/detail/logger.hpp create mode 100644 subprojects/task/include/beman/task/detail/poly.hpp create mode 100644 subprojects/task/include/beman/task/detail/promise_base.hpp create mode 100644 subprojects/task/include/beman/task/detail/promise_env.hpp create mode 100644 subprojects/task/include/beman/task/detail/promise_type.hpp create mode 100644 subprojects/task/include/beman/task/detail/result_type.hpp create mode 100644 subprojects/task/include/beman/task/detail/scheduler_of.hpp create mode 100644 subprojects/task/include/beman/task/detail/single_thread_context.hpp create mode 100644 subprojects/task/include/beman/task/detail/state.hpp create mode 100644 subprojects/task/include/beman/task/detail/state_base.hpp create mode 100644 subprojects/task/include/beman/task/detail/state_rep.hpp create mode 100644 subprojects/task/include/beman/task/detail/stop_source.hpp create mode 100644 subprojects/task/include/beman/task/detail/sub_visit.hpp create mode 100644 subprojects/task/include/beman/task/detail/task.hpp create mode 100644 subprojects/task/include/beman/task/detail/task_scheduler.hpp create mode 100644 subprojects/task/include/beman/task/detail/with_error.hpp create mode 100644 subprojects/task/include/beman/task/task.hpp create mode 100644 subprojects/task/infra/.beman_submodule create mode 100644 subprojects/task/infra/.github/CODEOWNERS create mode 100644 subprojects/task/infra/.github/workflows/pre-commit.yml create mode 100644 subprojects/task/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml create mode 100644 subprojects/task/infra/.gitignore create mode 100644 subprojects/task/infra/.pre-commit-config.yaml create mode 100644 subprojects/task/infra/LICENSE create mode 100644 subprojects/task/infra/README.md create mode 100644 subprojects/task/infra/cmake/Config.cmake.in create mode 100644 subprojects/task/infra/cmake/appleclang-toolchain.cmake create mode 100644 subprojects/task/infra/cmake/beman-install-library.cmake create mode 100644 subprojects/task/infra/cmake/gnu-toolchain.cmake create mode 100644 subprojects/task/infra/cmake/llvm-libc++-toolchain.cmake create mode 100644 subprojects/task/infra/cmake/llvm-toolchain.cmake create mode 100644 subprojects/task/infra/cmake/msvc-toolchain.cmake create mode 100644 subprojects/task/infra/cmake/use-fetch-content.cmake create mode 100644 subprojects/task/lockfile.json create mode 100644 subprojects/task/src/beman/task/CMakeLists.txt create mode 100644 subprojects/task/src/beman/task/task.cpp create mode 100644 subprojects/task/tests/beman/task/CMakeLists.txt create mode 100644 subprojects/task/tests/beman/task/allocator_of.test.cpp create mode 100644 subprojects/task/tests/beman/task/allocator_support.test.cpp create mode 100644 subprojects/task/tests/beman/task/completion.test.cpp create mode 100644 subprojects/task/tests/beman/task/error_types_of.test.cpp create mode 100644 subprojects/task/tests/beman/task/final_awaiter.test.cpp create mode 100644 subprojects/task/tests/beman/task/find_allocator.test.cpp create mode 100644 subprojects/task/tests/beman/task/handle.test.cpp create mode 100644 subprojects/task/tests/beman/task/lazy.test.cpp create mode 100644 subprojects/task/tests/beman/task/msvc-asan-issue.test.cpp create mode 100644 subprojects/task/tests/beman/task/poly.test.cpp create mode 100644 subprojects/task/tests/beman/task/promise_base.test.cpp create mode 100644 subprojects/task/tests/beman/task/promise_type.test.cpp create mode 100644 subprojects/task/tests/beman/task/result_type.test.cpp create mode 100644 subprojects/task/tests/beman/task/scheduler_of.test.cpp create mode 100644 subprojects/task/tests/beman/task/single_thread_context.test.cpp create mode 100644 subprojects/task/tests/beman/task/state_base.test.cpp create mode 100644 subprojects/task/tests/beman/task/sub_visit.test.cpp create mode 100644 subprojects/task/tests/beman/task/task.test.cpp create mode 100644 subprojects/task/tests/beman/task/task_scheduler.test.cpp create mode 100644 subprojects/task/tests/beman/task/with_error.test.cpp diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 103e054e4..df0f35563 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7 diff --git a/common/coroutines.h b/common/coroutines.h new file mode 100644 index 000000000..a054d99af --- /dev/null +++ b/common/coroutines.h @@ -0,0 +1,30 @@ +/* coroutines.h + * + * Copyright (c) 2026 Andrey Kutejko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#ifndef _COROUTINES_H_ +#define _COROUTINES_H_ + +#include +#include + +template +using task = beman::execution::task; + +#endif diff --git a/common/meson.build b/common/meson.build index dada45930..f9cd20a10 100644 --- a/common/meson.build +++ b/common/meson.build @@ -23,7 +23,7 @@ libsynaptic = static_library( 'synaptic', common_sources, cpp_args: base_cpp_args + rpm_compile_args, - dependencies: common_deps, + dependencies: common_deps + [task_dep], include_directories: [root_inc, common_inc], ) diff --git a/common/rinstallprogress.cc b/common/rinstallprogress.cc index c9fce7de5..880187418 100644 --- a/common/rinstallprogress.cc +++ b/common/rinstallprogress.cc @@ -68,7 +68,7 @@ const char* RInstallProgress::getResultStr(pkgPackageManager::OrderResult res) return "Unknown install result."; } -pkgPackageManager::OrderResult RInstallProgress::start(pkgPackageManager *pm, +task RInstallProgress::start(pkgPackageManager *pm, int numPackages, int numPackagesTotal) { @@ -85,7 +85,7 @@ pkgPackageManager::OrderResult RInstallProgress::start(pkgPackageManager *pm, res = pm->DoInstallPreFork(); if (res == pkgPackageManager::Failed) - return res; + co_return res; /* * This will make a pipe from where we can read child's output @@ -124,7 +124,7 @@ pkgPackageManager::OrderResult RInstallProgress::start(pkgPackageManager *pm, res = pm->DoInstallPreFork(); if (res == pkgPackageManager::Failed) - return res; + co_return res; _child_id = fork(); @@ -135,19 +135,19 @@ pkgPackageManager::OrderResult RInstallProgress::start(pkgPackageManager *pm, } #endif - startUpdate(); + co_await startUpdate(); while (waitpid(_child_id, &ret, WNOHANG) == 0) - updateInterface(); + co_await updateInterface(); res = (pkgPackageManager::OrderResult) WEXITSTATUS(ret); - finishUpdate(); + co_await finishUpdate(); #ifdef HAVE_RPM close(_childin); #endif - return res; + co_return res; } // vim:sts=4:sw=4 diff --git a/common/rinstallprogress.h b/common/rinstallprogress.h index 75f3ca8f1..9ef98754e 100644 --- a/common/rinstallprogress.h +++ b/common/rinstallprogress.h @@ -31,6 +31,8 @@ #include +#include "coroutines.h" + class RInstallProgress { protected: int _stdout; @@ -49,22 +51,26 @@ class RInstallProgress { static std::string errorMsg; static std::string incompleteMsg; - virtual void startUpdate() { + [[nodiscard]] virtual task startUpdate() { + co_return; } - virtual void updateInterface() { + [[nodiscard]] virtual task updateInterface() { + co_return; } - virtual void finishUpdate() { + [[nodiscard]] virtual task finishUpdate() { + co_return; } public: // get a str feed to the user with the result of the install run virtual const char* getResultStr(pkgPackageManager::OrderResult); - virtual pkgPackageManager::OrderResult start(pkgPackageManager *pm, + [[nodiscard]] virtual task start(pkgPackageManager *pm, int numPackages = 0, int numPackagesTotal = 0); RInstallProgress():_donePackagesTotal(0), _numPackagesTotal(0),_updateFinished(false) {} + virtual ~RInstallProgress() {} }; diff --git a/common/rpackagelister.cc b/common/rpackagelister.cc index 16d0a03b5..99a7a66c0 100644 --- a/common/rpackagelister.cc +++ b/common/rpackagelister.cc @@ -1406,7 +1406,7 @@ bool RPackageLister::getDownloadUris(vector &uris) return true; } -bool RPackageLister::commitChanges(pkgAcquireStatus *status, +task RPackageLister::commitChanges(pkgAcquireStatus *status, RInstallProgress *iprog) { FileFd lock; @@ -1417,7 +1417,7 @@ bool RPackageLister::commitChanges(pkgAcquireStatus *status, _updating = true; if (!lockPackageCache(lock)) - return false; + co_return false; if(_config->FindB("Synaptic::Log::Changes",true)) makeCommitLog(); @@ -1427,7 +1427,7 @@ bool RPackageLister::commitChanges(pkgAcquireStatus *status, assert(_cache->list() != NULL); // Read the source list if (_cache->list()->ReadMainList() == false) { - _userDialog-> + co_await _userDialog-> warning(_("Ignoring invalid record(s) in sources.list file!")); } @@ -1490,7 +1490,7 @@ bool RPackageLister::commitChanges(pkgAcquireStatus *status, if (_config->FindB("Volatile::Download-Only", false)) { _updating = false; - return !Failed; + co_return !Failed; } if (Failed) { @@ -1508,7 +1508,7 @@ bool RPackageLister::commitChanges(pkgAcquireStatus *status, message += "(" + serverError + ")\n"; message += _("Do you want to continue, ignoring these packages?"); - if (!_userDialog->confirm(message.c_str())) + if (!co_await _userDialog->confirm(message.c_str())) goto gave_wood; } // Try to deal with missing package files @@ -1527,7 +1527,7 @@ bool RPackageLister::commitChanges(pkgAcquireStatus *status, _system->UnLockInner(); pkgPackageManager::OrderResult Res = - iprog->start(rPM, numPackages, numPackagesTotal); + co_await iprog->start(rPM, numPackages, numPackagesTotal); _system->LockInner(); if (Res == pkgPackageManager::Failed || _error->PendingError()) { if (Transient == false) @@ -1562,11 +1562,11 @@ bool RPackageLister::commitChanges(pkgAcquireStatus *status, writeCommitLog(); delete rPM; - return Ret; + co_return Ret; gave_wood: delete rPM; - return false; + co_return false; } void RPackageLister::writeCommitLog() diff --git a/common/rpackagelister.h b/common/rpackagelister.h index 747dc6f54..7513eb0f0 100644 --- a/common/rpackagelister.h +++ b/common/rpackagelister.h @@ -29,6 +29,7 @@ #include "rpackagecache.h" #include "rpackagestatus.h" +#include "coroutines.h" #include #include @@ -295,7 +296,7 @@ class RPackageLister { bool distUpgrade(); bool cleanPackageCache(bool forceClean = false); bool updateCache(pkgAcquireStatus *status, std::string &error); - bool commitChanges(pkgAcquireStatus *status, RInstallProgress *iprog); + [[nodiscard]] task commitChanges(pkgAcquireStatus *status, RInstallProgress *iprog); // some information bool getDownloadUris(std::vector &uris); diff --git a/common/ruserdialog.cc b/common/ruserdialog.cc index 96ecb2f41..fa1fd5293 100644 --- a/common/ruserdialog.cc +++ b/common/ruserdialog.cc @@ -27,10 +27,10 @@ #include #include "ruserdialog.h" -bool RUserDialog::showErrors() +task RUserDialog::showErrors() { if (_error->empty()) - return false; + co_return false; while (!_error->empty()) { std::string message; @@ -42,13 +42,13 @@ bool RUserDialog::showErrors() if (!message.empty()) { if (iserror) - error(message.c_str()); + co_await error(message.c_str()); else - warning(message.c_str()); + co_await warning(message.c_str()); } } - return true; + co_return true; } // vim:ts=3:sw=3:et diff --git a/common/ruserdialog.h b/common/ruserdialog.h index 6fb67bf0c..ab3d006fb 100644 --- a/common/ruserdialog.h +++ b/common/ruserdialog.h @@ -26,6 +26,8 @@ #include "config.h" // IWYU pragma: associated +#include "coroutines.h" + class RUserDialog { public: enum ButtonsType { @@ -42,29 +44,31 @@ class RUserDialog { DialogError }; - virtual bool message(const char *msg, + [[nodiscard]] virtual task message(const char *msg, DialogType dialog = DialogInfo, ButtonsType buttons = ButtonsDefault, bool defaultResponse = true) = 0; - virtual bool confirm(const char *msg, bool defaultResponse = true) { - return message(msg, DialogQuestion, ButtonsYesNo, defaultResponse); + [[nodiscard]] virtual task confirm(const char *msg, bool defaultResponse = true) { + co_return co_await message(msg, DialogQuestion, ButtonsYesNo, defaultResponse); } - virtual bool proceed(const char *msg, bool defaultResponse = true) { - return message(msg, DialogInfo, ButtonsOkCancel, defaultResponse); + [[nodiscard]] virtual task proceed(const char *msg, bool defaultResponse = true) { + co_return co_await message(msg, DialogInfo, ButtonsOkCancel, defaultResponse); } - virtual bool warning(const char *msg, bool nocancel = true) { - return nocancel ? message(msg, DialogWarning) - : message(msg, DialogWarning, ButtonsOkCancel, false); + [[nodiscard]] virtual task warning(const char *msg, bool nocancel = true) { + if (nocancel) + co_return co_await message(msg, DialogWarning); + else + co_return co_await message(msg, DialogWarning, ButtonsOkCancel, false); } - virtual void error(const char *msg) { - message(msg, DialogError); + [[nodiscard]] virtual task error(const char *msg) { + co_await message(msg, DialogError); } - virtual bool showErrors(); + [[nodiscard]] virtual task showErrors(); }; diff --git a/debian/control b/debian/control index 3f1ee8934..368eab0e6 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: synaptic Section: admin Priority: optional Maintainer: Michael Vogt -Build-Depends: debhelper-compat (= 12), gettext, meson, ninja-build, libapt-pkg-dev, libgtk-3-dev, libvte-2.91-dev, libpolkit-gobject-1-dev, libsm-dev, lsb-release, libxapian-dev, xmlto +Build-Depends: debhelper-compat (= 12), gettext, meson, ninja-build, cmake, libapt-pkg-dev, libgtk-3-dev, libvte-2.91-dev, libpolkit-gobject-1-dev, libsm-dev, lsb-release, libxapian-dev, libtbb-dev, xmlto Build-Conflicts: librpm-dev Standards-Version: 4.5.0 Vcs-Git: https://github.com/mvo5/synaptic.git diff --git a/gtk/gsynaptic.cc b/gtk/gsynaptic.cc index d286e663a..1e10afe19 100644 --- a/gtk/gsynaptic.cc +++ b/gtk/gsynaptic.cc @@ -69,6 +69,8 @@ static gint applicationHandleLocalOptions(GApplication* app, gpointer user_data); static void applicationStartup(GApplication* app, gpointer user_data); static void applicationActivate(GApplication* app, gpointer user_data); +[[nodiscard]] static task coApplicationActivate(RGMainWindow *mainWindow, + RPackageLister *packageLister); static GOptionEntry option_entries[] = { @@ -115,13 +117,13 @@ static void SetLanguages() _config->Set("Volatile::Languages", LangList); } -void welcome_dialog(RGMainWindow *mainWindow) +[[nodiscard]] task welcome_dialog(RGMainWindow *mainWindow) { // show welcome dialog if (_config->FindB("Synaptic::showWelcomeDialog", true) && !_config->FindB("Volatile::Upgrade-Mode",false)) { RGGtkBuilderUserDialog dia(mainWindow); - dia.run("welcome"); + co_await dia.co_run("welcome"); GtkWidget *cb = GTK_WIDGET(gtk_builder_get_object(dia.getGtkBuilder(), "checkbutton_show_again")); assert(cb); @@ -216,10 +218,10 @@ pid_t TestLock(string File) // *) if not, send signal // 2. check if we can get a /var/lib/dpkg/lock // *) if not, show message and fail -void check_and_aquire_lock() +[[nodiscard]] static task check_and_aquire_lock() { if (getuid() != 0) - return; + co_return; GtkWidget *dia; gchar *msg = NULL; @@ -260,7 +262,7 @@ void check_and_aquire_lock() NULL); gtk_message_dialog_set_markup(GTK_MESSAGE_DIALOG(dia), msg); - gtk_dialog_run(GTK_DIALOG(dia)); + co_await co_run_dialog(GTK_DIALOG(dia)); gtk_widget_destroy(dia); } g_free(msg); @@ -287,7 +289,7 @@ void check_and_aquire_lock() NULL); gtk_message_dialog_set_markup(GTK_MESSAGE_DIALOG(dia), msg); - gtk_dialog_run(GTK_DIALOG(dia)); + co_await co_run_dialog(GTK_DIALOG(dia)); g_free(msg); exit(1); } @@ -400,15 +402,14 @@ static void applicationStartup(GApplication* app, gpointer user_data) gtk_icon_theme_append_search_path(gtk_icon_theme_get_default(), PACKAGE_DATA_DIR "/synaptic/icons"); if (!RInitConfiguration("synaptic.conf")) { - RGUserDialog userDialog; - userDialog.showErrors(); - exit(1); + start_task([app]() -> task { + RGUserDialog userDialog; + co_await userDialog.showErrors(); + exit(1); + }); + return; } - // check if there is another application runing and - // act accordingly - check_and_aquire_lock(); - // read configuration early _roptions->restore(); @@ -417,6 +418,11 @@ static void applicationStartup(GApplication* app, gpointer user_data) // init the static pkgStatus class. this loads the status pixmaps // and colors RGPackageStatus::pkgStatus.init(); + + start_task([]() -> task { + // check if there is another application runing and act accordingly + co_await check_and_aquire_lock(); + }); } static void applicationActivate(GApplication* app, gpointer user_data) @@ -426,9 +432,6 @@ static void applicationActivate(GApplication* app, gpointer user_data) return; } - bool UpdateMode = _config->FindB("Volatile::Update-Mode",false); - bool NonInteractive = _config->FindB("Volatile::Non-Interactive", false); - RPackageLister *packageLister = new RPackageLister(); RGMainWindow *mainWindow = new RGMainWindow(GTK_APPLICATION(app), packageLister, "main"); @@ -469,9 +472,31 @@ static void applicationActivate(GApplication* app, gpointer user_data) else mainWindow->show(); - RGFlushInterface(); + start_task([mainWindow, packageLister]() -> task { + co_await coApplicationActivate(mainWindow, packageLister); + }); +} - mainWindow->setInterfaceLocked(true); +[[nodiscard]] static task coApplicationActivate(RGMainWindow *mainWindow, + RPackageLister *packageLister) +{ + if (getuid() != 0) { + RGUserDialog userDialog; + co_await userDialog.warning(g_strdup_printf("%s\n\n%s", + _("Starting \"Synaptic Package Manager\" without " + "administrative privileges"), + _("You will not be able to apply " + "any changes, but you can still " + "export the marked changes or " + "create a download script " + "for them."))); + } + + bool UpdateMode = _config->FindB("Volatile::Update-Mode",false); + bool NonInteractive = _config->FindB("Volatile::Non-Interactive", false); + + co_await RGFlushInterface(); + co_await mainWindow->setInterfaceLocked(true); string cd_mount_point = _config->Find("Volatile::AddCdrom-Mode", ""); if(!cd_mount_point.empty()) { @@ -487,14 +512,14 @@ static void applicationActivate(GApplication* app, gpointer user_data) if(!UpdateMode) { mainWindow->setTreeLocked(true); if(!packageLister->openCache()) { - mainWindow->showErrors(); + co_await mainWindow->showErrors(); exit(1); } - mainWindow->restoreState(); - mainWindow->showErrors(); + co_await mainWindow->restoreState(); + co_await mainWindow->showErrors(); mainWindow->setTreeLocked(false); } - + if (_config->FindB("Volatile::startInRepositories", false)) { mainWindow->cbShowSourcesWindow(NULL, NULL, mainWindow); } @@ -517,19 +542,19 @@ static void applicationActivate(GApplication* app, gpointer user_data) packageLister->registerObserver(mainWindow); } - mainWindow->setInterfaceLocked(false); + co_await mainWindow->setInterfaceLocked(false); if(UpdateMode) { mainWindow->cbUpdateClicked(nullptr, nullptr, mainWindow); mainWindow->setTreeLocked(true); if(!packageLister->openCache()) { - mainWindow->showErrors(); + co_await mainWindow->showErrors(); exit(1); } - mainWindow->restoreState(); + co_await mainWindow->restoreState(); mainWindow->setTreeLocked(false); - mainWindow->showErrors(); - mainWindow->changeView(PACKAGE_VIEW_STATUS, _("Installed (upgradable)")); + co_await mainWindow->showErrors(); + co_await mainWindow->changeView(PACKAGE_VIEW_STATUS, _("Installed (upgradable)")); } if(_config->FindB("Volatile::TestMeHarder", false)) @@ -540,7 +565,7 @@ static void applicationActivate(GApplication* app, gpointer user_data) while(true) { mainWindow->cbUpdateClicked(nullptr, nullptr, mainWindow); - mainWindow->changeView(PACKAGE_VIEW_STATUS, _("Installed")); + co_await mainWindow->changeView(PACKAGE_VIEW_STATUS, _("Installed")); GtkTreePath *p = gtk_tree_path_new_from_string("0"); mainWindow->cbPackageListRowActivated(NULL, p, NULL, mainWindow); @@ -551,10 +576,10 @@ static void applicationActivate(GApplication* app, gpointer user_data) } } - if(_config->FindB("Volatile::Upgrade-Mode",false) + if(_config->FindB("Volatile::Upgrade-Mode",false) || _config->FindB("Volatile::DistUpgrade-Mode",false) ) { mainWindow->cbUpgradeClicked(nullptr, nullptr, mainWindow); - mainWindow->changeView(PACKAGE_VIEW_CUSTOM, _("Marked Changes")); + co_await mainWindow->changeView(PACKAGE_VIEW_CUSTOM, _("Marked Changes")); } if(_config->FindB("Volatile::TaskWindow",false)) { @@ -563,13 +588,13 @@ static void applicationActivate(GApplication* app, gpointer user_data) string filter = _config->Find("Volatile::initialFilter",""); if(filter != "") - mainWindow->changeView(PACKAGE_VIEW_CUSTOM, filter); + co_await mainWindow->changeView(PACKAGE_VIEW_CUSTOM, filter); if (NonInteractive) { mainWindow->cbProceedClicked(nullptr, nullptr, mainWindow); exit(0); } else { - welcome_dialog(mainWindow); + co_await welcome_dialog(mainWindow); gtk_widget_grab_focus( GTK_WIDGET(gtk_builder_get_object( mainWindow->getGtkBuilder(), "entry_fast_search"))); diff --git a/gtk/rgcacheprogress.cc b/gtk/rgcacheprogress.cc index 156096266..6f3eecfaa 100644 --- a/gtk/rgcacheprogress.cc +++ b/gtk/rgcacheprogress.cc @@ -53,34 +53,38 @@ RGCacheProgress::~RGCacheProgress() void RGCacheProgress::Update() { if (!CheckChange()) { - //RGFlushInterface(); + //co_await RGFlushInterface(); return; } - if (!_mapped) { - gtk_widget_show(_prog); - RGFlushInterface(); - _mapped = true; - } + start_task([this]() -> task { + if (!_mapped) { + gtk_widget_show(_prog); + co_await RGFlushInterface(); + _mapped = true; + } - if (MajorChange) - gtk_label_set_text(GTK_LABEL(_label), utf8(Op.c_str())); + if (MajorChange) + gtk_label_set_text(GTK_LABEL(_label), utf8(Op.c_str())); - // only call set_fraction when the changes are noticable (0.1%) - if (fabs(Percent- - gtk_progress_bar_get_fraction(GTK_PROGRESS_BAR(_prog))*100.0) > 0.1) - gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_prog), - (float)Percent / 100.0); + // only call set_fraction when the changes are noticable (0.1%) + if (fabs(Percent- + gtk_progress_bar_get_fraction(GTK_PROGRESS_BAR(_prog))*100.0) > 0.1) + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_prog), + (float)Percent / 100.0); - RGFlushInterface(); + co_await RGFlushInterface(); + }); } void RGCacheProgress::Done() { - gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_prog), 1.0); - RGFlushInterface(); + start_task([this]() -> task { + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_prog), 1.0); + co_await RGFlushInterface(); - gtk_widget_hide(_prog); + gtk_widget_hide(_prog); - _mapped = false; + _mapped = false; + }); } diff --git a/gtk/rgcacheprogress.h b/gtk/rgcacheprogress.h index 386bfe665..2643ce19c 100644 --- a/gtk/rgcacheprogress.h +++ b/gtk/rgcacheprogress.h @@ -27,6 +27,7 @@ #include #include +#include "coroutines.h" class RGCacheProgress:public OpProgress { GtkWidget *_parent; diff --git a/gtk/rgcdscanner.cc b/gtk/rgcdscanner.cc index 4fb60f12a..bc95f949e 100644 --- a/gtk/rgcdscanner.cc +++ b/gtk/rgcdscanner.cc @@ -55,7 +55,7 @@ void RGCDScanner::update(string text, int current) gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_pbar), ((float)current) / _total); show(); - RGFlushInterface(); + co_await RGFlushInterface(); } RGCDScanner::RGCDScanner(RGMainWindow *main, RUserDialog *userDialog) diff --git a/gtk/rgchangelogdialog.cc b/gtk/rgchangelogdialog.cc index 772e8a7b7..479b334f8 100644 --- a/gtk/rgchangelogdialog.cc +++ b/gtk/rgchangelogdialog.cc @@ -40,7 +40,7 @@ class RGWindow; using namespace std; -void ShowChangelogDialog(RGWindow *me, RPackage *pkg) +task ShowChangelogDialog(RGWindow *me, RPackage *pkg) { RGFetchProgress *status = new RGFetchProgress(me);; status->setDescription(_("Downloading Changelog"), @@ -83,7 +83,7 @@ void ShowChangelogDialog(RGWindow *me, RPackage *pkg) gtk_text_buffer_insert_at_cursor(buffer, "\n", -1); } - dia.run(); + co_await dia.co_run(); // clean up delete status; diff --git a/gtk/rgchangelogdialog.h b/gtk/rgchangelogdialog.h index 3872d04d5..48edc306b 100644 --- a/gtk/rgchangelogdialog.h +++ b/gtk/rgchangelogdialog.h @@ -23,9 +23,11 @@ #include "config.h" // IWYU pragma: associated +#include "coroutines.h" + class RGWindow; class RPackage; -void ShowChangelogDialog(RGWindow *me, RPackage *pkg); +[[nodiscard]] task ShowChangelogDialog(RGWindow *me, RPackage *pkg); #endif diff --git a/gtk/rgdebinstallprogress.cc b/gtk/rgdebinstallprogress.cc index af43e0a87..f2e35528c 100644 --- a/gtk/rgdebinstallprogress.cc +++ b/gtk/rgdebinstallprogress.cc @@ -173,7 +173,7 @@ int ipc_send_fd(int fd) return 0; } -int ipc_recv_fd() +[[nodiscard]] static task ipc_recv_fd() { int ret; @@ -200,8 +200,8 @@ int ipc_recv_fd() connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); if(connfd > 0) break; - usleep(1000); - RGFlushInterface(); + co_await sleep_ms { 1 }; + co_await RGFlushInterface(); } // read_fd read_fd(connfd, &c,1,&fd); @@ -209,10 +209,10 @@ int ipc_recv_fd() close(connfd); close(listenfd); - return fd; + co_return fd; } -void RGDebInstallProgress::conffile(gchar *conffile, gchar *status) +task RGDebInstallProgress::conffile(gchar *conffile, gchar *status) { string primary, secondary; gchar *m,*s,*p; @@ -282,7 +282,7 @@ void RGDebInstallProgress::conffile(gchar *conffile, gchar *status) GtkTextBuffer *text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view)); gtk_text_buffer_set_text(text_buffer,diff.c_str(),-1); - int res = dia.run(NULL,true); + int res = co_await dia.co_run(NULL,true); if(res == GTK_RESPONSE_YES) vte_terminal_feed_child(VTE_TERMINAL(_term), "y\n",2); else @@ -292,11 +292,11 @@ void RGDebInstallProgress::conffile(gchar *conffile, gchar *status) last_term_action = time(NULL); } -void RGDebInstallProgress::startUpdate() +task RGDebInstallProgress::startUpdate() { child_has_exited=false; show(); - RGFlushInterface(); + co_await RGFlushInterface(); } void RGDebInstallProgress::cbCancel(GtkWidget *self, void *data) @@ -317,10 +317,11 @@ void RGDebInstallProgress::cbClose(GtkWidget *self, void *data) me->_updateFinished = true; } -bool RGDebInstallProgress::close() +gboolean RGDebInstallProgress::windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data) { - if(child_has_exited) - cbClose(NULL, this); + auto me = static_cast(data); + if (me->child_has_exited) + cbClose(NULL, me); return TRUE; } @@ -427,6 +428,8 @@ RGDebInstallProgress::RGDebInstallProgress(RGMainWindow *main, g_signal_connect(VTE_TERMINAL(_term), "child-exited", G_CALLBACK(child_exited), this); + g_signal_connect(G_OBJECT(_win), "delete-event", + G_CALLBACK(windowCloseCallback), this); if(_userDialog == NULL) _userDialog = new RGUserDialog(this); @@ -458,23 +461,23 @@ gboolean RGDebInstallProgress::key_press_event(GtkWidget *widget, // user pressed ctrl-c if (event->keyval == GDK_c && event->state & GDK_CONTROL_MASK) { - gchar *summary = _("Ctrl-c pressed"); - char *msg = _("This will abort the operation and may leave the system " - "in a broken state. Are you sure you want to do that?"); - GtkWidget *dia = gtk_message_dialog_new (GTK_WINDOW(me->_win), - GTK_DIALOG_DESTROY_WITH_PARENT, - GTK_MESSAGE_WARNING, - GTK_BUTTONS_YES_NO, - "%s", summary); - gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG(dia), "%s", msg); - int res = gtk_dialog_run (GTK_DIALOG (dia)); - gtk_widget_destroy (dia); - switch(res) { - case GTK_RESPONSE_YES: - return false; - case GTK_RESPONSE_NO: - return true; - } + start_task([me]() -> task { + gchar *summary = _("Ctrl-c pressed"); + char *msg = _("This will abort the operation and may leave the system " + "in a broken state. Are you sure you want to do that?"); + GtkWidget *dia = gtk_message_dialog_new (GTK_WINDOW(me->_win), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_YES_NO, + "%s", summary); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG(dia), "%s", msg); + int res = co_await co_run_dialog (GTK_DIALOG (dia)); + gtk_widget_destroy (dia); + if (res == GTK_RESPONSE_YES) { + vte_terminal_feed_child(VTE_TERMINAL(me->_term), "\x03", 1); // Send Ctrl+C to terminal + } + }); + return true; } else if (event->keyval == GDK_C && event->state & (GDK_CONTROL_MASK|GDK_SHIFT_MASK)) { // ctrl+shift+C copy to clipboard to mimic gnome-terminal behavior @@ -524,7 +527,7 @@ void RGDebInstallProgress::terminalAction(GtkWidget *terminal, } } -void RGDebInstallProgress::updateInterface() +task RGDebInstallProgress::updateInterface() { char buf[2]; static char line[1024] = ""; @@ -570,7 +573,7 @@ void RGDebInstallProgress::updateInterface() } else if(strstr(status, "pmconffile") != NULL) { // conffile-request from dpkg, needs to be parsed different //cout << split[2] << " " << split[3] << endl; - conffile(pkg, split[3]); + co_await conffile(pkg, split[3]); } else { _startCounting = true; gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_pbarTotal), 0); @@ -623,17 +626,11 @@ void RGDebInstallProgress::updateInterface() gtk_window_set_urgency_hint(GTK_WINDOW(_win), TRUE); } - - if (gtk_events_pending()) { - while (gtk_events_pending()) - gtk_main_iteration(); - } else { - // 25fps - usleep(1000000/25); - } + // 25fps + co_await sleep_ms { 40 }; } -pkgPackageManager::OrderResult RGDebInstallProgress::start(pkgPackageManager *pm, +task RGDebInstallProgress::start(pkgPackageManager *pm, int numPackages, int numPackagesTotal) { @@ -643,7 +640,7 @@ pkgPackageManager::OrderResult RGDebInstallProgress::start(pkgPackageManager *pm res = pm->DoInstallPreFork(); if (res == pkgPackageManager::Failed) - return res; + co_return res; int master; _child_id = forkpty(&master, NULL, NULL, NULL); @@ -658,9 +655,9 @@ pkgPackageManager::OrderResult RGDebInstallProgress::start(pkgPackageManager *pm NULL); gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), _("Error failed to fork pty")); - gtk_dialog_run(GTK_DIALOG(dialog)); + co_await co_run_dialog(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - return res; + co_return res; } else if (_child_id == 0) { int fd[2]; pipe(fd); @@ -691,14 +688,14 @@ pkgPackageManager::OrderResult RGDebInstallProgress::start(pkgPackageManager *pm if (err != NULL) { std::cerr << "failed to create new pty: " << err->message << std::endl; g_error_free (err); - return pkgPackageManager::Failed; + co_return pkgPackageManager::Failed; } vte_terminal_set_pty(VTE_TERMINAL(_term), pty); // FIXME: is there a race here? i.e. what if the child is dead before // we can set it? vte_terminal_watch_child(VTE_TERMINAL(_term), _child_id); - _childin = ipc_recv_fd(); + _childin = co_await ipc_recv_fd(); if(_childin < 0) { // something _bad_ happend. so the terminal window and hope for the best GtkWidget *w = GTK_WIDGET(gtk_builder_get_object(_builder, "expander_terminal")); @@ -713,28 +710,28 @@ pkgPackageManager::OrderResult RGDebInstallProgress::start(pkgPackageManager *pm _numPackages = numPackages; _numPackagesTotal = numPackagesTotal; - startUpdate(); + co_await startUpdate(); while(!child_has_exited) - updateInterface(); + co_await updateInterface(); - finishUpdate(); + co_await finishUpdate(); ::close(_childin); ::close(master); _config->Clear("APT::Keep-Fds", _childin); - return res; + co_return res; } -void RGDebInstallProgress::finishUpdate() +task RGDebInstallProgress::finishUpdate() { if (_startCounting) { gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_pbarTotal), 1.0); } - RGFlushInterface(); + co_await RGFlushInterface(); GtkWidget *_closeB = GTK_WIDGET(gtk_builder_get_object(_builder, "button_close")); gtk_widget_set_sensitive(_closeB, TRUE); @@ -768,7 +765,7 @@ void RGDebInstallProgress::finishUpdate() case 1: // error gtk_image_set_from_icon_name(GTK_IMAGE(img), "dialog-error", GTK_ICON_SIZE_DIALOG); - _userDialog->showErrors(); + co_await _userDialog->showErrors(); break; case 2: // incomplete gtk_image_set_from_icon_name(GTK_IMAGE(img), "dialog-information", @@ -780,8 +777,7 @@ void RGDebInstallProgress::finishUpdate() // wait for user action while(true) { // events - while (gtk_events_pending()) - gtk_main_iteration(); + co_await glib_idle {}; // user clicked "close" button if(_updateFinished) @@ -793,7 +789,7 @@ void RGDebInstallProgress::finishUpdate() break; // wait a bit - g_usleep(100000); + co_await sleep_ms { 100 }; } // set the value again, it may have changed diff --git a/gtk/rgdebinstallprogress.h b/gtk/rgdebinstallprogress.h index b9df27e38..2082eb9b1 100644 --- a/gtk/rgdebinstallprogress.h +++ b/gtk/rgdebinstallprogress.h @@ -117,22 +117,22 @@ class RGDebInstallProgress:public RInstallProgress, public RGGtkBuilderWindow static void child_exited(VteTerminal *vteterminal, gint ret, gpointer data); static void terminalAction(GtkWidget *terminal, TermAction action); + static gboolean windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data); GtkCssProvider *_cssProvider; protected: - virtual void startUpdate(); - virtual void updateInterface(); - virtual void finishUpdate(); - virtual bool close(); + [[nodiscard]] virtual task startUpdate(); + [[nodiscard]] virtual task updateInterface(); + [[nodiscard]] virtual task finishUpdate(); - virtual pkgPackageManager::OrderResult start(pkgPackageManager *pm, + [[nodiscard]] virtual task start(pkgPackageManager *pm, int numPackages = 0, int totalPackages = 0); virtual void prepare(RPackageLister *lister); - void conffile(gchar *conffile, gchar *status); + [[nodiscard]] task conffile(gchar *conffile, gchar *status); // gtk stuff static void cbCancel(GtkWidget *self, void *data); diff --git a/gtk/rgdummyinstallprogress.cc b/gtk/rgdummyinstallprogress.cc index d0aabbcc6..d569ed29b 100644 --- a/gtk/rgdummyinstallprogress.cc +++ b/gtk/rgdummyinstallprogress.cc @@ -29,22 +29,17 @@ #include #include -void RGDummyInstallProgress::startUpdate() +task RGDummyInstallProgress::startUpdate() { - RGFlushInterface(); + co_await RGFlushInterface(); } -void RGDummyInstallProgress::finishUpdate() +task RGDummyInstallProgress::finishUpdate() { - RGFlushInterface(); + co_await RGFlushInterface(); } -void RGDummyInstallProgress::updateInterface() +task RGDummyInstallProgress::updateInterface() { - if (gtk_events_pending()) { - while (gtk_events_pending()) - gtk_main_iteration(); - } else { - usleep(1000000); - } + co_await sleep_ms { 1000 }; } diff --git a/gtk/rgdummyinstallprogress.h b/gtk/rgdummyinstallprogress.h index fbe6301aa..73c1257f1 100644 --- a/gtk/rgdummyinstallprogress.h +++ b/gtk/rgdummyinstallprogress.h @@ -29,9 +29,9 @@ class RGDummyInstallProgress:public RInstallProgress { protected: - virtual void startUpdate(); - virtual void updateInterface(); - virtual void finishUpdate(); + [[nodiscard]] virtual task startUpdate(); + [[nodiscard]] virtual task updateInterface(); + [[nodiscard]] virtual task finishUpdate(); public: RGDummyInstallProgress():RInstallProgress() {}; diff --git a/gtk/rgfetchprogress.cc b/gtk/rgfetchprogress.cc index 2d5a917f6..dcd4777cb 100644 --- a/gtk/rgfetchprogress.cc +++ b/gtk/rgfetchprogress.cc @@ -69,10 +69,10 @@ static const int COLUMN_PERCENT_WIDTH=100; static const int COLUMN_PERCENT_HEIGHT=18; -bool RGFetchProgress::close() +gboolean RGFetchProgress::windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data) { - stopDownload(NULL, this); - + auto me = static_cast(data); + me->_cancelled = true; return TRUE; } @@ -149,6 +149,8 @@ RGFetchProgress::RGFetchProgress(RGWindow *win) g_signal_connect (expander, "notify::expanded", G_CALLBACK (expanderActivate), this); + g_signal_connect(G_OBJECT(_win), "delete-event", + G_CALLBACK(windowCloseCallback), this); } void RGFetchProgress::expanderActivate(GObject *object, @@ -190,16 +192,18 @@ void RGFetchProgress::setDescription(string mainText, string secondText) bool RGFetchProgress::MediaChange(string Media, string Drive) { - gchar *msg; + start_task([this, Media, Drive]() -> task { + gchar *msg; - msg = g_strdup_printf(_("Please insert the disk labeled:\n%s\nin drive %s"), - Media.c_str(), Drive.c_str()); + msg = g_strdup_printf(_("Please insert the disk labeled:\n%s\nin drive %s"), + Media.c_str(), Drive.c_str()); - RGUserDialog userDialog(this); - _cancelled = !userDialog.proceed(msg); + RGUserDialog userDialog(this); + _cancelled = !co_await userDialog.proceed(msg); - RGFlushInterface(); - g_free(msg); + co_await RGFlushInterface(); + g_free(msg); + }); return true; #if 0 // this code can be used when apt fixed ubuntu #2281 (patch pending) @@ -242,7 +246,9 @@ void RGFetchProgress::IMSHit(pkgAcquire::ItemDesc & Itm) //cout << "void RGFetchProgress::IMSHit(pkgAcquire::ItemDesc &Itm)" << endl; updateStatus(Itm, DLHit); - RGFlushInterface(); + start_task([]() -> task { + co_await RGFlushInterface(); + }); } @@ -250,7 +256,9 @@ void RGFetchProgress::Fetch(pkgAcquire::ItemDesc & Itm) { updateStatus(Itm, DLQueued); - RGFlushInterface(); + start_task([]() -> task { + co_await RGFlushInterface(); + }); } @@ -258,7 +266,9 @@ void RGFetchProgress::Done(pkgAcquire::ItemDesc &Itm) { updateStatus(Itm, DLDone); - RGFlushInterface(); + start_task([]() -> task { + co_await RGFlushInterface(); + }); } void RGFetchProgress::Fail(pkgAcquire::ItemDesc &Itm) @@ -268,7 +278,9 @@ void RGFetchProgress::Fail(pkgAcquire::ItemDesc &Itm) updateStatus(Itm, DLFailed); - RGFlushInterface(); + start_task([]() -> task { + co_await RGFlushInterface(); + }); } bool RGFetchProgress::Pulse(pkgAcquire *Owner) @@ -289,14 +301,6 @@ bool RGFetchProgress::Pulse(pkgAcquire *Owner) if(CurrentItems == TotalItems) percent=100.0; - // only do something if there is some real progress - if (fabsf(percent- - gtk_progress_bar_get_fraction(GTK_PROGRESS_BAR(_mainProgressBar))*100.0) < 0.1) - { - RGFlushInterface(); - return !_cancelled; - } - gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_mainProgressBar), percent / 100.0); @@ -346,8 +350,6 @@ bool RGFetchProgress::Pulse(pkgAcquire *Owner) gtk_progress_bar_set_text(GTK_PROGRESS_BAR(_mainProgressBar), s); g_free(s); - RGFlushInterface(); - return !_cancelled; } @@ -357,20 +359,24 @@ void RGFetchProgress::Start() pkgAcquireStatus::Start(); _cancelled = false; - RGFlushInterface(); + start_task([]() -> task { + co_await RGFlushInterface(); + }); } void RGFetchProgress::Stop() { //cout << "RGFetchProgress::Stop()" << endl; - RGFlushInterface(); hide(); pkgAcquireStatus::Stop(); //FIXME: this needs to be handled in a better way (gtk-2 maybe?) sleep(1); // this sucks, but if ommited, the window will not always // closed (e.g. when a package is only deleted) - RGFlushInterface(); + + start_task([]() -> task { + co_await RGFlushInterface(); + }); } void RGFetchProgress::stopDownload(GtkWidget *self, void *data) diff --git a/gtk/rgfetchprogress.h b/gtk/rgfetchprogress.h index 19d3cf1ab..1b54d603f 100644 --- a/gtk/rgfetchprogress.h +++ b/gtk/rgfetchprogress.h @@ -59,6 +59,7 @@ class RGFetchProgress : public pkgAcquireStatus, public RGGtkBuilderWindow { bool _cancelled; void updateStatus(pkgAcquire::ItemDesc & Itm, int status); + static gboolean windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data); static void stopDownload(GtkWidget *self, void *data); static void cursorChanged(GtkTreeView *treeview, @@ -81,7 +82,6 @@ class RGFetchProgress : public pkgAcquireStatus, public RGGtkBuilderWindow { virtual void Fail(pkgAcquire::ItemDesc &Itm); virtual void Start(); virtual void Stop(); - virtual bool close(); bool Pulse(pkgAcquire * Owner); diff --git a/gtk/rgfiltermanager.cc b/gtk/rgfiltermanager.cc index 1d54ca2ce..dc1e6c11a 100644 --- a/gtk/rgfiltermanager.cc +++ b/gtk/rgfiltermanager.cc @@ -883,7 +883,7 @@ void RGFilterManagerWindow::cancelAction(GtkWidget *self, void *data) me->_filterview->registerFilter(filter); } - me->close(); + me->hide(); } void RGFilterManagerWindow::okAction(GtkWidget *self, void *data) @@ -893,5 +893,5 @@ void RGFilterManagerWindow::okAction(GtkWidget *self, void *data) me->applyFilterAction(self, data); me->_filterview->storeFilters(); - me->close(); + me->hide(); } diff --git a/gtk/rggtkbuilderwindow.cc b/gtk/rggtkbuilderwindow.cc index 4ada4635f..ecfc71465 100644 --- a/gtk/rggtkbuilderwindow.cc +++ b/gtk/rggtkbuilderwindow.cc @@ -85,8 +85,7 @@ RGGtkBuilderWindow::RGGtkBuilderWindow(RGWindow *parent, string name, string mai //gtk_window_set_title(GTK_WINDOW(_win), (char *)name.c_str()); g_object_set_data(G_OBJECT(_win), "me", this); - g_signal_connect(G_OBJECT(_win), "delete-event", - G_CALLBACK(windowCloseCallback), this); + _topBox = NULL; // honor foreign parent windows (to make embedding easy) diff --git a/gtk/rginstallprogress.cc b/gtk/rginstallprogress.cc index fb46e086f..1e7984a2b 100644 --- a/gtk/rginstallprogress.cc +++ b/gtk/rginstallprogress.cc @@ -80,14 +80,8 @@ RGInstallProgressMsgs::~RGInstallProgressMsgs() void RGInstallProgressMsgs::onCloseClicked(GtkWidget *self, void *data) { - //RGInstallProgressMsgs *me = (RGInstallProgressMsgs*)data; - gtk_main_quit(); -} - -bool RGInstallProgressMsgs::close() -{ - gtk_main_quit(); - return true; + RGInstallProgressMsgs *me = (RGInstallProgressMsgs*)data; + gtk_window_close (GTK_WINDOW(me->_win)); } void RGInstallProgressMsgs::addText(const char *text, bool bold) @@ -129,20 +123,20 @@ bool RGInstallProgressMsgs::empty() return gtk_text_buffer_get_char_count(_textBuffer) == 0; } -void RGInstallProgressMsgs::run() +task RGInstallProgressMsgs::run() { show(); - gtk_main(); + co_await co_run_window(GTK_WINDOW(_win)); hide(); } -void RGInstallProgress::startUpdate() +task RGInstallProgress::startUpdate() { show(); - RGFlushInterface(); + co_await RGFlushInterface(); } -void RGInstallProgress::finishUpdate() +task RGInstallProgress::finishUpdate() { char buf[1024]; memset(buf, 0, 1024); @@ -154,7 +148,7 @@ void RGInstallProgress::finishUpdate() GTK_BUTTONS_OK, _("APT system reports:\n%s"), utf8(buf)); - gtk_dialog_run(GTK_DIALOG(dia)); + co_await co_run_dialog(GTK_DIALOG(dia)); gtk_widget_destroy(dia); } @@ -165,16 +159,16 @@ void RGInstallProgress::finishUpdate() if (_msgs.empty() == false && _config->FindB("Synaptic::IgnorePMOutput", false) == false) - _msgs.run(); + co_await _msgs.run(); - RGFlushInterface(); + co_await RGFlushInterface(); hide(); } void RGInstallProgress::prepare(RPackageLister *lister) { - for (unsigned int row = 0; row < lister->packagesSize(); row++) { + for (int row = 0; row < lister->packagesSize(); row++) { RPackage *elem = lister->getPackage(row); // Is it going to be seen? @@ -191,7 +185,7 @@ void RGInstallProgress::prepare(RPackageLister *lister) } } -void RGInstallProgress::updateInterface() +task RGInstallProgress::updateInterface() { char buf[2]; static char line[1024] = ""; @@ -249,15 +243,12 @@ void RGInstallProgress::updateInterface() } } - if (gtk_events_pending()) { - while (gtk_events_pending()) - gtk_main_iteration(); - } else { - usleep(5000); - if (_startCounting == false) { - gtk_progress_bar_pulse(GTK_PROGRESS_BAR(_pbar)); - gtk_progress_bar_pulse(GTK_PROGRESS_BAR(_pbarTotal)); - } + co_await RGFlushInterface(); + co_await sleep_ms { 5 }; + + if (_startCounting == false) { + gtk_progress_bar_pulse(GTK_PROGRESS_BAR(_pbar)); + gtk_progress_bar_pulse(GTK_PROGRESS_BAR(_pbarTotal)); } } diff --git a/gtk/rginstallprogress.h b/gtk/rginstallprogress.h index 77f7f129c..07a9f8f04 100644 --- a/gtk/rginstallprogress.h +++ b/gtk/rginstallprogress.h @@ -56,8 +56,7 @@ class RGInstallProgressMsgs:public RGGtkBuilderWindow { virtual void addLine(const char *line); virtual bool empty(); - virtual void run(); - virtual bool close(); + [[nodiscard]] virtual task run(); RGInstallProgressMsgs(RGWindow *win); ~RGInstallProgressMsgs(); @@ -85,9 +84,9 @@ class RGInstallProgress:public RInstallProgress, public RGGtkBuilderWindow { GtkCssProvider *_cssProviderBold; protected: - virtual void startUpdate(); - virtual void updateInterface(); - virtual void finishUpdate(); + [[nodiscard]] virtual task startUpdate(); + [[nodiscard]] virtual task updateInterface(); + [[nodiscard]] virtual task finishUpdate(); virtual void prepare(RPackageLister *lister); diff --git a/gtk/rglogview.cc b/gtk/rglogview.cc index 8cdcce703..8e00d8743 100644 --- a/gtk/rglogview.cc +++ b/gtk/rglogview.cc @@ -131,7 +131,7 @@ void RGLogView::readLogs() void RGLogView::cbCloseClicked(GtkWidget *self, void *data) { RGLogView *me = (RGLogView*)data; - me->close(); + me->hide(); } void RGLogView::cbTreeSelectionChanged(GtkTreeSelection *selection, diff --git a/gtk/rgmainwindow.cc b/gtk/rgmainwindow.cc index b12c99879..a660b082c 100644 --- a/gtk/rgmainwindow.cc +++ b/gtk/rgmainwindow.cc @@ -126,7 +126,7 @@ enum { DEP_NAME_COLUMN, /* text */ GtkCssProvider *RGMainWindow::_fastSearchCssProvider = NULL; -void RGMainWindow::changeView(int view, string subView) +task RGMainWindow::changeView(int view, string subView) { if(_config->FindB("Debug::Synaptic::View",false)) ioprintf(clog, "RGMainWindow::changeView(): view '%i' subView '%s'\n", @@ -155,7 +155,7 @@ void RGMainWindow::changeView(int view, string subView) GtkTreeSelection* selection; setBusyCursor(true); - setInterfaceLocked(TRUE); + co_await setInterfaceLocked(TRUE); GtkWidget *tview = GTK_WIDGET(gtk_builder_get_object(_builder, "treeview_subviews")); selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tview)); if(!subView.empty()) { @@ -179,7 +179,7 @@ void RGMainWindow::changeView(int view, string subView) } _lister->setSubView(subView); refreshTable(pkg,false); - setInterfaceLocked(FALSE); + co_await setInterfaceLocked(FALSE); setBusyCursor(false); _blockActions = FALSE; setStatusText(); @@ -289,9 +289,9 @@ string RGMainWindow::selectedSubView() return ret; } -bool RGMainWindow::showErrors() +task RGMainWindow::showErrors() { - return _userDialog->showErrors(); + co_return co_await _userDialog->showErrors(); } void RGMainWindow::notifyChange(RPackage *pkg) @@ -512,21 +512,27 @@ void RGMainWindow::cbMenuAutoInstalledClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - if (me->_blockActions) - return; - bool active = g_variant_get_boolean(parameter); + start_task([me, active]() -> task { + co_await me->autoInstalled(active); + }); +} + +task RGMainWindow::autoInstalled(bool active) +{ + if (_blockActions) + co_return; GtkTreeSelection *selection; GtkTreeIter iter; - GList *list, *li; + GList *li; RPackage *pkg; - selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(me->_treeView)); - list = li = gtk_tree_selection_get_selected_rows(selection, &me->_pkgList); + selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(_treeView)); + li = gtk_tree_selection_get_selected_rows(selection, &_pkgList); while (li != NULL) { - gtk_tree_model_get_iter(me->_pkgList, &iter, (GtkTreePath *) (li->data)); - gtk_tree_model_get(me->_pkgList, &iter, PKG_COLUMN, &pkg, -1); + gtk_tree_model_get_iter(_pkgList, &iter, (GtkTreePath *) (li->data)); + gtk_tree_model_get(_pkgList, &iter, PKG_COLUMN, &pkg, -1); if (pkg == NULL) { li = g_list_next(li); continue; @@ -538,24 +544,23 @@ void RGMainWindow::cbMenuAutoInstalledClicked(GSimpleAction *action, // write it GtkWidget *progress = GTK_WIDGET(gtk_builder_get_object - (me->_builder, "progressbar_main")); + (_builder, "progressbar_main")); GtkWidget *label = GTK_WIDGET(gtk_builder_get_object - (me->_builder, "label_status")); + (_builder, "label_status")); RGCacheProgress cacheProgress(progress, label); - me->_lister->getCache()->deps()->writeStateFile(&cacheProgress,true); + _lister->getCache()->deps()->writeStateFile(&cacheProgress,true); // refresh - me->setInterfaceLocked(TRUE); - me->_lister->unregisterObserver(me); + co_await setInterfaceLocked(TRUE); + _lister->unregisterObserver(this); - me->_lister->getCache()->deps()->MarkAndSweep(); - me->_lister->refreshView(); + _lister->getCache()->deps()->MarkAndSweep(); + _lister->refreshView(); - me->_lister->registerObserver(me); - me->refreshTable(); - me->refreshSubViewList(); - me->setInterfaceLocked(FALSE); - + _lister->registerObserver(this); + refreshTable(); + refreshSubViewList(); + co_await setInterfaceLocked(FALSE); } // install a specific version @@ -566,60 +571,62 @@ void RGMainWindow::cbInstallFromVersion(GSimpleAction *action, //cout << "RGMainWindow::cbInstallFromVersion()" << endl; RGMainWindow *me = (RGMainWindow *) data; - RPackage *pkg = me->selectedPackage(); - if(pkg == NULL) - return; + start_task([me]() -> task { + RPackage *pkg = me->selectedPackage(); + if(pkg == NULL) + co_return; + + RGGtkBuilderUserDialog dia(me,"change_version"); + + GtkWidget *label = GTK_WIDGET(gtk_builder_get_object(dia.getGtkBuilder(), + "label_text")); + gchar *str_name = g_strdup_printf(_("Select the version of %s that should be forced for installation"), pkg->name()); + gchar *str = g_strdup_printf("%s\n\n%s", str_name, + _("The package manager always selects the most applicable version available. If you force a different version from the default one, errors in the dependency handling can occur.")); + gtk_label_set_markup(GTK_LABEL(label), str); + g_free(str_name); + g_free(str); - RGGtkBuilderUserDialog dia(me,"change_version"); + GtkWidget *available_versions_combo = GTK_WIDGET(gtk_builder_get_object + (dia.getGtkBuilder(), + "combobox_available_versions")); + int canidateNr = 0; + vector > versions = pkg->getAvailableVersions(); + for(unsigned int i=0;iavailableVersion(); + if(verStr && versions[i].first == string(verStr)) + canidateNr = i; + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(available_versions_combo), + str); + //cout << "got: " << str << endl; + g_free(str); + } + gtk_combo_box_set_active(GTK_COMBO_BOX(available_versions_combo), + canidateNr); + if(!co_await dia.co_run()) { + //cout << "cancel" << endl; + co_return; // user clicked cancel + } - GtkWidget *label = GTK_WIDGET(gtk_builder_get_object(dia.getGtkBuilder(), - "label_text")); - gchar *str_name = g_strdup_printf(_("Select the version of %s that should be forced for installation"), pkg->name()); - gchar *str = g_strdup_printf("%s\n\n%s", str_name, - _("The package manager always selects the most applicable version available. If you force a different version from the default one, errors in the dependency handling can occur.")); - gtk_label_set_markup(GTK_LABEL(label), str); - g_free(str_name); - g_free(str); - - GtkWidget *available_versions_combo = GTK_WIDGET(gtk_builder_get_object - (dia.getGtkBuilder(), - "combobox_available_versions")); - int canidateNr = 0; - vector > versions = pkg->getAvailableVersions(); - for(unsigned int i=0;iavailableVersion(); - if(verStr && versions[i].first == string(verStr)) - canidateNr = i; - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(available_versions_combo), - str); - //cout << "got: " << str << endl; - g_free(str); - } - gtk_combo_box_set_active(GTK_COMBO_BOX(available_versions_combo), - canidateNr); - if(!dia.run()) { - //cout << "cancel" << endl; - return; // user clicked cancel - } + int nr = gtk_combo_box_get_active(GTK_COMBO_BOX(available_versions_combo)); - int nr = gtk_combo_box_get_active(GTK_COMBO_BOX(available_versions_combo)); + pkg->setNotify(false); + // nr-1 here as we add a "do not override" to the option menu + pkg->setVersion(versions[nr].first.c_str()); + co_await me->pkgAction(PKG_INSTALL_FROM_VERSION); - pkg->setNotify(false); - // nr-1 here as we add a "do not override" to the option menu - pkg->setVersion(versions[nr].first.c_str()); - me->pkgAction(PKG_INSTALL_FROM_VERSION); - - if (!(pkg->getFlags() & RPackage::FInstall)) - pkg->unsetVersion(); // something went wrong + if (!(pkg->getFlags() & RPackage::FInstall)) + pkg->unsetVersion(); // something went wrong - pkg->setNotify(true); + pkg->setNotify(true); + }); } -bool RGMainWindow::askStateChange(RPackageLister::pkgState state, +task RGMainWindow::askStateChange(RPackageLister::pkgState state, const vector &exclude) { vector toKeep; @@ -640,7 +647,7 @@ bool RGMainWindow::askStateChange(RPackageLister::pkgState state, RGChangesWindow changes(this); changes.confirm(_lister, toKeep, toInstall, toReInstall, toUpgrade, toRemove, toDowngrade, notAuthenticated); - int res = gtk_dialog_run(GTK_DIALOG(changes.window())); + int res = co_await co_run_dialog(GTK_DIALOG(changes.window())); if( res != GTK_RESPONSE_OK) { // canceled operation _lister->restoreState(state); @@ -651,16 +658,16 @@ bool RGMainWindow::askStateChange(RPackageLister::pkgState state, } } - return changed; + co_return changed; } -void RGMainWindow::pkgAction(RGPkgAction action) +task RGMainWindow::pkgAction(RGPkgAction action) { GtkTreeSelection *selection; GtkTreeIter iter; GList *li, *list; - setInterfaceLocked(TRUE); + co_await setInterfaceLocked(TRUE); _blockActions = TRUE; // get list of selected pkgs @@ -727,15 +734,15 @@ void RGMainWindow::pkgAction(RGPkgAction action) break; case PKG_DELETE: // delete if(flags & RPackage::FInstalled) - pkgRemoveHelper(pkg); + co_await pkgRemoveHelper(pkg); break; case PKG_PURGE: // purge if(flags & RPackage::FInstalled || flags & RPackage::FResidualConfig) - pkgRemoveHelper(pkg, true); + co_await pkgRemoveHelper(pkg, true); break; case PKG_DELETE_WITH_DEPS: if(flags & RPackage::FInstalled || flags & RPackage::FResidualConfig) - pkgRemoveHelper(pkg, true, true); + co_await pkgRemoveHelper(pkg, true, true); break; default: cout << "uh oh!!!!!!!!!" << endl; @@ -753,14 +760,14 @@ void RGMainWindow::pkgAction(RGPkgAction action) _lister->notifyCachePostChange(); - bool changed = askStateChange(state, exclude); + bool changed = co_await askStateChange(state, exclude); if (changed) { bool failed=false; // check for failed installs, if a installs fails, restore old state // as the Fixer may do wired thinks when trying to resolve the problem if (action == PKG_INSTALL) { - failed = checkForFailedInst(instPkgs); + failed = co_await checkForFailedInst(instPkgs); if(failed) _lister->restoreState(state); } @@ -777,11 +784,11 @@ void RGMainWindow::pkgAction(RGPkgAction action) refreshSubViewList(); _blockActions = FALSE; - setInterfaceLocked(FALSE); + co_await setInterfaceLocked(FALSE); refreshTable(pkg); } -bool RGMainWindow::checkForFailedInst(vector instPkgs) +task RGMainWindow::checkForFailedInst(vector instPkgs) { string failedReason; bool failed = false; @@ -805,14 +812,14 @@ bool RGMainWindow::checkForFailedInst(vector instPkgs) "textview")); GtkTextBuffer *tb = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); gtk_text_buffer_set_text(tb, utf8(failedReason.c_str()), -1); - dia.run(); + co_await dia.co_run(); // we informaed the user about the problem, we can clear the // apt error stack // CHECKME: is this discard here really needed? _error->Discard(); } - return failed; + co_return failed; } struct ActionClosure @@ -844,12 +851,15 @@ static void setActionShortcut(RGMainWindow *me, GtkAccelGroup *accel_group, cons RGMainWindow::RGMainWindow(GtkApplication *app, RPackageLister *packLister, string name) : RGGtkBuilderWindow(NULL, name), _lister(packLister), _pkgList(0), _treeView(0), _tasksWin(0), _iconLegendPanel(0), _pkgDetails(0), - _logView(0), _installProgress(0), _fetchProgress(0), + _logView(0), _fetchProgress(0), _installProgress(0), _fastSearchEventID(-1) { assert(_win); gtk_application_add_window (GTK_APPLICATION(app), GTK_WINDOW(_win)); + g_signal_connect(G_OBJECT(_win), "delete-event", + G_CALLBACK(windowCloseCallback), this); + const GActionEntry entries[] = { { "read-markings", cbOpenClicked }, { "save-markings", cbSaveClicked }, @@ -1056,7 +1066,7 @@ void RGMainWindow::buildTreeView() for (GList * li = g_list_first(columns); li != NULL; li = g_list_next(li)) { - int i = gtk_tree_view_remove_column(GTK_TREE_VIEW(_treeView), + gtk_tree_view_remove_column(GTK_TREE_VIEW(_treeView), GTK_TREE_VIEW_COLUMN(li->data)); } // need to free the list here @@ -1089,7 +1099,6 @@ void RGMainWindow::buildInterface() _config->FindI("Synaptic::windowY", 100)); if(_config->FindB("Synaptic::Maximized",false)) gtk_window_maximize(GTK_WINDOW(_win)); - RGFlushInterface(); if (_fastSearchCssProvider == NULL) { _fastSearchCssProvider = gtk_css_provider_new(); @@ -1188,7 +1197,7 @@ void RGMainWindow::buildInterface() // not restored in the same place as it was. if(!_config->FindB("Volatile::HideMainwindow", false)) show(); - RGFlushInterface(); + gtk_paned_set_position(GTK_PANED(vpaned), _config->FindI("Synaptic::vpanedPos", 140)); gtk_paned_set_position(GTK_PANED(hpaned), @@ -1364,17 +1373,17 @@ void RGMainWindow::pkgInstallHelper(RPackage *pkg, bool fixBroken, _lister->fixBroken(); } -void RGMainWindow::pkgRemoveHelper(RPackage *pkg, bool purge, bool withDeps) +task RGMainWindow::pkgRemoveHelper(RPackage *pkg, bool purge, bool withDeps) { if (pkg->getFlags() & RPackage::FImportant) { gchar* warning = g_strdup_printf(_( "Removing package \"%s\" may render the " "system unusable.\n" "Are you sure you want to do that?"), pkg->name()); - bool confirmed = _userDialog->confirm(warning, false); + bool confirmed = co_await _userDialog->confirm(warning, false); g_free(warning); if (!confirmed) { - return; + co_return; } } if (!withDeps) @@ -1440,10 +1449,10 @@ void RGMainWindow::setStatusText(char *text) } -void RGMainWindow::saveState() +task RGMainWindow::saveState() { if (_config->FindB("Volatile::NoStateSaving", false) == true) - return; + co_return; GtkWidget *vpaned = GTK_WIDGET(gtk_builder_get_object (_builder, "vpaned_main")); @@ -1470,13 +1479,13 @@ void RGMainWindow::saveState() if (!RWriteConfigFile(*_config)) { _error->Error(_("An error occurred while saving configurations.")); - _userDialog->showErrors(); + co_await _userDialog->showErrors(); } if (!_roptions->store()) cerr << "Internal Error: error storing raptoptions" << endl; } -bool RGMainWindow::restoreState() +task RGMainWindow::restoreState() { // see if we have broken packages (might be better in some @@ -1491,13 +1500,13 @@ bool RGMainWindow::restoreState() "You have %i broken packages on your system!\n\n" "Use the \"Broken\" filter to locate them.", broken); msg = g_strdup_printf(msg, broken); - _userDialog->warning(msg); + co_await _userDialog->warning(msg); g_free(msg); } if(!_config->FindB("Volatile::Upgrade-Mode",false)) { int viewNr = _config->FindI("Synaptic::ViewMode", 0); - changeView(viewNr); + co_await changeView(viewNr); // we auto set to "All" on startup when we have gtk2.4 (without // the list is too slow) @@ -1511,33 +1520,51 @@ bool RGMainWindow::restoreState() gtk_tree_selection_select_iter(selection, &iter); } updatePackageInfo(NULL); + co_return true; +} + + +gboolean RGMainWindow::windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data) +{ + RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + co_await me->close(); + }); return true; } -bool RGMainWindow::close() +void RGMainWindow::closeWin(GSimpleAction *action, GVariant *parameter, gpointer data) +{ + auto me = (RGMainWindow *) data; + start_task([me]() -> task { + co_await me->close(); + }); +} + + +task RGMainWindow::close() { if (_interfaceLocked > 0) - return true; + co_return; RGGtkBuilderUserDialog dia(this); - if (_unsavedChanges == false || dia.run("quit")) { + if (_unsavedChanges == false || co_await dia.co_run("quit")) { _error->Discard(); - saveState(); - showErrors(); + co_await saveState(); + co_await showErrors(); exit(0); } - return true; } -void RGMainWindow::setInterfaceLocked(bool flag) +task RGMainWindow::setInterfaceLocked(bool flag) { if (flag) { _interfaceLocked++; if (_interfaceLocked > 1) - return; + co_return; gtk_widget_set_sensitive(_win, FALSE); if(gtk_widget_get_visible(_win)) @@ -1547,7 +1574,7 @@ void RGMainWindow::setInterfaceLocked(bool flag) _interfaceLocked--; if (_interfaceLocked > 0) - return; + co_return; gtk_widget_set_sensitive(_win, TRUE); if(gtk_widget_get_visible(_win)) @@ -1555,8 +1582,7 @@ void RGMainWindow::setInterfaceLocked(bool flag) } // fast enough with the new fixed-height mode - while (gtk_events_pending()) - gtk_main_iteration(); + co_await RGFlushInterface(); } void RGMainWindow::setTreeLocked(bool flag) @@ -1583,7 +1609,9 @@ void RGMainWindow::cbPkgAction(RGPkgAction action) if (gtk_widget_has_focus (entry) && action == PKG_DELETE) { return; } - pkgAction(action); + start_task([this, action]() -> task { + co_await pkgAction(action); + }); } void RGMainWindow::cbPkgActionUnmark(GSimpleAction *action, @@ -1716,13 +1744,14 @@ void RGMainWindow::cbChangelogDialog(GSimpleAction *action, { RGMainWindow *me = (RGMainWindow*)data; - RPackage *pkg = me->selectedPackage(); - if(pkg == NULL) - return; - - me->setInterfaceLocked(TRUE); - ShowChangelogDialog(me, pkg); - me->setInterfaceLocked(FALSE); + start_task([me]() -> task { + RPackage *pkg = me->selectedPackage(); + if (pkg != NULL) { + co_await me->setInterfaceLocked(TRUE); + co_await ShowChangelogDialog(me, pkg); + co_await me->setInterfaceLocked(FALSE); + } + }); } void RGMainWindow::cbPackageListRowActivated(GtkTreeView *treeview, @@ -1731,32 +1760,39 @@ void RGMainWindow::cbPackageListRowActivated(GtkTreeView *treeview, gpointer data) { //cout << "RGMainWindow::cbPackageListRowActivated()" << endl; - RGMainWindow *me = (RGMainWindow *) data; + start_task([me, treeview, path] -> task { + co_await me->packageListRowActivated(treeview, path); + }); +} + +task RGMainWindow::packageListRowActivated(GtkTreeView *treeview, + GtkTreePath *path) +{ GtkTreeIter iter; RPackage *pkg = NULL; - if (!gtk_tree_model_get_iter(me->_pkgList, &iter, path)) - return; + if (!gtk_tree_model_get_iter(_pkgList, &iter, path)) + co_return; - gtk_tree_model_get(me->_pkgList, &iter, PKG_COLUMN, &pkg, -1); + gtk_tree_model_get(_pkgList, &iter, PKG_COLUMN, &pkg, -1); assert(pkg); int flags = pkg->getFlags(); if(flags & RPackage::FPinned) - return; + co_return; if (!(flags & RPackage::FInstalled)) { if (flags & RPackage::FKeep) - me->pkgAction(PKG_INSTALL); + co_await pkgAction(PKG_INSTALL); else if (flags & RPackage::FInstall) - me->pkgAction(PKG_KEEP); + co_await pkgAction(PKG_KEEP); } else if (flags & RPackage::FOutdated) { if (flags & RPackage::FKeep) - me->pkgAction(PKG_INSTALL); + co_await pkgAction(PKG_INSTALL); else if (flags & RPackage::FUpgrade) - me->pkgAction(PKG_KEEP); + co_await pkgAction(PKG_KEEP); } // make sure we do not lose the keyboard focus (this happens in @@ -1766,12 +1802,12 @@ void RGMainWindow::cbPackageListRowActivated(GtkTreeView *treeview, GtkTreePath *start = gtk_tree_path_new(); bool ok = gtk_tree_view_get_visible_range(GTK_TREE_VIEW(treeview), &start, NULL); - if (ok && gtk_tree_model_get_iter_first(me->_pkgList, &iter)) { + if (ok && gtk_tree_model_get_iter_first(_pkgList, &iter)) { gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(treeview), start, NULL, true, 0.0, 0.0); } gtk_tree_path_free(start); - me->setStatusText(); + setStatusText(); } void RGMainWindow::cbAddCDROM(GSimpleAction *action, @@ -1779,31 +1815,33 @@ void RGMainWindow::cbAddCDROM(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - RGCDScanner scan(me, me->_userDialog); - me->setInterfaceLocked(TRUE); - bool updateCache = false; - bool dontStop = true; - while (dontStop) { - if (scan.run() == false) - me->showErrors(); - else - updateCache = true; - if(_config->FindB("APT::CDROM::NoMount", false)) - dontStop=false; - else - dontStop = me->_userDialog->confirm(_("Do you want to add another CD-ROM?")); - } - scan.hide(); - if (updateCache) { - me->setTreeLocked(TRUE); - if (!me->_lister->openCache()) { - me->showErrors(); - exit(1); + start_task([me]() -> task { + RGCDScanner scan(me, me->_userDialog); + co_await me->setInterfaceLocked(TRUE); + bool updateCache = false; + bool dontStop = true; + while (dontStop) { + if (co_await scan.run() == false) + co_await me->showErrors(); + else + updateCache = true; + if(_config->FindB("APT::CDROM::NoMount", false)) + dontStop=false; + else + dontStop = co_await me->_userDialog->confirm(_("Do you want to add another CD-ROM?")); } - me->setTreeLocked(FALSE); - me->refreshTable(me->selectedPackage()); - } - me->setInterfaceLocked(FALSE); + scan.hide(); + if (updateCache) { + me->setTreeLocked(TRUE); + if (!me->_lister->openCache()) { + co_await me->showErrors(); + exit(1); + } + me->setTreeLocked(FALSE); + me->refreshTable(me->selectedPackage()); + } + co_await me->setInterfaceLocked(FALSE); + }); } void RGMainWindow::cbTasksClicked(GSimpleAction *action, @@ -1828,45 +1866,51 @@ void RGMainWindow::cbOpenClicked(GSimpleAction *action, { //std::cout << "RGMainWindow::openClicked()" << endl; RGMainWindow *me = (RGMainWindow*)data; + start_task([me]() -> task { + GtkWidget *filesel; + filesel = gtk_file_chooser_dialog_new(_("Open changes"), + GTK_WINDOW(me->window()), + GTK_FILE_CHOOSER_ACTION_OPEN, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Open"), GTK_RESPONSE_ACCEPT, + NULL); + if (co_await co_run_dialog(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) { + co_await me->setInterfaceLocked(TRUE); + gtk_widget_hide(filesel); + co_await RGFlushInterface(); + + RPackageLister::pkgState state; + me->_lister->saveState(state); + + GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(filesel)); + char *filename = g_file_get_path(file); + g_object_unref (file); + + me->selectionsFilename = filename; + ifstream in(filename); + if (!in != 0) { + _error->Error(_("Can't read %s"), filename); + co_await me->_userDialog->showErrors(); + g_free (filename); + co_return; + } - GtkWidget *filesel; - filesel = gtk_file_chooser_dialog_new(_("Open changes"), - GTK_WINDOW(me->window()), - GTK_FILE_CHOOSER_ACTION_OPEN, - _("_Cancel"), GTK_RESPONSE_CANCEL, - _("_Open"), GTK_RESPONSE_ACCEPT, - NULL); - if(gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) { - me->setInterfaceLocked(TRUE); - gtk_widget_hide(filesel); - RGFlushInterface(); - - RPackageLister::pkgState state; - me->_lister->saveState(state); + g_free (filename); - const char *file; - file = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel)); - me->selectionsFilename = file; + me->_lister->unregisterObserver(me); + // read the selections from the file + me->_lister->readSelections(in); + co_await me->askStateChange(state); - ifstream in(file); - if (!in != 0) { - _error->Error(_("Can't read %s"), file); - me->_userDialog->showErrors(); - return; + // refresh to ensure that broken dependencies are displayed + me->_lister->registerObserver(me); + me->refreshTable(); + me->refreshSubViewList(); + me->setStatusText(); + co_await me->setInterfaceLocked(FALSE); } - me->_lister->unregisterObserver(me); - // read the selections from the file - me->_lister->readSelections(in); - me->askStateChange(state); - - // refresh to ensure that broken dependencies are displayed - me->_lister->registerObserver(me); - me->refreshTable(); - me->refreshSubViewList(); - me->setStatusText(); - me->setInterfaceLocked(FALSE); - } - gtk_widget_destroy(filesel); + gtk_widget_destroy(filesel); + }); } void RGMainWindow::cbSaveClicked(GSimpleAction *action, @@ -1875,24 +1919,29 @@ void RGMainWindow::cbSaveClicked(GSimpleAction *action, { //std::cout << "RGMainWindow::saveClicked()" << endl; RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + co_await me->saveClicked(); + }); +} - if (me->selectionsFilename == "") { - me->cbSaveAsClicked(nullptr, nullptr, data); - return; +task RGMainWindow::saveClicked() +{ + if (selectionsFilename == "") { + co_await saveAsClicked(); + co_return; } - ofstream out(me->selectionsFilename.c_str()); + ofstream out(selectionsFilename.c_str()); if (!out != 0) { - _error->Error(_("Can't write %s"), me->selectionsFilename.c_str()); - me->_userDialog->showErrors(); - return; + _error->Error(_("Can't write %s"), selectionsFilename.c_str()); + co_await _userDialog->showErrors(); + co_return; } - me->_lister->unregisterObserver(me); - me->_lister->writeSelections(out, me->saveFullState); - me->_lister->registerObserver(me); - me->setStatusText(); - + _lister->unregisterObserver(this); + _lister->writeSelections(out, saveFullState); + _lister->registerObserver(this); + setStatusText(); } @@ -1902,26 +1951,31 @@ void RGMainWindow::cbSaveAsClicked(GSimpleAction *action, { //std::cout << "RGMainWindow::saveAsClicked()" << endl; RGMainWindow *me = (RGMainWindow*)data; + start_task([me]() -> task { + co_await me->saveAsClicked(); + }); +} +task RGMainWindow::saveAsClicked() +{ GtkWidget *filesel; - filesel = gtk_file_chooser_dialog_new(_("Save changes"), - GTK_WINDOW(me->window()), - GTK_FILE_CHOOSER_ACTION_SAVE, - _("_Cancel"), GTK_RESPONSE_CANCEL, - _("_Save"), GTK_RESPONSE_ACCEPT, - NULL); - GtkWidget *checkButton = - gtk_check_button_new_with_label(_("Save full state, not only changes")); - gtk_file_chooser_set_extra_widget(GTK_FILE_CHOOSER(filesel), checkButton); - - if(gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) { - const char *file; - file = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel)); - me->selectionsFilename = file; - me->saveFullState = - gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(checkButton)); + filesel = gtk_file_chooser_dialog_new(_("Save changes"), + GTK_WINDOW(window()), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, + NULL); + gtk_file_chooser_add_choice(GTK_FILE_CHOOSER(filesel), "full", _("Save full state, not only changes"), NULL, NULL); + + if (co_await co_run_dialog(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) { + GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(filesel)); + char *filename = g_file_get_path (file); + selectionsFilename = filename; + g_free (filename); + g_object_unref (file); + saveFullState = g_strcmp0("true", gtk_file_chooser_get_choice(GTK_FILE_CHOOSER(filesel), "full")) == 0; // now call save for the actual saving - me->cbSaveClicked(nullptr, nullptr, me); + co_await saveClicked(); } gtk_widget_destroy(filesel); } @@ -1989,80 +2043,78 @@ void RGMainWindow::cbShowSourcesWindow(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - - // FIXME: make this all go into the repository window - bool Changed = false; - bool ForceReload = _config->FindB("Synaptic::UpdateAfterSrcChange",false); - - if(!g_file_test("/usr/bin/software-properties-gtk", - G_FILE_TEST_IS_EXECUTABLE) - || _config->FindB("Synaptic::dontUseGnomeSoftwareProperties", false)) - { - RGRepositoryEditor w(me); - Changed = w.Run(); - } else { - // use gnome-software-properties window - me->setInterfaceLocked(TRUE); - GPid pid; - int status; - const char *argv[5]; - argv[0] = "/usr/bin/software-properties-gtk"; - argv[1] = "-n"; - argv[2] = "-t"; - argv[3] = g_strdup_printf("%lu", GDK_WINDOW_XID(gtk_widget_get_window(me->_win))); - argv[4] = NULL; - g_spawn_async(NULL, const_cast(argv), NULL, - (GSpawnFlags)G_SPAWN_DO_NOT_REAP_CHILD, - NULL, NULL, &pid, NULL); - // kill the child if the window is deleted - while(waitpid(pid, &status, WNOHANG) == 0) { - usleep(50000); - RGFlushInterface(); - } - Changed = WEXITSTATUS(status); - me->setInterfaceLocked(FALSE); - } - - RGFlushInterface(); - - // auto update after repostitory change - if (Changed == true && ForceReload) { - me->cbUpdateClicked(nullptr, nullptr, data); - } else if(Changed == true && - _config->FindB("Synaptic::AskForUpdateAfterSrcChange",true)) { - // ask for update after repo change - GtkWidget *cb, *dialog; - dialog = gtk_message_dialog_new (GTK_WINDOW(me->window()), - GTK_DIALOG_DESTROY_WITH_PARENT, - GTK_MESSAGE_INFO, - GTK_BUTTONS_NONE, - _("Repositories changed")); - // TRANSLATORS: this message appears when the user added/removed - // a repository (sources.list entry) a reload (apt-get update) is - // needed then - gchar *msgstr = _("The repository information " - "has changed. " - "You have to click on the " - "\"Reload\" button for your changes to " - "take effect"); - gtk_message_dialog_set_markup(GTK_MESSAGE_DIALOG(dialog), msgstr); - gtk_dialog_add_buttons(GTK_DIALOG(dialog), _("_Cancel"), GTK_RESPONSE_REJECT, _("_Reload"), GTK_RESPONSE_ACCEPT, NULL); - GtkWidget* reload_button = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); - GtkWidget* refresh_image = gtk_image_new_from_icon_name("view-refresh", GTK_ICON_SIZE_BUTTON); - gtk_button_set_image(GTK_BUTTON(reload_button), refresh_image); - cb = gtk_check_button_new_with_label(_("Never show this message again")); - gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), cb, true, true, 0); - gtk_widget_show(cb); - gint response = gtk_dialog_run (GTK_DIALOG (dialog)); - gtk_widget_hide(dialog); - if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cb))) { - _config->Set("Synaptic::AskForUpdateAfterSrcChange", false); + start_task([me]() -> task { + // FIXME: make this all go into the repository window + bool Changed = false; + bool ForceReload = _config->FindB("Synaptic::UpdateAfterSrcChange",false); + + if(!g_file_test("/usr/bin/software-properties-gtk", + G_FILE_TEST_IS_EXECUTABLE) + || _config->FindB("Synaptic::dontUseGnomeSoftwareProperties", false)) + { + RGRepositoryEditor w(me); + Changed = co_await w.Run(); + } else { + // use gnome-software-properties window + co_await me->setInterfaceLocked(TRUE); + GPid pid; + int status; + const char *argv[5]; + argv[0] = "/usr/bin/software-properties-gtk"; + argv[1] = "-n"; + argv[4] = NULL; + g_spawn_async(NULL, const_cast(argv), NULL, + (GSpawnFlags)G_SPAWN_DO_NOT_REAP_CHILD, + NULL, NULL, &pid, NULL); + // kill the child if the window is deleted + while(waitpid(pid, &status, WNOHANG) == 0) { + usleep(50000); + co_await RGFlushInterface(); + } + Changed = WEXITSTATUS(status); + co_await me->setInterfaceLocked(FALSE); } - if (response == GTK_RESPONSE_ACCEPT) { - me->cbUpdateClicked(nullptr, nullptr, data); + + co_await RGFlushInterface(); + + // auto update after repostitory change + if (Changed == true && ForceReload) { + me->cbUpdateClicked(nullptr, nullptr, me); + } else if(Changed == true && + _config->FindB("Synaptic::AskForUpdateAfterSrcChange",true)) { + // ask for update after repo change + GtkWidget *cb, *dialog; + dialog = gtk_message_dialog_new (GTK_WINDOW(me->window()), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_NONE, + _("Repositories changed")); + // TRANSLATORS: this message appears when the user added/removed + // a repository (sources.list entry) a reload (apt-get update) is + // needed then + gchar *msgstr = _("The repository information " + "has changed. " + "You have to click on the " + "\"Reload\" button for your changes to " + "take effect"); + gtk_message_dialog_set_markup(GTK_MESSAGE_DIALOG(dialog), msgstr); + gtk_dialog_add_buttons(GTK_DIALOG(dialog), _("_Cancel"), GTK_RESPONSE_REJECT, _("_Reload"), GTK_RESPONSE_ACCEPT, NULL); + GtkWidget* reload_button = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); + GtkWidget* refresh_image = gtk_image_new_from_icon_name("view-refresh", GTK_ICON_SIZE_BUTTON); + gtk_button_set_image(GTK_BUTTON(reload_button), refresh_image); + cb = gtk_check_button_new_with_label(_("Never show this message again")); + gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), cb, true, true, 0); + gint response = co_await co_run_dialog (GTK_DIALOG (dialog)); + gtk_widget_hide(dialog); + if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cb))) { + _config->Set("Synaptic::AskForUpdateAfterSrcChange", false); + } + if (response == GTK_RESPONSE_ACCEPT) { + me->cbUpdateClicked(nullptr, nullptr, me); + } + gtk_widget_destroy(dialog); } - gtk_widget_destroy (dialog); - } + }); } static void traverseToolbarButtons(GtkWidget *toolbar, @@ -2159,45 +2211,45 @@ void RGMainWindow::cbFindToolClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + if (me->_findWin == NULL) { + me->_findWin = new RGFindWindow(me); + } - if (me->_findWin == NULL) { - me->_findWin = new RGFindWindow(me); - } - - me->_findWin->selectText(); - int res = gtk_dialog_run(GTK_DIALOG(me->_findWin->window())); - if (res == GTK_RESPONSE_OK) { - - // clear the quick search, otherwise both apply and that is - // confusing - gtk_entry_set_text(GTK_ENTRY(me->_entry_fast_search), ""); - - string str = me->_findWin->getFindString(); - me->setBusyCursor(true); - - // we need to convert here as the DDTP project does not use utf-8 - const char *locale_str = utf8_to_locale(str.c_str()); - if(locale_str == NULL) // invalid utf-8 - locale_str = str.c_str(); - - int type = me->_findWin->getSearchType(); - GtkWidget *progress = GTK_WIDGET(gtk_builder_get_object - (me->_builder, "progressbar_main")); - GtkWidget *label = GTK_WIDGET(gtk_builder_get_object - (me->_builder, "label_status")); - RGCacheProgress searchProgress(progress, label); - int found = me->_lister->searchView()->setSearch(str,type, - locale_str, - searchProgress); - me->changeView(PACKAGE_VIEW_SEARCH, str); - - me->setBusyCursor(false); - gchar *statusstr = g_strdup_printf(_("Found %i packages"), found); - me->setStatusText(statusstr); - me->updatePackageInfo(NULL); - g_free(statusstr); - } - + me->_findWin->selectText(); + int res = co_await co_run_dialog(GTK_DIALOG(me->_findWin->window())); + if (res == GTK_RESPONSE_OK) { + + // clear the quick search, otherwise both apply and that is + // confusing + gtk_entry_set_text(GTK_ENTRY(me->_entry_fast_search), ""); + + string str = me->_findWin->getFindString(); + me->setBusyCursor(true); + + // we need to convert here as the DDTP project does not use utf-8 + const char *locale_str = utf8_to_locale(str.c_str()); + if(locale_str == NULL) // invalid utf-8 + locale_str = str.c_str(); + + int type = me->_findWin->getSearchType(); + GtkWidget *progress = GTK_WIDGET(gtk_builder_get_object + (me->_builder, "progressbar_main")); + GtkWidget *label = GTK_WIDGET(gtk_builder_get_object + (me->_builder, "label_status")); + RGCacheProgress searchProgress(progress, label); + int found = me->_lister->searchView()->setSearch(str,type, + locale_str, + searchProgress); + co_await me->changeView(PACKAGE_VIEW_SEARCH, str); + + me->setBusyCursor(false); + gchar *statusstr = g_strdup_printf(_("Found %i packages"), found); + me->setStatusText(statusstr); + me->updatePackageInfo(NULL); + g_free(statusstr); + } + }); } void RGMainWindow::cbShowAboutPanel(GSimpleAction *action, @@ -2264,8 +2316,14 @@ void RGMainWindow::cbHelpAction(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + co_await me->helpAction(); + }); +} - me->setStatusText(_("Starting help viewer...")); +task RGMainWindow::helpAction() +{ + setStatusText(_("Starting help viewer...")); // FIXME: move this into rgutils as well (or rgspawn.cc) vector cmd; @@ -2278,7 +2336,7 @@ void RGMainWindow::cbHelpAction(GSimpleAction *action, } if (cmd.empty()) { - me->_userDialog->error(_("No help viewer is installed!\n\n" + co_await _userDialog->error(_("No help viewer is installed!\n\n" "You need either the GNOME help viewer 'yelp', " "or any browser setup to use xdg-open " "to view the synaptic manual.\n\n" @@ -2286,7 +2344,7 @@ void RGMainWindow::cbHelpAction(GSimpleAction *action, "with 'man synaptic' from the " "command line or view the html version located " "in the 'synaptic/html' folder.")); - return; + co_return; } RunAsSudoUserCommand(cmd); } @@ -2294,16 +2352,17 @@ void RGMainWindow::cbHelpAction(GSimpleAction *action, void RGMainWindow::cbCloseFilterManagerAction(void *self, bool okcancel) { RGMainWindow *me = (RGMainWindow *) self; + start_task([me]() -> task { + // FIXME: only do all this if the user didn't click "cancel" in the dialog - // FIXME: only do all this if the user didn't click "cancel" in the dialog - - me->setInterfaceLocked(TRUE); + co_await me->setInterfaceLocked(TRUE); - me->_lister->filterView()->refreshFilters(); - me->refreshTable(); - me->refreshSubViewList(); + me->_lister->filterView()->refreshFilters(); + me->refreshTable(); + me->refreshSubViewList(); - me->setInterfaceLocked(FALSE); + co_await me->setInterfaceLocked(FALSE); + }); } @@ -2313,23 +2372,23 @@ void RGMainWindow::cbShowFilterManagerWindow(GSimpleAction *action, { RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + if (me->_fmanagerWin == NULL) { + me->_fmanagerWin = new RGFilterManagerWindow(me, me->_lister->filterView()); + } - if (me->_fmanagerWin == NULL) { - me->_fmanagerWin = new RGFilterManagerWindow(me, me->_lister->filterView()); - } - - me->_fmanagerWin->readFilters(); - int res = gtk_dialog_run(GTK_DIALOG(me->_fmanagerWin->window())); - if(res == GTK_RESPONSE_OK) { - me->setInterfaceLocked(TRUE); + me->_fmanagerWin->readFilters(); + int res = co_await co_run_dialog(GTK_DIALOG(me->_fmanagerWin->window())); + if(res == GTK_RESPONSE_OK) { + co_await me->setInterfaceLocked(TRUE); - me->_lister->filterView()->refreshFilters(); - me->refreshTable(); - me->refreshSubViewList(); + me->_lister->filterView()->refreshFilters(); + me->refreshTable(); + me->refreshSubViewList(); - me->setInterfaceLocked(FALSE); - } - + co_await me->setInterfaceLocked(FALSE); + } + }); } void RGMainWindow::cbSelectedRow(GtkTreeSelection *selection, gpointer data) @@ -2375,22 +2434,24 @@ void RGMainWindow::cbClearAllChangesClicked(GSimpleAction *action, { //cout << "clearAllChangesClicked" << endl; RGMainWindow *me = (RGMainWindow *) data; - me->setInterfaceLocked(TRUE); - me->_lister->unregisterObserver(me); - me->setTreeLocked(TRUE); - - // reset - if (!me->_lister->openCache()) { - me->showErrors(); - exit(1); - } + start_task([me]() -> task { + co_await me->setInterfaceLocked(TRUE); + me->_lister->unregisterObserver(me); + me->setTreeLocked(TRUE); - me->_lister->registerObserver(me); - me->setTreeLocked(FALSE); - me->refreshTable(); - me->refreshSubViewList(); - me->setInterfaceLocked(FALSE); - me->setStatusText(); + // reset + if (!me->_lister->openCache()) { + co_await me->showErrors(); + exit(1); + } + + me->_lister->registerObserver(me); + me->setTreeLocked(FALSE); + me->refreshTable(); + me->refreshSubViewList(); + co_await me->setInterfaceLocked(FALSE); + me->setStatusText(); + }); } @@ -2400,16 +2461,18 @@ void RGMainWindow::cbUndoClicked(GSimpleAction *action, { //cout << "undoClicked" << endl; RGMainWindow *me = (RGMainWindow *) data; - me->setInterfaceLocked(TRUE); + start_task([me]() -> task { + co_await me->setInterfaceLocked(TRUE); - me->_lister->unregisterObserver(me); + me->_lister->unregisterObserver(me); - // undo - me->_lister->undo(); + // undo + me->_lister->undo(); - me->_lister->registerObserver(me); - me->refreshTable(); - me->setInterfaceLocked(FALSE); + me->_lister->registerObserver(me); + me->refreshTable(); + co_await me->setInterfaceLocked(FALSE); + }); } void RGMainWindow::cbRedoClicked(GSimpleAction *action, @@ -2418,16 +2481,18 @@ void RGMainWindow::cbRedoClicked(GSimpleAction *action, { //cout << "redoClicked" << endl; RGMainWindow *me = (RGMainWindow *) data; - me->setInterfaceLocked(TRUE); + start_task([me]() -> task { + co_await me->setInterfaceLocked(TRUE); - me->_lister->unregisterObserver(me); + me->_lister->unregisterObserver(me); - // redo - me->_lister->redo(); + // redo + me->_lister->redo(); - me->_lister->registerObserver(me); - me->refreshTable(); - me->setInterfaceLocked(FALSE); + me->_lister->registerObserver(me); + me->refreshTable(); + co_await me->setInterfaceLocked(FALSE); + }); } void RGMainWindow::cbPkgReconfigureClicked(GSimpleAction *action, @@ -2436,23 +2501,29 @@ void RGMainWindow::cbPkgReconfigureClicked(GSimpleAction *action, { RGMainWindow *me = (RGMainWindow *) data; //cout << "RGMainWindow::pkgReconfigureClicked()" << endl; + start_task([me]() -> task { + co_await me->pkgReconfigureClicked(); + }); +} - if(me->selectedPackage() == NULL) - return; +task RGMainWindow::pkgReconfigureClicked() +{ + if(selectedPackage() == NULL) + co_return; RPackage *pkg = NULL; - pkg = me->_lister->getPackage("libgnome2-perl"); + pkg = _lister->getPackage("libgnome2-perl"); if (pkg && pkg->installedVersion() == NULL) { - me->_userDialog->error(_("Cannot start configuration tool!\n" + co_await _userDialog->error(_("Cannot start configuration tool!\n" "You have to install the required package " "'libgnome2-perl'.")); - return; + co_return; } - me->setStatusText(_("Starting package configuration tool...")); + setStatusText(_("Starting package configuration tool...")); const gchar *cmd[] = { "/usr/sbin/dpkg-reconfigure", "-fgnome", - me->selectedPackage()->name(), + selectedPackage()->name(), NULL }; GError *error = NULL; g_spawn_async("/", const_cast(cmd), NULL, (GSpawnFlags)0, NULL, NULL, NULL, &error); @@ -2467,12 +2538,18 @@ void RGMainWindow::cbPkgHelpClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + co_await me->pkgHelpClicked(); + }); +} - if(me->selectedPackage() == NULL) - return; +task RGMainWindow::pkgHelpClicked() +{ + if(selectedPackage() == NULL) + co_return; //cout << "RGMainWindow::pkgHelpClicked()" << endl; - me->setStatusText(_("Starting package documentation viewer...")); + setStatusText(_("Starting package documentation viewer...")); // mozilla eats bookmarks when run under sudo (because it does not // change $HOME) so we better play safe here @@ -2484,11 +2561,11 @@ void RGMainWindow::cbPkgHelpClicked(GSimpleAction *action, if (is_binary_in_path("dwww")) { const gchar *cmd[5]; cmd[0] = "dwww"; - cmd[1] = me->selectedPackage()->name(); + cmd[1] = selectedPackage()->name(); cmd[2] = NULL; g_spawn_async("/tmp", const_cast(cmd), NULL, (GSpawnFlags)0, NULL, NULL, NULL, NULL); } else { - me->_userDialog->error(_("You have to install the package \"dwww\" " + co_await _userDialog->error(_("You have to install the package \"dwww\" " "to browse the documentation of a package")); } } @@ -2497,14 +2574,15 @@ void RGMainWindow::cbPkgHelpClicked(GSimpleAction *action, void RGMainWindow::cbChangedView(GtkWidget *self, void *data) { RGMainWindow *me = (RGMainWindow *) data; + start_task([self, me]() -> task { + // only act on the active buttons + if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(self)) || me->_blockActions == TRUE) { + co_return; + } - // only act on the active buttons - if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(self)) || - me->_blockActions == TRUE) - return; - - long view = (long)g_object_get_data(G_OBJECT(self), "index"); - me->changeView(view); + long view = (long)g_object_get_data(G_OBJECT(self), "index"); + co_await me->changeView(view); + }); } void RGMainWindow::cbChangedSubView(GtkTreeSelection *selection, @@ -2553,150 +2631,152 @@ void RGMainWindow::cbProceedClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - RGSummaryWindow *summ; - - // nothing to do - int listed, installed, broken; - int toInstall, toRemove; - double size; - me->_lister->getStats(installed, broken, toInstall, toRemove, size); - if((toInstall + toRemove) == 0) - return; - - // check whether we can really do it - if (!me->_lister->check()) { - me->_userDialog->error(_("Could not apply changes!\n" - "Fix broken packages first.")); - return; - } + start_task([me]() -> task { + RGSummaryWindow *summ; + + // nothing to do + int listed, installed, broken; + int toInstall, toRemove; + double size; + me->_lister->getStats(installed, broken, toInstall, toRemove, size); + if((toInstall + toRemove) == 0) + co_return; + + // check whether we can really do it + if (!me->_lister->check()) { + co_await me->_userDialog->error(_("Could not apply changes!\n" + "Fix broken packages first.")); + co_return; + } - int a,b,c,d,e,f,g,h,unAuthenticated; - double s; - me->_lister->getSummary(a,b,c,d,e,f,g,h,unAuthenticated,s); - if(unAuthenticated || - _config->FindB("Volatile::Non-Interactive", false) == false) { - // show a summary of what's gonna happen - RGSummaryWindow summ(me, me->_lister); - if (!summ.showAndConfirm()) { - // canceled operation - return; + int a,b,c,d,e,f,g,h,unAuthenticated; + double s; + me->_lister->getSummary(a,b,c,d,e,f,g,h,unAuthenticated,s); + if(unAuthenticated || + _config->FindB("Volatile::Non-Interactive", false) == false) { + // show a summary of what's gonna happen + RGSummaryWindow summ(me, me->_lister); + if (!co_await summ.showAndConfirm()) { + // canceled operation + co_return; + } } - } - me->setInterfaceLocked(TRUE); - me->updatePackageInfo(NULL); + co_await me->setInterfaceLocked(TRUE); + me->updatePackageInfo(NULL); - me->setStatusText(_("Applying marked changes. This may take a while...")); + me->setStatusText(_("Applying marked changes. This may take a while...")); - // fetch packages - RGFetchProgress *fprogress=me->_fetchProgress = new RGFetchProgress(me); - fprogress->setDescription(_("Downloading Package Files"), ""); -// _("The package files will be cached locally for installation.")); + // fetch packages + RGFetchProgress *fprogress=me->_fetchProgress = new RGFetchProgress(me); + fprogress->setDescription(_("Downloading Package Files"), ""); + // _("The package files will be cached locally for installation.")); - // Do not let the treeview access the cache during the update. - me->setTreeLocked(TRUE); + // Do not let the treeview access the cache during the update. + me->setTreeLocked(TRUE); - // save selections to temporary file - const gchar *file = - g_strdup_printf("%s/selections.proceed", RConfDir().c_str()); - ofstream out(file); - if (!out != 0) { - _error->Error(_("Can't write %s"), file); - me->_userDialog->showErrors(); - return; - } - me->_lister->writeSelections(out, false); + // save selections to temporary file + const gchar *file = + g_strdup_printf("%s/selections.proceed", RConfDir().c_str()); + ofstream out(file); + if (!out != 0) { + _error->Error(_("Can't write %s"), file); + co_await me->_userDialog->showErrors(); + co_return; + } + me->_lister->writeSelections(out, false); - RInstallProgress *iprogress; -#ifdef HAVE_TERMINAL -#ifdef HAVE_RPM - bool UseTerminal = false; -#else - // no RPM - #ifdef WITH_DPKG_STATUSFD - bool UseTerminal = false; + RInstallProgress *iprogress; + #ifdef HAVE_TERMINAL + #ifdef HAVE_RPM + bool UseTerminal = false; #else - bool UseTerminal = true; - #endif // DPKG -#endif // HAVE_RPM - RGTermInstallProgress *term = NULL; - if (_config->FindB("Synaptic::UseTerminal", UseTerminal) == true) - iprogress = term = new RGTermInstallProgress(me); - else -#endif // HAVE_TERMINAL + // no RPM + #ifdef WITH_DPKG_STATUSFD + bool UseTerminal = false; + #else + bool UseTerminal = true; + #endif // DPKG + #endif // HAVE_RPM + RGTermInstallProgress *term = NULL; + if (_config->FindB("Synaptic::UseTerminal", UseTerminal) == true) + iprogress = term = new RGTermInstallProgress(me); + else + #endif // HAVE_TERMINAL -#ifdef HAVE_RPM - iprogress = new RGInstallProgress(me, me->_lister); -#else - #ifdef WITH_DPKG_STATUSFD - iprogress = new RGDebInstallProgress(me,me->_lister); - #else - iprogress = new RGDummyInstallProgress(); - #endif // WITH_DPKG_STATUSFD -#endif // HAVE_RPM - me->_installProgress = dynamic_cast(iprogress); - - //bool result = me->_lister->commitChanges(fprogress, iprogress); - me->_lister->commitChanges(fprogress, iprogress); - - // FIXME: move this into the terminal class -#ifdef HAVE_TERMINAL - // wait until the term dialog is closed - if (term != NULL) { - while (gtk_widget_get_visible(GTK_WIDGET(term->window()))) { - RGFlushInterface(); - usleep(100000); + #ifdef HAVE_RPM + iprogress = new RGInstallProgress(me, me->_lister); + #else + #ifdef WITH_DPKG_STATUSFD + iprogress = new RGDebInstallProgress(me,me->_lister); + #else + iprogress = new RGDummyInstallProgress(); + #endif // WITH_DPKG_STATUSFD + #endif // HAVE_RPM + me->_installProgress = dynamic_cast(iprogress); + + //bool result = co_await me->_lister->commitChanges(fprogress, iprogress); + co_await me->_lister->commitChanges(fprogress, iprogress); + + // FIXME: move this into the terminal class + #ifdef HAVE_TERMINAL + // wait until the term dialog is closed + if (term != NULL) { + while (gtk_widget_get_visible(GTK_WIDGET(term->window()))) { + co_await RGFlushInterface(); + co_await sleep_ms { 100 }; + } + } + #endif + delete fprogress; + me->_fetchProgress = NULL; + delete iprogress; + me->_installProgress = NULL; + + if (_config->FindB("Synaptic::IgnorePMOutput", false) == false) { + co_await me->showErrors(); + } else { + _error->Discard(); + } + if (_config->FindB("Volatile::Non-Interactive", false) == true) { + co_return; } - } -#endif - delete fprogress; - me->_fetchProgress = NULL; - delete iprogress; - me->_installProgress = NULL; - - if (_config->FindB("Synaptic::IgnorePMOutput", false) == false) { - me->showErrors(); - } else { - _error->Discard(); - } - if (_config->FindB("Volatile::Non-Interactive", false) == true) { - return; - } - if (_config->FindB("Synaptic::AskQuitOnProceed", false) == true - && me->_userDialog->confirm(_("Do you want to quit Synaptic?"))) { - _error->Discard(); - me->saveState(); - me->showErrors(); - exit(0); - } + if (_config->FindB("Synaptic::AskQuitOnProceed", false) == true + && co_await me->_userDialog->confirm(_("Do you want to quit Synaptic?"))) { + _error->Discard(); + co_await me->saveState(); + co_await me->showErrors(); + exit(0); + } - if (_config->FindB("Volatile::Download-Only", false) == false) { - // reset the cache - if (!me->_lister->openCache()) { - me->showErrors(); - exit(1); + if (_config->FindB("Volatile::Download-Only", false) == false) { + // reset the cache + if (!me->_lister->openCache()) { + co_await me->showErrors(); + exit(1); + } } - } - // reread saved selections - ifstream in(file); - if (!in != 0) { - _error->Error(_("Can't read %s"), file); - me->_userDialog->showErrors(); - return; - } - me->_lister->readSelections(in); - unlink(file); - g_free((void *)file); + // reread saved selections + ifstream in(file); + if (!in != 0) { + _error->Error(_("Can't read %s"), file); + co_await me->_userDialog->showErrors(); + co_return; + } + me->_lister->readSelections(in); + unlink(file); + g_free((void *)file); - me->setTreeLocked(FALSE); - me->refreshTable(); - me->refreshSubViewList(); - me->setInterfaceLocked(FALSE); - me->updatePackageInfo(NULL); + me->setTreeLocked(FALSE); + me->refreshTable(); + me->refreshSubViewList(); + co_await me->setInterfaceLocked(FALSE); + me->updatePackageInfo(NULL); + }); } void RGMainWindow::cbShowWelcomeDialog(GSimpleAction *action, @@ -2704,49 +2784,51 @@ void RGMainWindow::cbShowWelcomeDialog(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - RGGtkBuilderUserDialog dia(me); - dia.run("welcome"); - GtkWidget *cb = GTK_WIDGET(gtk_builder_get_object - (dia.getGtkBuilder(), "checkbutton_show_again")); - assert(cb); - _config->Set("Synaptic::showWelcomeDialog", - gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cb))); + start_task([me]() -> task { + RGGtkBuilderUserDialog dia(me); + co_await dia.co_run("welcome"); + GtkWidget *cb = GTK_WIDGET(gtk_builder_get_object + (dia.getGtkBuilder(), "checkbutton_show_again")); + assert(cb); + _config->Set("Synaptic::showWelcomeDialog", + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cb))); + }); } -gboolean RGMainWindow::xapianDoSearch(void *data) +void RGMainWindow::xapianDoSearch(void *data) { RGMainWindow *me = (RGMainWindow *) data; - const gchar *str = gtk_entry_get_text(GTK_ENTRY(me->_entry_fast_search)); - GtkStyleContext *styleContext = gtk_widget_get_style_context(me->_entry_fast_search); + start_task([me]() -> task { + const gchar *str = gtk_entry_get_text(GTK_ENTRY(me->_entry_fast_search)); + GtkStyleContext *styleContext = gtk_widget_get_style_context(me->_entry_fast_search); - me->_fastSearchEventID = -1; - me->setBusyCursor(true); - RGFlushInterface(); - if(str == NULL || strlen(str) <= 1) { - // reset the color - gtk_style_context_remove_provider(styleContext, GTK_STYLE_PROVIDER(_fastSearchCssProvider)); - // if the user has cleared the search, refresh the view - // Gtk-CRITICAL **: gtk_tree_view_unref_tree_helper: assertion `node != NULL' failed - // at us, see LP: #38397 for more information - gtk_tree_view_set_model(GTK_TREE_VIEW(me->_treeView), NULL); - me->_lister->reapplyFilter(); - me->refreshTable(); - me->setBusyCursor(false); - } else if(strlen(str) > 1) { - // only search when there is more than one char entered, single - // char searches tend to be very slow + me->_fastSearchEventID = -1; me->setBusyCursor(true); - RGFlushInterface(); - gtk_tree_view_set_model(GTK_TREE_VIEW(me->_treeView), NULL); - me->refreshTable(); - // set color to a light yellow to make it more obvious that a search - // is performed - gtk_style_context_add_provider(styleContext, GTK_STYLE_PROVIDER(_fastSearchCssProvider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - } - me->setBusyCursor(false); - - return FALSE; + co_await RGFlushInterface(); + if(str == NULL || strlen(str) <= 1) { + // reset the color + gtk_style_context_remove_provider(styleContext, GTK_STYLE_PROVIDER(_fastSearchCssProvider)); + // if the user has cleared the search, refresh the view + // Gtk-CRITICAL **: gtk_tree_view_unref_tree_helper: assertion `node != NULL' failed + // at us, see LP: #38397 for more information + gtk_tree_view_set_model(GTK_TREE_VIEW(me->_treeView), NULL); + me->_lister->reapplyFilter(); + me->refreshTable(); + me->setBusyCursor(false); + } else if(strlen(str) > 1) { + // only search when there is more than one char entered, single + // char searches tend to be very slow + me->setBusyCursor(true); + co_await RGFlushInterface(); + gtk_tree_view_set_model(GTK_TREE_VIEW(me->_treeView), NULL); + me->refreshTable(); + // set color to a light yellow to make it more obvious that a search + // is performed + gtk_style_context_add_provider(styleContext, GTK_STYLE_PROVIDER(_fastSearchCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + me->setBusyCursor(false); + }); } void RGMainWindow::cbSearchEntryChanged(GtkWidget *edit, void *data) @@ -2757,7 +2839,7 @@ void RGMainWindow::cbSearchEntryChanged(GtkWidget *edit, void *data) g_source_remove(me->_fastSearchEventID); me->_fastSearchEventID = -1; } - me->_fastSearchEventID = g_timeout_add(500, xapianDoSearch, me); + me->_fastSearchEventID = g_timeout_add_once(500, xapianDoSearch, me); } void RGMainWindow::cbUpdateClicked(GSimpleAction *action, @@ -2765,77 +2847,77 @@ void RGMainWindow::cbUpdateClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + // need to delete dialogs, as they might have data pointing + // to old stuff + //xxx delete me->_fmanagerWin; + me->_fmanagerWin = NULL; - // need to delete dialogs, as they might have data pointing - // to old stuff -//xxx delete me->_fmanagerWin; - me->_fmanagerWin = NULL; - - RGFetchProgress *progress=me->_fetchProgress= new RGFetchProgress(me); - progress->setDescription(_("Downloading Package Information"), - _("The repositories will be checked for new, removed " - "or upgraded software packages.")); + RGFetchProgress *progress=me->_fetchProgress= new RGFetchProgress(me); + progress->setDescription(_("Downloading Package Information"), + _("The repositories will be checked for new, removed " + "or upgraded software packages.")); - me->setStatusText(_("Reloading package information...")); + me->setStatusText(_("Reloading package information...")); - me->setInterfaceLocked(TRUE); - me->setTreeLocked(TRUE); - me->_lister->unregisterObserver(me); - - // save to temporary file - const gchar *file = - g_strdup_printf("%s/selections.update", RConfDir().c_str()); - ofstream out(file); - if (!out != 0) { - _error->Error(_("Can't write %s"), file); - me->_userDialog->showErrors(); - return; - } - me->_lister->writeSelections(out, false); + co_await me->setInterfaceLocked(TRUE); + me->setTreeLocked(TRUE); + me->_lister->unregisterObserver(me); - // update cache and forget about the previous new packages - // (only if no error occurred) - string error; - if (!me->_lister->updateCache(progress,error)) { - RGGtkBuilderUserDialog dia(me,"update_failed"); - GtkWidget *tv = GTK_WIDGET(gtk_builder_get_object(dia.getGtkBuilder(), - "textview")); - GtkTextBuffer *tb = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); - gtk_text_buffer_set_text(tb, utf8(error.c_str()), -1); - dia.run(); - } else { - me->forgetNewPackages(); - _config->Set("Synaptic::update::last",time(NULL)); - } - delete progress; - me->_fetchProgress=NULL; + // save to temporary file + const gchar *file = g_strdup_printf("%s/selections.update", RConfDir().c_str()); + ofstream out(file); + if (!out != 0) { + _error->Error(_("Can't write %s"), file); + co_await me->_userDialog->showErrors(); + co_return; + } + me->_lister->writeSelections(out, false); + + // update cache and forget about the previous new packages + // (only if no error occurred) + string error; + if (!me->_lister->updateCache(progress,error)) { + RGGtkBuilderUserDialog dia(me,"update_failed"); + GtkWidget *tv = GTK_WIDGET(gtk_builder_get_object(dia.getGtkBuilder(), + "textview")); + GtkTextBuffer *tb = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + gtk_text_buffer_set_text(tb, utf8(error.c_str()), -1); + co_await dia.co_run(); + } else { + me->forgetNewPackages(); + _config->Set("Synaptic::update::last",time(NULL)); + } + delete progress; + me->_fetchProgress=NULL; - // show errors and warnings (like the gpg failures for the package list) - me->showErrors(); + // show errors and warnings (like the gpg failures for the package list) + co_await me->showErrors(); - if(!me->_lister->openCache()) { - me->showErrors(); - exit(1); - } - // reread saved selections - ifstream in(file); - if (!in != 0) { - _error->Error(_("Can't read %s"), file); - me->_userDialog->showErrors(); - return; - } - me->_lister->readSelections(in); - unlink(file); - g_free((void *)file); + if(!me->_lister->openCache()) { + co_await me->showErrors(); + exit(1); + } + // reread saved selections + ifstream in(file); + if (!in != 0) { + _error->Error(_("Can't read %s"), file); + co_await me->_userDialog->showErrors(); + co_return; + } + me->_lister->readSelections(in); + unlink(file); + g_free((void *)file); - // check if the index needs to be rebuild - me->xapianDoIndexUpdate(me); + // check if the index needs to be rebuild + me->xapianDoIndexUpdate(me); - me->setTreeLocked(FALSE); - me->refreshTable(); - me->refreshSubViewList(); - me->setInterfaceLocked(FALSE); - me->setStatusText(); + me->setTreeLocked(FALSE); + me->refreshTable(); + me->refreshSubViewList(); + co_await me->setInterfaceLocked(FALSE); + me->setStatusText(); + }); } void RGMainWindow::cbFixBrokenClicked(GSimpleAction *action, @@ -2843,19 +2925,21 @@ void RGMainWindow::cbFixBrokenClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - RPackage *pkg = me->selectedPackage(); + start_task([me]() -> task { + RPackage *pkg = me->selectedPackage(); - bool res = me->_lister->fixBroken(); - me->setInterfaceLocked(TRUE); - me->refreshTable(pkg); + bool res = me->_lister->fixBroken(); + co_await me->setInterfaceLocked(TRUE); + me->refreshTable(pkg); - if (!res) - me->setStatusText(_("Failed to resolve dependency problems!")); - else - me->setStatusText(_("Successfully fixed dependency problems")); + if (!res) + me->setStatusText(_("Failed to resolve dependency problems!")); + else + me->setStatusText(_("Successfully fixed dependency problems")); - me->setInterfaceLocked(FALSE); - me->showErrors(); + co_await me->setInterfaceLocked(FALSE); + co_await me->showErrors(); + }); } @@ -2864,89 +2948,91 @@ void RGMainWindow::cbUpgradeClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - RPackage *pkg = me->selectedPackage(); - bool dist_upgrade; - int res; - - if (!me->_lister->check()) { - me->_userDialog->error( - _("Could not upgrade the system!\n" - "Fix broken packages first.")); - return; - } - // check if we have saved upgrade type - UpgradeType upgrade = - (UpgradeType) _config->FindI("Synaptic::UpgradeType", UPGRADE_DIST); - - // special case for non-interactive upgrades - if(_config->FindB("Volatile::Non-Interactive", false)) - if(_config->FindB("Volatile::Upgrade-Mode", false)) - upgrade = UPGRADE_NORMAL; - else if(_config->FindB("Volatile::DistUpgrade-Mode", false)) - upgrade = UPGRADE_DIST; - - - if (upgrade == UPGRADE_ASK) { - // ask what type of upgrade the user wants - GtkBuilder *builder; - GtkWidget *button; - - RGGtkBuilderUserDialog dia(me); - res = dia.run("upgrade", true); - switch(res) { - case GTK_RESPONSE_CANCEL: - case GTK_RESPONSE_DELETE_EVENT: - return; - case GTK_RESPONSE_YES: - dist_upgrade = true; - break; - case GTK_RESPONSE_NO: - dist_upgrade = false; - break; - default: - cerr << "unknown return " << res - << " from UpgradeDialog, please report" << endl; + start_task([me]() -> task { + RPackage *pkg = me->selectedPackage(); + bool dist_upgrade; + int res; + + if (!me->_lister->check()) { + co_await me->_userDialog->error( + _("Could not upgrade the system!\n" + "Fix broken packages first.")); + co_return; + } + // check if we have saved upgrade type + UpgradeType upgrade = + (UpgradeType) _config->FindI("Synaptic::UpgradeType", UPGRADE_DIST); + + // special case for non-interactive upgrades + if(_config->FindB("Volatile::Non-Interactive", false)) { + if(_config->FindB("Volatile::Upgrade-Mode", false)) + upgrade = UPGRADE_NORMAL; + else if(_config->FindB("Volatile::DistUpgrade-Mode", false)) + upgrade = UPGRADE_DIST; } - builder = dia.getGtkBuilder(); - // see if the user wants the answer saved - button = GTK_WIDGET(gtk_builder_get_object - (builder, "checkbutton_remember")); - if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) - _config->Set("Synaptic::upgradeType", dist_upgrade); - } else { - // use the saved answer (don't ask) - dist_upgrade = upgrade; - } - // do the work - me->setInterfaceLocked(TRUE); - me->setStatusText(_("Marking all available upgrades...")); + if (upgrade == UPGRADE_ASK) { + // ask what type of upgrade the user wants + GtkBuilder *builder; + GtkWidget *button; + + RGGtkBuilderUserDialog dia(me); + res = co_await dia.co_run("upgrade", true); + switch(res) { + case GTK_RESPONSE_CANCEL: + case GTK_RESPONSE_DELETE_EVENT: + co_return; + case GTK_RESPONSE_YES: + dist_upgrade = true; + break; + case GTK_RESPONSE_NO: + dist_upgrade = false; + break; + default: + cerr << "unknown return " << res + << " from UpgradeDialog, please report" << endl; + } + builder = dia.getGtkBuilder(); + // see if the user wants the answer saved + button = GTK_WIDGET(gtk_builder_get_object + (builder, "checkbutton_remember")); + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) + _config->Set("Synaptic::upgradeType", dist_upgrade); + } else { + // use the saved answer (don't ask) + dist_upgrade = upgrade; + } - me->_lister->saveUndoState(); - - RPackageLister::pkgState state; - me->_lister->saveState(state); + // do the work + co_await me->setInterfaceLocked(TRUE); + me->setStatusText(_("Marking all available upgrades...")); - if (dist_upgrade) - res = me->_lister->distUpgrade(); - else - res = me->_lister->upgrade(); + me->_lister->saveUndoState(); - if(me->askStateChange(state)) - { - me->refreshTable(pkg); + RPackageLister::pkgState state; + me->_lister->saveState(state); - if (res) - me->setStatusText(_("Successfully marked available upgrades")); + if (dist_upgrade) + res = me->_lister->distUpgrade(); else - me->setStatusText(_("Failed to mark all available upgrades!")); - } else { - // if the user canceled the action, just show the default message - me->setStatusText(); - } + res = me->_lister->upgrade(); + + if (co_await me->askStateChange(state)) + { + me->refreshTable(pkg); + + if (res) + me->setStatusText(_("Successfully marked available upgrades")); + else + me->setStatusText(_("Failed to mark all available upgrades!")); + } else { + // if the user canceled the action, just show the default message + me->setStatusText(); + } - me->setInterfaceLocked(FALSE); - me->showErrors(); + co_await me->setInterfaceLocked(FALSE); + co_await me->showErrors(); + }); } void RGMainWindow::cbMenuPinClicked(GSimpleAction *action, @@ -2954,75 +3040,77 @@ void RGMainWindow::cbMenuPinClicked(GSimpleAction *action, gpointer data) { RGMainWindow *me = (RGMainWindow *) data; - bool active = g_variant_get_boolean(parameter); - GtkTreeSelection *selection; - GtkTreeIter iter; - RPackage *pkg; - if (me->_blockActions) - return; + start_task([me, active]() -> task { + GtkTreeSelection *selection; + GtkTreeIter iter; + RPackage *pkg; - selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(me->_treeView)); - GList *li, *list; + if (me->_blockActions) + co_return; - list = li = gtk_tree_selection_get_selected_rows(selection, &me->_pkgList); - if (li == NULL) - return; + selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(me->_treeView)); + GList *li, *list; - me->setInterfaceLocked(TRUE); - me->_lister->unregisterObserver(me); + list = li = gtk_tree_selection_get_selected_rows(selection, &me->_pkgList); + if (li == NULL) + co_return; - // save to temporary file - const gchar *file = - g_strdup_printf("%s/selections.hold", RConfDir().c_str()); - ofstream out(file); - if (!out != 0) { - _error->Error(_("Can't write %s"), file); - me->_userDialog->showErrors(); - return; - } - me->_lister->writeSelections(out, false); + co_await me->setInterfaceLocked(TRUE); + me->_lister->unregisterObserver(me); - while (li != NULL) { - gtk_tree_model_get_iter(me->_pkgList, &iter, (GtkTreePath *) (li->data)); - gtk_tree_model_get(me->_pkgList, &iter, PKG_COLUMN, &pkg, -1); - if (pkg == NULL) { - li = g_list_next(li); - continue; + // save to temporary file + const gchar *file = + g_strdup_printf("%s/selections.hold", RConfDir().c_str()); + ofstream out(file); + if (!out != 0) { + _error->Error(_("Can't write %s"), file); + co_await me->_userDialog->showErrors(); + co_return; } + me->_lister->writeSelections(out, false); + + while (li != NULL) { + gtk_tree_model_get_iter(me->_pkgList, &iter, (GtkTreePath *) (li->data)); + gtk_tree_model_get(me->_pkgList, &iter, PKG_COLUMN, &pkg, -1); + if (pkg == NULL) { + li = g_list_next(li); + continue; + } - pkg->setPinned(active); - _roptions->setPackageLock(pkg->name(), active); - li = g_list_next(li); - } - me->setTreeLocked(TRUE); - if (!me->_lister->openCache()) { - me->showErrors(); - exit(1); - } + pkg->setPinned(active); + _roptions->setPackageLock(pkg->name(), active); + li = g_list_next(li); + } + me->setTreeLocked(TRUE); + if (!me->_lister->openCache()) { + co_await me->showErrors(); + exit(1); + } - // reread saved selections - ifstream in(file); - if (!in != 0) { - _error->Error(_("Can't read %s"), file); - me->_userDialog->showErrors(); - return; - } - me->_lister->readSelections(in); - unlink(file); - g_free((void *)file); + // reread saved selections + ifstream in(file); + if (!in != 0) { + _error->Error(_("Can't read %s"), file); + co_await me->_userDialog->showErrors(); + co_return; + } + me->_lister->readSelections(in); + unlink(file); + g_free((void *)file); - // free the list - g_list_foreach(list, (void (*)(void *, void *))gtk_tree_path_free, NULL); - g_list_free(list); + // free the list + g_list_foreach(list, (void (*)(void *, void *))gtk_tree_path_free, NULL); + g_list_free(list); - me->_lister->registerObserver(me); - me->setTreeLocked(FALSE); - me->refreshTable(); - me->refreshSubViewList(); - me->refreshTable(); - me->setInterfaceLocked(FALSE); + me->_lister->registerObserver(me); + me->setTreeLocked(FALSE); + me->refreshTable(); + me->refreshSubViewList(); + me->refreshTable(); + co_await me->setInterfaceLocked(FALSE); + }); } void RGMainWindow::cbTreeviewPopupMenu(GtkWidget *treeview, @@ -3153,59 +3241,7 @@ GMenu* RGMainWindow::buildWeakDependsMenu(RPackage *pkg, void RGMainWindow::selectToInstall(vector packagenames) { RGMainWindow *me = this; - - RPackageLister::pkgState state; - vector exclude; - vector instPkgs; - - // we always save the state (for undo) - me->_lister->saveState(state); - me->_lister->notifyCachePreChange(); - - for(unsigned int i=0;i_lister->getPackage(packagenames[i]); - if (newpkg) { - // only install the package if it is not already installed or if - // it is outdated - if(!(newpkg->getFlags()&RPackage::FInstalled) || - (newpkg->getFlags()&RPackage::FOutdated)) { - // actual action - newpkg->setNotify(false); - me->pkgInstallHelper(newpkg); - newpkg->setNotify(true); - //exclude.push_back(newpkg); - instPkgs.push_back(newpkg); - } - } - } - - // ask for additional changes - me->setBusyCursor(true); - if(me->askStateChange(state, exclude)) { - me->_lister->saveUndoState(state); - if(me->checkForFailedInst(instPkgs)) - me->_lister->restoreState(state); - } - me->setBusyCursor(false); - me->_lister->notifyPostChange(NULL); - me->_lister->notifyCachePostChange(); - - RPackage *pkg = me->selectedPackage(); - me->refreshTable(pkg); - me->updatePackageInfo(pkg); -} - -void RGMainWindow::pkgInstallByNameHelper(GSimpleAction *action, - GVariant *parameter, - gpointer data) -{ - const char *name = g_variant_get_string(parameter, nullptr); - //cout << "pkgInstallByNameHelper: " << name << endl; - - RGMainWindow *me = (RGMainWindow*) data; - - RPackage *newpkg = (RPackage *) me->_lister->getPackage(name); - if (newpkg) { + start_task([me, packagenames]() -> task { RPackageLister::pkgState state; vector exclude; vector instPkgs; @@ -3214,27 +3250,81 @@ void RGMainWindow::pkgInstallByNameHelper(GSimpleAction *action, me->_lister->saveState(state); me->_lister->notifyCachePreChange(); - // actual action - newpkg->setNotify(false); - me->pkgInstallHelper(newpkg); - newpkg->setNotify(true); - - exclude.push_back(newpkg); - instPkgs.push_back(newpkg); + for(unsigned int i=0;i_lister->getPackage(packagenames[i]); + if (newpkg) { + // only install the package if it is not already installed or if + // it is outdated + if(!(newpkg->getFlags()&RPackage::FInstalled) || + (newpkg->getFlags()&RPackage::FOutdated)) { + // actual action + newpkg->setNotify(false); + me->pkgInstallHelper(newpkg); + newpkg->setNotify(true); + //exclude.push_back(newpkg); + instPkgs.push_back(newpkg); + } + } + } // ask for additional changes - if(me->askStateChange(state, exclude)) { - me->_lister->saveUndoState(state); - if(me->checkForFailedInst(instPkgs)) - me->_lister->restoreState(state); + me->setBusyCursor(true); + if (co_await me->askStateChange(state, exclude)) { + me->_lister->saveUndoState(state); + if (co_await me->checkForFailedInst(instPkgs)) + me->_lister->restoreState(state); } + me->setBusyCursor(false); me->_lister->notifyPostChange(NULL); me->_lister->notifyCachePostChange(); RPackage *pkg = me->selectedPackage(); me->refreshTable(pkg); me->updatePackageInfo(pkg); - } + }); +} + +void RGMainWindow::pkgInstallByNameHelper(GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + const char *name = g_variant_get_string(parameter, nullptr); + //cout << "pkgInstallByNameHelper: " << name << endl; + + RGMainWindow *me = (RGMainWindow*) data; + start_task([me, name]() -> task { + RPackage *newpkg = (RPackage *) me->_lister->getPackage(name); + if (newpkg) { + RPackageLister::pkgState state; + vector exclude; + vector instPkgs; + + // we always save the state (for undo) + me->_lister->saveState(state); + me->_lister->notifyCachePreChange(); + + // actual action + newpkg->setNotify(false); + me->pkgInstallHelper(newpkg); + newpkg->setNotify(true); + + exclude.push_back(newpkg); + instPkgs.push_back(newpkg); + + // ask for additional changes + if (co_await me->askStateChange(state, exclude)) { + me->_lister->saveUndoState(state); + if (co_await me->checkForFailedInst(instPkgs)) + me->_lister->restoreState(state); + } + me->_lister->notifyPostChange(NULL); + me->_lister->notifyCachePostChange(); + + RPackage *pkg = me->selectedPackage(); + me->refreshTable(pkg); + me->updatePackageInfo(pkg); + } + }); } void RGMainWindow::cbGenerateDownloadScriptClicked(GSimpleAction *action, @@ -3243,41 +3333,48 @@ void RGMainWindow::cbGenerateDownloadScriptClicked(GSimpleAction *action, { //cout << "cbGenerateDownloadScriptClicked()" << endl; RGMainWindow *me = (RGMainWindow *) data; + start_task([me]() -> task { + int installed, broken, toInstall, toRemove; + double sizeChange; + me->_lister->getStats(installed, broken, toInstall, toRemove, sizeChange); + if(toInstall== 0) { + co_await me->_userDialog->message("Nothing to install/upgrade\n\n" + "Please select the \"Mark all Upgrades\" " + "button or some packages to install/upgrade."); + co_return; + } - int installed, broken, toInstall, toRemove; - double sizeChange; - me->_lister->getStats(installed, broken, toInstall, toRemove, sizeChange); - if(toInstall== 0) { - me->_userDialog->message("Nothing to install/upgrade\n\n" - "Please select the \"Mark all Upgrades\" " - "button or some packages to install/upgrade."); - return; - } - - vector uris; - if(!me->_lister->getDownloadUris(uris)) - return; + vector uris; + if(!me->_lister->getDownloadUris(uris)) + co_return; + + GtkWidget *filesel; + filesel = gtk_file_chooser_dialog_new(_("Save script"), + GTK_WINDOW(me->window()), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, + NULL); + int res = co_await co_run_dialog(GTK_DIALOG(filesel)); + GFile *selected_file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(filesel)); + gtk_widget_destroy(filesel); + if(res != GTK_RESPONSE_ACCEPT) { + g_object_unref(selected_file); + co_return; + } - GtkWidget *filesel; - filesel = gtk_file_chooser_dialog_new(_("Save script"), - GTK_WINDOW(me->window()), - GTK_FILE_CHOOSER_ACTION_SAVE, - _("_Cancel"), GTK_RESPONSE_CANCEL, - _("_Save"), GTK_RESPONSE_ACCEPT, - NULL); - int res = gtk_dialog_run(GTK_DIALOG(filesel)); - const char *file = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel)); - gtk_widget_destroy(filesel); - if(res != GTK_RESPONSE_ACCEPT) - return; + char *file = g_file_get_path (selected_file); + g_object_unref(selected_file); - // FIXME: this is prototype code, hardcoding wget here suckx - ofstream out(file); - out << "#!/bin/sh" << endl; - for(int i=0;iwindow()), - GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, - _("_Cancel"), GTK_RESPONSE_CANCEL, - _("_Open"), GTK_RESPONSE_ACCEPT, - NULL); - int res = gtk_dialog_run(GTK_DIALOG(filesel)); - const char *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel)); - gtk_widget_destroy(filesel); - if(res != GTK_RESPONSE_ACCEPT) - return; - if (!g_file_test(path, G_FILE_TEST_IS_DIR)) { - me->_userDialog->error(_("Please select a directory")); - return; - } - // now read the dir for debs - const gchar *file; - string pkgname; - stringstream pkgs; - GDir *dir = g_dir_open(path, 0, NULL); - while ( (file=g_dir_read_name(dir)) != NULL) { - if(g_pattern_match_simple("*_*.deb", file)) { - if(me->_lister->addArchiveToCache(string(path)+"/"+string(file), - pkgname)) - pkgs << pkgname << "\t install" << endl; + start_task([me]() -> task { + GtkWidget *filesel; + filesel = gtk_file_chooser_dialog_new(_("Select directory"), + GTK_WINDOW(me->window()), + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Open"), GTK_RESPONSE_ACCEPT, + NULL); + int res = co_await co_run_dialog(GTK_DIALOG(filesel)); + GFile *selected_file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(filesel)); + gtk_widget_destroy(filesel); + if(res != GTK_RESPONSE_ACCEPT) { + g_object_unref(selected_file); + co_return; + } + GFileType file_type = g_file_query_file_type(selected_file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL); + if (file_type != G_FILE_TYPE_DIRECTORY) { + co_await me->_userDialog->error(_("Please select a directory")); + g_object_unref(selected_file); + co_return; } - } - g_dir_close(dir); - // and set what we found as selection - pkgs.seekg(0); - if (pkgs.str() == "") - return; + // now read the dir for debs + char *path = g_file_get_path (selected_file); + const gchar *file; + string pkgname; + stringstream pkgs; + GDir *dir = g_dir_open(path, 0, NULL); + while ( (file=g_dir_read_name(dir)) != NULL) { + if(g_pattern_match_simple("*_*.deb", file)) { + if(me->_lister->addArchiveToCache(string(path)+"/"+string(file), pkgname)) + pkgs << pkgname << "\t install" << endl; + } + } + g_dir_close(dir); + g_free(path); + g_object_unref(selected_file); - me->_lister->unregisterObserver(me); - me->_lister->readSelections(pkgs); - me->_lister->registerObserver(me); - me->refreshTable(); + // and set what we found as selection + pkgs.seekg(0); + if (pkgs.str() == "") + co_return; - // show any errors - me->_userDialog->showErrors(); - - // click proceed - me->cbProceedClicked(nullptr, nullptr, me); + me->_lister->unregisterObserver(me); + me->_lister->readSelections(pkgs); + me->_lister->registerObserver(me); + me->refreshTable(); + + // show any errors + co_await me->_userDialog->showErrors(); + // click proceed + me->cbProceedClicked(nullptr, nullptr, me); + }); #else me->_userDialog->error("Sorry, not implemented for rpm, patches welcome"); #endif diff --git a/gtk/rgmainwindow.h b/gtk/rgmainwindow.h index 56706cc79..6a32e8fb1 100644 --- a/gtk/rgmainwindow.h +++ b/gtk/rgmainwindow.h @@ -31,6 +31,7 @@ #include "rggtkbuilderwindow.h" #include "rpackagelister.h" #include "rpackageview.h" +#include "coroutines.h" #include #include @@ -153,12 +154,9 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { // display/table releated void refreshSubViewList(); - virtual bool close(); - static void closeWin(GSimpleAction *action, - GVariant *parameter, - gpointer me) { - ((RGMainWindow *) me)->close(); - }; + [[nodiscard]] virtual task close(); + static gboolean windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data); + static void closeWin(GSimpleAction *action, GVariant *parameter, gpointer data); // misc void forgetNewPackages(); @@ -169,13 +167,13 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { std::string selectedSubView(); // helpers - void pkgAction(RGPkgAction action); - bool askStateChange(RPackageLister::pkgState, + [[nodiscard]] task pkgAction(RGPkgAction action); + [[nodiscard]] task askStateChange(RPackageLister::pkgState, const std::vector &exclude = std::vector()); - bool checkForFailedInst(std::vector instPkgs); + [[nodiscard]] task checkForFailedInst(std::vector instPkgs); void pkgInstallHelper(RPackage *pkg, bool fixBroken = true, bool reInstall = false); - void pkgRemoveHelper(RPackage *pkg, bool purge = false, + [[nodiscard]] task pkgRemoveHelper(RPackage *pkg, bool purge = false, bool withDeps = false); void pkgKeepHelper(RPackage *pkg); @@ -191,7 +189,7 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { // helpers for search-as-you-type static void cbSearchEntryChanged(GtkWidget *editable, void *data); static void xapianIndexUpdateFinished(GPid pid, gint status, void* data); - static gboolean xapianDoSearch(void *data); + static void xapianDoSearch(void *data); static gboolean xapianDoIndexUpdate(void *data); // RPackageObserver @@ -207,12 +205,12 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { void refreshTable(RPackage *selectedPkg = NULL,bool setAdjustments=true); - void changeView(int view, std::string subView=""); + [[nodiscard]] task changeView(int view, std::string subView=""); // install the list of packagenames and display a changes window void selectToInstall(std::vector packagenames); - void setInterfaceLocked(bool flag); + [[nodiscard]] task setInterfaceLocked(bool flag); void setTreeLocked(bool flag); void rebuildTreeView() { buildTreeView(); @@ -223,10 +221,10 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { // this helper will bring the current active window to the foreground void activeWindowToForeground(); - void saveState(); - bool restoreState(); + [[nodiscard]] task saveState(); + [[nodiscard]] task restoreState(); - bool showErrors(); + [[nodiscard]] task showErrors(); GMenu* buildWeakDependsMenu(RPackage *pkg, pkgCache::Dep::DepType); @@ -278,6 +276,8 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { GtkTreePath *arg1, GtkTreeViewColumn *arg2, gpointer user_data); + [[nodiscard]] task packageListRowActivated(GtkTreeView *treeview, + GtkTreePath *path); static void cbChangedView(GtkWidget *self, void *); static void cbChangedSubView(GtkTreeSelection *selection, gpointer data); @@ -296,9 +296,11 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { static void cbSaveClicked(GSimpleAction *action, GVariant *parameter, gpointer data); + [[nodiscard]] task saveClicked(); static void cbSaveAsClicked(GSimpleAction *action, GVariant *parameter, gpointer data); + [[nodiscard]] task saveAsClicked(); std::string selectionsFilename; bool saveFullState; static void cbGenerateDownloadScriptClicked(GSimpleAction *action, @@ -344,6 +346,7 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { static void cbMenuAutoInstalledClicked(GSimpleAction *action, GVariant *parameter, gpointer data); + [[nodiscard]] task autoInstalled(bool active); // filter menu static void cbShowFilterManagerWindow(GSimpleAction *action, @@ -376,6 +379,7 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { static void cbHelpAction(GSimpleAction *action, GVariant *parameter, gpointer data); + [[nodiscard]] task helpAction(); static void cbShowIconLegendPanel(GSimpleAction *action, GVariant *parameter, gpointer data); @@ -390,9 +394,11 @@ class RGMainWindow : public RGGtkBuilderWindow, public RPackageObserver { static void cbPkgHelpClicked(GSimpleAction *action, GVariant *parameter, gpointer data); + [[nodiscard]] task pkgHelpClicked(); static void cbPkgReconfigureClicked(GSimpleAction *action, GVariant *parameter, gpointer data); + [[nodiscard]] task pkgReconfigureClicked(); }; diff --git a/gtk/rgpkgcdrom.cc b/gtk/rgpkgcdrom.cc index 338bfef2a..510e339f8 100644 --- a/gtk/rgpkgcdrom.cc +++ b/gtk/rgpkgcdrom.cc @@ -57,7 +57,7 @@ class RGDiscName : public RGGtkBuilderWindow public: RGDiscName(RGWindow *wwin, const string defaultName); - bool run(string &name); + [[nodiscard]] task run(string &name); }; void RGCDScanner::Update(string text, int current) @@ -69,24 +69,22 @@ void RGCDScanner::Update(string text, int current) gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(_pbar), ((float)current) / totalSteps); show(); - RGFlushInterface(); } bool RGCDScanner::ChangeCdrom() { - return _userDialog->proceed(_("Please insert a disc in the drive.")); + // TODO + // co_return co_await _userDialog->proceed(_("Please insert a disc in the drive.")); + return false; } bool RGCDScanner::AskCdromName(string &name) { //cout << "askCdromName()" << endl; RGDiscName discName(this, name); - - if (!discName.run(name)) { - return false; - } - - return true; + // TODO +// co_return co_await discName.run(name); + return false; } RGCDScanner::RGCDScanner(RGMainWindow *main, RUserDialog *userDialog) @@ -116,11 +114,12 @@ RGCDScanner::RGCDScanner(RGMainWindow *main, RUserDialog *userDialog) GTK_WIN_POS_CENTER_ON_PARENT); } -bool RGCDScanner::run() +task RGCDScanner::run() { pkgCdrom scanner; - return scanner.Add(this); + bool result = scanner.Add(this); + co_return result; } RGDiscName::RGDiscName(RGWindow *wwin, const string defaultName) @@ -147,21 +146,22 @@ void RGDiscName::onOkClicked(GtkWidget *self, void *data) { RGDiscName *me = (RGDiscName *) data; me->_userConfirmed = true; - gtk_main_quit(); + gtk_window_close (GTK_WINDOW (me->_win)); } void RGDiscName::onCancelClicked(GtkWidget *self, void *data) { - gtk_main_quit(); + RGDiscName *me = (RGDiscName *) data; + gtk_window_close (GTK_WINDOW (me->_win)); } -bool RGDiscName::run(string &discName) +task RGDiscName::run(string &discName) { _userConfirmed = false; show(); - gtk_main(); + co_await co_run_window (GTK_WINDOW (_win)); discName = gtk_entry_get_text(GTK_ENTRY(_textEntry)); - return _userConfirmed; + co_return _userConfirmed; } #endif diff --git a/gtk/rgpkgcdrom.h b/gtk/rgpkgcdrom.h index 2b5184748..7669ee18c 100644 --- a/gtk/rgpkgcdrom.h +++ b/gtk/rgpkgcdrom.h @@ -30,6 +30,7 @@ #ifdef HAVE_APTPKG_CDROM #include "rgwindow.h" +#include "coroutines.h" #include #include @@ -53,7 +54,7 @@ class RGCDScanner:public pkgCdromStatus, public RGWindow { bool AskCdromName(std::string &defaultName); void Update(std::string text, int current); - bool run(); + [[nodiscard]] task run(); }; diff --git a/gtk/rgpkgdetails.cc b/gtk/rgpkgdetails.cc index a6b1b0a6c..d3dc93eff 100644 --- a/gtk/rgpkgdetails.cc +++ b/gtk/rgpkgdetails.cc @@ -150,11 +150,12 @@ void RGPkgDetailsWindow::cbShowBigScreenshot(GtkWidget *box, { //cerr << "cbShowBigScreenshot" << endl; RPackage *pkg = (RPackage *)data; - - doShowBigScreenshot(pkg); + start_task([pkg]() -> task { + co_await doShowBigScreenshot(pkg); + }); } -void RGPkgDetailsWindow::doShowBigScreenshot(RPackage *pkg) +task RGPkgDetailsWindow::doShowBigScreenshot(RPackage *pkg) { RGFetchProgress *status = new RGFetchProgress(NULL);; pkgAcquire fetcher(status); @@ -166,44 +167,46 @@ void RGPkgDetailsWindow::doShowBigScreenshot(RPackage *pkg) gtk_widget_show(img); GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (win)); gtk_container_add(GTK_CONTAINER(content_area), img); - gtk_dialog_run(GTK_DIALOG(win)); + co_await co_run_dialog(GTK_DIALOG(win)); gtk_widget_destroy(win); } void RGPkgDetailsWindow::cbShowScreenshot(GtkWidget *button, void *data) { struct screenshot_info *si = (struct screenshot_info*)data; + start_task([button, si]() -> task { + if(_config->FindB("Synaptic::InlineScreenshots") == false) + { + co_await doShowBigScreenshot(si->pkg); + } else { - if(_config->FindB("Synaptic::InlineScreenshots") == false) - { - doShowBigScreenshot(si->pkg); - return; - } else { - - // hide button - gtk_widget_hide(button); - - // get screenshot - RGFetchProgress *status = new RGFetchProgress(NULL);; - pkgAcquire fetcher(status); - string filename = si->pkg->getScreenshotFile(&fetcher); - GtkWidget *event = gtk_event_box_new(); - GtkWidget *img = gtk_image_new_from_file(filename.c_str()); - gtk_container_add(GTK_CONTAINER(event), img); - g_signal_connect(G_OBJECT(event), "button_press_event", - G_CALLBACK(cbShowBigScreenshot), - (void*)si->pkg); - gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(si->textview), - GTK_WIDGET(event), si->anchor); - gtk_widget_show_all(event); - } + // hide button + gtk_widget_hide(button); + + // get screenshot + RGFetchProgress *status = new RGFetchProgress(NULL);; + pkgAcquire fetcher(status); + string filename = si->pkg->getScreenshotFile(&fetcher); + GtkWidget *event = gtk_event_box_new(); + GtkWidget *img = gtk_image_new_from_file(filename.c_str()); + gtk_container_add(GTK_CONTAINER(event), img); + g_signal_connect(G_OBJECT(event), "button_press_event", + G_CALLBACK(cbShowBigScreenshot), + (void*)si->pkg); + gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(si->textview), + GTK_WIDGET(event), si->anchor); + gtk_widget_show_all(event); + } + }); } void RGPkgDetailsWindow::cbShowChangelog(GtkWidget *button, void *data) { RPackage *pkg = (RPackage*)data; RGWindow *parent = (RGWindow*)g_object_get_data(G_OBJECT(button), "me"); - ShowChangelogDialog(parent, pkg); + start_task([parent, pkg]() -> task { + co_await ShowChangelogDialog(parent, pkg); + }); } gboolean RGPkgDetailsWindow::cbOpenLink(GtkWidget *label, @@ -400,7 +403,7 @@ void RGPkgDetailsWindow::fillInValues(RGGtkBuilderWindow *me, gchar *str; vector list; vector > versions = pkg->getAvailableVersions(); - for(int i=0;i #include @@ -56,7 +57,7 @@ class RGPkgDetailsWindow : public RGGtkBuilderWindow { static void cbShowChangelog(GtkWidget *button, void *data); static gboolean cbOpenHomepage(GtkWidget *button, void *data); static gboolean cbOpenLink(GtkWidget *button, gchar *uri, void *data); - static void doShowBigScreenshot(RPackage *pkg); + [[nodiscard]] static task doShowBigScreenshot(RPackage *pkg); public: RGPkgDetailsWindow(RGWindow *parent); diff --git a/gtk/rgpreferenceswindow.cc b/gtk/rgpreferenceswindow.cc index 47f870030..c78401996 100644 --- a/gtk/rgpreferenceswindow.cc +++ b/gtk/rgpreferenceswindow.cc @@ -405,21 +405,28 @@ void RGPreferencesWindow::saveDistribution() } -void RGPreferencesWindow::saveAction(GtkWidget *self, void *data) +void RGPreferencesWindow::cbSaveAction(GtkWidget *self, void *data) { RGPreferencesWindow *me = (RGPreferencesWindow *) data; + start_task([me]() -> task { + co_await me->saveAction(); + }); +} + - me->saveGeneral(); - me->saveColumnsAndFonts(); - me->saveColors(); - me->saveFiles(); - me->saveNetwork(); - me->saveDistribution(); +task RGPreferencesWindow::saveAction() +{ + saveGeneral(); + saveColumnsAndFonts(); + saveColors(); + saveFiles(); + saveNetwork(); + saveDistribution(); if (!RWriteConfigFile(*_config)) { _error->Error(_("An error occurred while saving configurations.")); - RGUserDialog userDialog(me); - userDialog.showErrors(); + RGUserDialog userDialog(this); + co_await userDialog.showErrors(); } } @@ -427,62 +434,67 @@ void RGPreferencesWindow::saveAction(GtkWidget *self, void *data) void RGPreferencesWindow::closeAction(GtkWidget *self, void *data) { RGPreferencesWindow *me = (RGPreferencesWindow *) data; - me->close(); + me->hide(); } -void RGPreferencesWindow::doneAction(GtkWidget *self, void *data) +void RGPreferencesWindow::cbDoneAction(GtkWidget *self, void *data) { RGPreferencesWindow *me = (RGPreferencesWindow *) data; - me->saveAction(self, data); - if (me->distroChanged) { - me->hide(); - me->_lister->unregisterObserver(me->_mainWin); - me->_mainWin->setTreeLocked(TRUE); - if (!me->_lister->openCache()) { - me->_mainWin->showErrors(); - exit(1); + start_task([me]() -> task { + co_await me->doneAction(); + }); +} + +task RGPreferencesWindow::doneAction() +{ + co_await saveAction(); + if (distroChanged) { + hide(); + _lister->unregisterObserver(_mainWin); + _mainWin->setTreeLocked(TRUE); + if (!_lister->openCache()) { + co_await _mainWin->showErrors(); + exit(1); } - me->_mainWin->setTreeLocked(FALSE); - me->_lister->registerObserver(me->_mainWin); - me->_mainWin->refreshTable(); + _mainWin->setTreeLocked(FALSE); + _lister->registerObserver(_mainWin); + _mainWin->refreshTable(); } - me->closeAction(self, data); + closeAction(nullptr, this); } -void RGPreferencesWindow::changeFontAction(GtkWidget *self, void *data) +void RGPreferencesWindow::changeDefaultFontAction(GtkWidget *self, void *data) { - const char *fontName, *propName; - - switch (GPOINTER_TO_INT(data)) { - case FONT_DEFAULT: - propName = "Synaptic::FontName"; - fontName = "sans 10"; - break; - case FONT_TERMINAL: - propName = "Synaptic::TerminalFontName"; - fontName = "monospace 10"; - break; - default: - cerr << "changeFontAction called with unknown argument" << endl; - return; - } + auto me = static_cast(data); + start_task([me]() -> task { + co_await me->changeFont("Synaptic::FontName", "sans 10"); + }); +} +void RGPreferencesWindow::changeTerminalFontAction(GtkWidget *self, void *data) +{ + auto me = static_cast(data); + start_task([me]() -> task { + co_await me->changeFont("Synaptic::TerminalFontName", "monospace 10"); + }); +} + +task RGPreferencesWindow::changeFont(const char *propName, const char *defaultValue) +{ GtkWidget *fontsel = gtk_font_chooser_dialog_new(_("Choose font"), - GTK_WINDOW(gtk_widget_get_toplevel(self))); + GTK_WINDOW(gtk_widget_get_toplevel(_win))); + gtk_window_set_modal(GTK_WINDOW(fontsel), true); gtk_font_chooser_set_font(GTK_FONT_CHOOSER(fontsel), - _config->Find(propName, fontName).c_str()); - - gint result = gtk_dialog_run(GTK_DIALOG(fontsel)); - if (result != GTK_RESPONSE_OK) { - gtk_widget_destroy(fontsel); - return; - } + _config->Find(propName, defaultValue).c_str()); - fontName = gtk_font_chooser_get_font(GTK_FONT_CHOOSER(fontsel)); - //cout << "fontname: " << fontName << endl; + int result = co_await co_run_dialog(GTK_DIALOG(fontsel)); + if (result == GTK_RESPONSE_OK) { + auto fontName = gtk_font_chooser_get_font(GTK_FONT_CHOOSER(fontsel)); + //cout << "fontname: " << fontName << endl; _config->Set(propName, fontName); + } gtk_widget_destroy(fontsel); } @@ -914,27 +926,37 @@ void RGPreferencesWindow::cbToggleColumn(GtkWidget *self, char*path_string, } -void RGPreferencesWindow::colorClicked(GtkWidget *self, void *data) +void RGPreferencesWindow::cbColorClicked(GtkWidget *self, void *data) +{ + RGPreferencesWindow *me = (RGPreferencesWindow *) g_object_get_data(G_OBJECT(self), "me"); + int status = GPOINTER_TO_INT(data); + + start_task([me, status]() -> task { + co_await me->colorClicked(status); + }); +} + +task RGPreferencesWindow::colorClicked(int status) { GtkWidget *color_dialog; - RGPreferencesWindow *me; - me = (RGPreferencesWindow *) g_object_get_data(G_OBJECT(self), "me"); color_dialog = gtk_color_chooser_dialog_new(_("Color selection"), - GTK_WINDOW(gtk_builder_get_object(me->_builder, "window_preferences"))); + GTK_WINDOW(window())); + gtk_window_set_modal(GTK_WINDOW(color_dialog), true); gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(color_dialog), false); GdkRGBA *color = NULL; - color = RGPackageStatus::pkgStatus.getColor(GPOINTER_TO_INT(data)); + color = RGPackageStatus::pkgStatus.getColor(status); if (color != NULL) gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(color_dialog), color); - if (gtk_dialog_run(GTK_DIALOG(color_dialog)) == GTK_RESPONSE_OK) { + int response_id = co_await co_run_dialog(GTK_DIALOG(color_dialog)); + if (response_id == GTK_RESPONSE_OK) { GdkRGBA current_color; gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(color_dialog), ¤t_color); - RGPackageStatus::pkgStatus.setColor(GPOINTER_TO_INT(data), + RGPackageStatus::pkgStatus.setColor(status, gdk_rgba_copy(¤t_color)); - me->readColors(); + readColors(); } gtk_widget_destroy(color_dialog); } @@ -1165,10 +1187,10 @@ RGPreferencesWindow::RGPreferencesWindow(RGWindow *win, G_CALLBACK(closeAction), this); g_signal_connect(gtk_builder_get_object(_builder, "apply"), "clicked", - G_CALLBACK(saveAction), this); + G_CALLBACK(cbSaveAction), this); g_signal_connect(gtk_builder_get_object(_builder, "ok"), "clicked", - G_CALLBACK(doneAction), this); + G_CALLBACK(cbDoneAction), this); g_signal_connect(gtk_builder_get_object(_builder, "button_clean_cache"), "clicked", @@ -1184,7 +1206,7 @@ RGPreferencesWindow::RGPreferencesWindow(RGWindow *win, g_signal_connect(gtk_builder_get_object(_builder, "button_default_font"), "clicked", - G_CALLBACK(changeFontAction),GINT_TO_POINTER(FONT_DEFAULT)); + G_CALLBACK(changeDefaultFontAction), this); g_signal_connect(gtk_builder_get_object(_builder, "checkbutton_user_terminal_font"), "toggled", @@ -1195,8 +1217,8 @@ RGPreferencesWindow::RGPreferencesWindow(RGWindow *win, g_signal_connect(gtk_builder_get_object(_builder, "button_terminal_font"), "clicked", - G_CALLBACK(changeFontAction), - GINT_TO_POINTER(FONT_TERMINAL)); + G_CALLBACK(changeTerminalFontAction), + this); checkbuttonUserTerminalFontToggled(NULL, this); checkbuttonUserFontToggled(NULL, this); @@ -1216,7 +1238,7 @@ RGPreferencesWindow::RGPreferencesWindow(RGWindow *win, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_set_data(G_OBJECT(button), "me", this); g_signal_connect(G_OBJECT(button), "clicked", - G_CALLBACK(colorClicked), GINT_TO_POINTER(i)); + G_CALLBACK(cbColorClicked), GINT_TO_POINTER(i)); g_free(color_button); } @@ -1233,30 +1255,31 @@ void RGPreferencesWindow::buttonAuthenticationClicked(GtkWidget *self, void *data) { RGPreferencesWindow *me = (RGPreferencesWindow *)data; - - RGGtkBuilderUserDialog dia(me, "authentication"); - GtkBuilder *dia_xml = dia.getGtkBuilder(); - GtkWidget *entry_user = GTK_WIDGET(gtk_builder_get_object(dia_xml,"entry_username")); - GtkWidget *entry_pass = GTK_WIDGET(gtk_builder_get_object(dia_xml,"entry_password")); - - // now set the values - string now_user = _config->Find("Synaptic::httpProxyUser",""); - gtk_entry_set_text(GTK_ENTRY(entry_user), now_user.c_str()); - string now_pass = _config->Find("Synaptic::httpProxyPass",""); - gtk_entry_set_text(GTK_ENTRY(entry_pass), now_pass.c_str()); - - int res = dia.run(); - - if(!res) - return; - - // get the entered data - const gchar *user = gtk_entry_get_text(GTK_ENTRY(entry_user)); - const gchar *pass = gtk_entry_get_text(GTK_ENTRY(entry_pass)); - - // write out the configuration - _config->Set("Synaptic::httpProxyUser",user); - _config->Set("Synaptic::httpProxyPass",pass); + start_task([me]() -> task { + RGGtkBuilderUserDialog dia(me, "authentication"); + GtkBuilder *dia_xml = dia.getGtkBuilder(); + GtkWidget *entry_user = GTK_WIDGET(gtk_builder_get_object(dia_xml,"entry_username")); + GtkWidget *entry_pass = GTK_WIDGET(gtk_builder_get_object(dia_xml,"entry_password")); + + // now set the values + string now_user = _config->Find("Synaptic::httpProxyUser",""); + gtk_entry_set_text(GTK_ENTRY(entry_user), now_user.c_str()); + string now_pass = _config->Find("Synaptic::httpProxyPass",""); + gtk_entry_set_text(GTK_ENTRY(entry_pass), now_pass.c_str()); + + int res = co_await dia.co_run(); + + if(!res) + co_return; + + // get the entered data + const gchar *user = gtk_entry_get_text(GTK_ENTRY(entry_user)); + const gchar *pass = gtk_entry_get_text(GTK_ENTRY(entry_pass)); + + // write out the configuration + _config->Set("Synaptic::httpProxyUser",user); + _config->Set("Synaptic::httpProxyPass",pass); + }); } // vim:ts=3:sw=3:et diff --git a/gtk/rgpreferenceswindow.h b/gtk/rgpreferenceswindow.h index 302a7b55b..4626a297a 100644 --- a/gtk/rgpreferenceswindow.h +++ b/gtk/rgpreferenceswindow.h @@ -26,6 +26,7 @@ #include "config.h" // IWYU pragma: associated #include "rggtkbuilderwindow.h" +#include "coroutines.h" #include "gtk/gtkcssprovider.h" #include @@ -110,12 +111,15 @@ class RGPreferencesWindow:public RGGtkBuilderWindow { static void cbToggleColumn(GtkWidget *self, char *path, void *data); // callbacks - static void changeFontAction(GtkWidget *self, void *data); + static void changeDefaultFontAction(GtkWidget *self, void *data); + static void changeTerminalFontAction(GtkWidget *self, void *data); + [[nodiscard]] task changeFont(const char *propName, const char *defaultValue); static void checkbuttonUserFontToggled(GtkWidget *self, void *data); static void checkbuttonUserTerminalFontToggled(GtkWidget *self, void *data); - static void saveAction(GtkWidget *self, void *data); + static void cbSaveAction(GtkWidget *self, void *data); + [[nodiscard]] task saveAction(); void saveGeneral(); void saveColumnsAndFonts(); void saveColors(); @@ -131,10 +135,12 @@ class RGPreferencesWindow:public RGGtkBuilderWindow { void readDistribution(); static void closeAction(GtkWidget *self, void *data); - static void doneAction(GtkWidget *self, void *data); + static void cbDoneAction(GtkWidget *self, void *data); + [[nodiscard]] task doneAction(); static void clearCacheAction(GtkWidget *self, void *data); - static void colorClicked(GtkWidget *self, void *data); + static void cbColorClicked(GtkWidget *self, void *data); + [[nodiscard]] task colorClicked(int status); static void buttonAuthenticationClicked(GtkWidget *self, void *data); static void useProxyToggled(GtkWidget *self, void *data); diff --git a/gtk/rgrepositorywin.cc b/gtk/rgrepositorywin.cc index dee11c34f..8e0ce4a19 100644 --- a/gtk/rgrepositorywin.cc +++ b/gtk/rgrepositorywin.cc @@ -37,7 +37,7 @@ #include #include #include -#include +#include #include #include #include @@ -93,42 +93,43 @@ void RGRepositoryEditor::item_toggled(GtkCellRendererToggle *cell, { RGRepositoryEditor *me = (RGRepositoryEditor *)g_object_get_data(G_OBJECT(cell), "me"); GtkTreeModel *model = (GtkTreeModel *) data; - GtkTreePath *path = gtk_tree_path_new_from_string(path_str); - GtkTreeIter iter; - gboolean toggle_item; - gchar *section = NULL; - - /* get toggled iter */ - gtk_tree_model_get_iter(model, &iter, path); - gtk_tree_model_get(model, &iter, - STATUS_COLUMN, &toggle_item, - SECTIONS_COLUMN, §ion, -1); - - /* do something with the value */ - toggle_item ^= 1; - - // special warty check - if(toggle_item && section && g_strrstr(section, "universe")) { - gchar *msg = _("You are adding the \"universe\" component.\n\n " - "Packages in this component are not supported. " - "Are you sure?"); - if(!me->_userDialog->message(msg, RUserDialog::DialogWarning, - RUserDialog::ButtonsOkCancel)) { - gtk_tree_path_free(path); - g_free(section); - return; - } - } - - /* set new value */ - gtk_list_store_set(GTK_LIST_STORE(model), &iter, - STATUS_COLUMN, toggle_item, -1); - - me->_dirty = true; - - /* clean up */ - g_free(section); - gtk_tree_path_free(path); + start_task([me, path_str, model]() -> task { + GtkTreePath *path = gtk_tree_path_new_from_string(path_str); + GtkTreeIter iter; + gboolean toggle_item; + gchar *section = NULL; + + /* get toggled iter */ + gtk_tree_model_get_iter(model, &iter, path); + gtk_tree_model_get(model, &iter, + STATUS_COLUMN, &toggle_item, + SECTIONS_COLUMN, §ion, -1); + + /* do something with the value */ + toggle_item ^= 1; + + // special warty check + if (toggle_item && section && g_strrstr(section, "universe")) { + gchar *msg = _("You are adding the \"universe\" component.\n\n " + "Packages in this component are not supported. " + "Are you sure?"); + if (!co_await me->_userDialog->message(msg, RUserDialog::DialogWarning, RUserDialog::ButtonsOkCancel)) { + gtk_tree_path_free(path); + g_free(section); + co_return; + } + } + + /* set new value */ + gtk_list_store_set(GTK_LIST_STORE(model), &iter, + STATUS_COLUMN, toggle_item, -1); + + me->_dirty = true; + + /* clean up */ + g_free(section); + gtk_tree_path_free(path); + }); } @@ -231,7 +232,7 @@ RGRepositoryEditor::RGRepositoryEditor(RGWindow *parent) select = gtk_tree_view_get_selection(GTK_TREE_VIEW(_sourcesListView)); gtk_tree_selection_set_mode(select, GTK_SELECTION_SINGLE); g_signal_connect(G_OBJECT(select), "changed", - G_CALLBACK(SelectionChanged), this); + G_CALLBACK(cbSelectionChanged), this); //_cbEnabled = GTK_WIDGET(gtk_builder_get_object(_builder, "checkbutton_enabled")); @@ -336,7 +337,7 @@ RGRepositoryEditor::RGRepositoryEditor(RGWindow *parent) g_signal_connect(GTK_WIDGET(gtk_builder_get_object(_builder, "button_ok")), "clicked", - G_CALLBACK(DoOK), this); + G_CALLBACK(cbDoOK), this); g_signal_connect(GTK_WIDGET(gtk_builder_get_object (_builder, "button_cancel")), @@ -401,10 +402,10 @@ RGRepositoryEditor::~RGRepositoryEditor() } -bool RGRepositoryEditor::Run() +task RGRepositoryEditor::Run() { if (_lst.ReadSources() == false) { - _userDialog-> + co_await _userDialog-> warning(_("Ignoring invalid record(s) in sources.list file!")); //return false; } @@ -413,8 +414,8 @@ bool RGRepositoryEditor::Run() if (_lst.ReadVendors() == false) { _error->Error(_("Cannot read vendors.list file")); - _userDialog->showErrors(); - return false; + co_await _userDialog->showErrors(); + co_return false; } GtkTreeIter iter; @@ -446,8 +447,8 @@ bool RGRepositoryEditor::Run() UpdateVendorMenu(); - gtk_main(); - return _applied; + co_await co_run_window(GTK_WINDOW(_win)); + co_return _applied; } void RGRepositoryEditor::UpdateVendorMenu() @@ -530,7 +531,7 @@ void RGRepositoryEditor::DoAdd(GtkWidget *, gpointer data) me->_dirty=true; } -void RGRepositoryEditor::doEdit() +task RGRepositoryEditor::doEdit() { //cout << "RGRepositoryEditor::doEdit()"<Type |= SourcesList::RepomdSrc; break; default: - _userDialog->error(_("Unknown source type")); - return; + co_await _userDialog->error(_("Unknown source type")); + co_return; } #if 0 // PORTME, no vendor id support right now @@ -662,52 +663,66 @@ void RGRepositoryEditor::DoRemove(GtkWidget *, gpointer data) me->_dirty=true; } -void RGRepositoryEditor::DoOK(GtkWidget *, gpointer data) +void RGRepositoryEditor::cbDoOK(GtkWidget *, gpointer data) { RGRepositoryEditor *me = (RGRepositoryEditor *) data; + start_task([me]() -> task { + co_await me->DoOK(); + }); +} - me->doEdit(); - me->_lst.UpdateSources(); +task RGRepositoryEditor::DoOK() +{ + co_await doEdit(); + _lst.UpdateSources(); // check if we actually can parse the sources.list pkgSourceList List; if (!List.ReadMainList()) { - me->_userDialog->showErrors(); - me->_savedList.UpdateSources(); - return; + co_await _userDialog->showErrors(); + _savedList.UpdateSources(); + co_return; } - gtk_main_quit(); - me->_applied = me->_dirty; + _applied = _dirty; + gtk_window_close (GTK_WINDOW (_win)); } void RGRepositoryEditor::DoCancel(GtkWidget *, gpointer data) { - //RGRepositoryEditor *me = (RGRepositoryEditor *)data; - gtk_main_quit(); + RGRepositoryEditor *me = (RGRepositoryEditor *)data; + + gtk_window_close (GTK_WINDOW (me->_win)); } -void RGRepositoryEditor::SelectionChanged(GtkTreeSelection *selection, +void RGRepositoryEditor::cbSelectionChanged(GtkTreeSelection *selection, gpointer data) { RGRepositoryEditor *me = (RGRepositoryEditor *) data; //cout << "RGRepositoryEditor::SelectionChanged()"< task { + co_await me->SelectionChanged(selection); + }); +} + +task RGRepositoryEditor::SelectionChanged(GtkTreeSelection *selection) +{ GtkTreeIter iter; GtkTreeModel *model; - gtk_widget_set_sensitive(me->_editTable, TRUE); + gtk_widget_set_sensitive(_editTable, TRUE); - gtk_widget_set_sensitive(me->_upBut, TRUE); - gtk_widget_set_sensitive(me->_downBut, TRUE); - gtk_widget_set_sensitive(me->_deleteBut, TRUE); + gtk_widget_set_sensitive(_upBut, TRUE); + gtk_widget_set_sensitive(_downBut, TRUE); + gtk_widget_set_sensitive(_deleteBut, TRUE); if (gtk_tree_selection_get_selected(selection, &model, &iter)) { - me->doEdit(); // save the old row - if (me->_lastIter != NULL) - gtk_tree_iter_free(me->_lastIter); - me->_lastIter = gtk_tree_iter_copy(&iter); + co_await doEdit(); // save the old row + if (_lastIter != NULL) + gtk_tree_iter_free(_lastIter); + _lastIter = gtk_tree_iter_copy(&iter); const SourcesList::SourceRecord *rec; gtk_tree_model_get(model, &iter, RECORD_COLUMN, &rec, -1); @@ -727,30 +742,30 @@ void RGRepositoryEditor::SelectionChanged(GtkTreeSelection *selection, id = ITEM_TYPE_REPOMD; else if (rec->Type & SourcesList::RepomdSrc) id = ITEM_TYPE_REPOMDSRC; - gtk_combo_box_set_active(GTK_COMBO_BOX(me->_optType), id); + gtk_combo_box_set_active(GTK_COMBO_BOX(_optType), id); - gtk_combo_box_set_active(GTK_COMBO_BOX(me->_optVendor), - me->VendorMenuIndex(rec->VendorID)); + gtk_combo_box_set_active(GTK_COMBO_BOX(_optVendor), + VendorMenuIndex(rec->VendorID)); - gtk_entry_set_text(GTK_ENTRY(me->_entryURI), utf8(rec->URI.c_str())); - gtk_entry_set_text(GTK_ENTRY(me->_entryDist), utf8(rec->Dist.c_str())); - gtk_entry_set_text(GTK_ENTRY(me->_entrySect), ""); + gtk_entry_set_text(GTK_ENTRY(_entryURI), utf8(rec->URI.c_str())); + gtk_entry_set_text(GTK_ENTRY(_entryDist), utf8(rec->Dist.c_str())); + gtk_entry_set_text(GTK_ENTRY(_entrySect), ""); for (unsigned int I = 0; I < rec->NumSections; I++) { - int pos = gtk_editable_get_position(GTK_EDITABLE(me->_entrySect)); - gtk_editable_insert_text(GTK_EDITABLE(me->_entrySect), + int pos = gtk_editable_get_position(GTK_EDITABLE(_entrySect)); + gtk_editable_insert_text(GTK_EDITABLE(_entrySect), utf8(rec->Sections[I].c_str()), -1, &pos); - gtk_editable_insert_text(GTK_EDITABLE(me->_entrySect), " ", -1, &pos); + gtk_editable_insert_text(GTK_EDITABLE(_entrySect), " ", -1, &pos); } } else { //cout << "no selection" << endl; - gtk_widget_set_sensitive(me->_editTable, FALSE); + gtk_widget_set_sensitive(_editTable, FALSE); - gtk_widget_set_sensitive(me->_upBut, FALSE); - gtk_widget_set_sensitive(me->_downBut, FALSE); - gtk_widget_set_sensitive(me->_deleteBut, FALSE); + gtk_widget_set_sensitive(_upBut, FALSE); + gtk_widget_set_sensitive(_downBut, FALSE); + gtk_widget_set_sensitive(_deleteBut, FALSE); } } diff --git a/gtk/rgrepositorywin.h b/gtk/rgrepositorywin.h index 187f821dc..fe1311c3c 100644 --- a/gtk/rgrepositorywin.h +++ b/gtk/rgrepositorywin.h @@ -30,6 +30,7 @@ #include "rggtkbuilderwindow.h" #include "rsources.h" +#include "coroutines.h" #include #include @@ -43,7 +44,7 @@ class RGWindow; typedef std::list::iterator SourcesListIter; typedef std::list::iterator VendorsListIter; -class RGRepositoryEditor:RGGtkBuilderWindow { +class RGRepositoryEditor : public RGGtkBuilderWindow { SourcesList _lst, _savedList; int _selectedRow; @@ -81,24 +82,26 @@ class RGRepositoryEditor:RGGtkBuilderWindow { static void DoAdd(GtkWidget *, gpointer); static void DoUpDown(GtkWidget *, gpointer); static void DoRemove(GtkWidget *, gpointer); - static void DoOK(GtkWidget *, gpointer); + static void cbDoOK(GtkWidget *, gpointer); + [[nodiscard]] task DoOK(); static void DoCancel(GtkWidget *, gpointer); static void VendorsWindow(GtkWidget *, gpointer); - static void SelectionChanged(GtkTreeSelection *selection, gpointer data); + static void cbSelectionChanged(GtkTreeSelection *selection, gpointer data); + [[nodiscard]] task SelectionChanged(GtkTreeSelection *selection); // treeview item toggled static void item_toggled(GtkCellRendererToggle *cell, gchar *path_str, gpointer data); // get values - void doEdit(); + [[nodiscard]] task doEdit(); public: RGRepositoryEditor(RGWindow *parent); ~RGRepositoryEditor(); - bool Run(); + [[nodiscard]] task Run(); }; #endif diff --git a/gtk/rgsummarywindow.cc b/gtk/rgsummarywindow.cc index e38e9cef5..fce2d6caf 100644 --- a/gtk/rgsummarywindow.cc +++ b/gtk/rgsummarywindow.cc @@ -45,6 +45,7 @@ #include class RGWindow; +#include "rgutils.h" using namespace std; @@ -478,20 +479,20 @@ RGSummaryWindow::RGSummaryWindow(RGWindow *wwin, RPackageLister *lister) skipTaskbar(false); } -bool RGSummaryWindow::showAndConfirm() +task RGSummaryWindow::showAndConfirm() { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(_dlonlyB), _config->FindB("Volatile::Download-Only", false)); - int res = gtk_dialog_run(GTK_DIALOG(_win)); + int res = co_await co_run_dialog(GTK_DIALOG(_win)); bool confirmed = (res == GTK_RESPONSE_APPLY); RGUserDialog userDialog(this); if (confirmed && _potentialBreak - && userDialog.warning(_("Essential packages will be removed.\n" + && co_await userDialog.warning(_("Essential packages will be removed.\n" "This may render your system unusable!\n") , false) == false) - return false; + co_return false; _config->Set("Volatile::Download-Only", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(_dlonlyB)) ? @@ -502,5 +503,5 @@ bool RGSummaryWindow::showAndConfirm() "true" : "false"); #endif - return confirmed; + co_return confirmed; } diff --git a/gtk/rgsummarywindow.h b/gtk/rgsummarywindow.h index f5b636cc9..cff4f2b6b 100644 --- a/gtk/rgsummarywindow.h +++ b/gtk/rgsummarywindow.h @@ -27,6 +27,7 @@ #include "config.h" // IWYU pragma: associated +#include "coroutines.h" #include "rggtkbuilderwindow.h" #include @@ -58,7 +59,7 @@ class RGSummaryWindow:public RGGtkBuilderWindow { public: RGSummaryWindow(RGWindow *win, RPackageLister *lister); - bool showAndConfirm(); + [[nodiscard]] task showAndConfirm(); }; #endif diff --git a/gtk/rgtaskswin.cc b/gtk/rgtaskswin.cc index c415d1aad..772a5d656 100644 --- a/gtk/rgtaskswin.cc +++ b/gtk/rgtaskswin.cc @@ -124,48 +124,49 @@ void RGTasksWin::cbButtonDetailsClicked(GtkWidget *self, void *data) { //cout << "cbButtonDetailsClicked(GtkWidget *self, void *data)"<setBusyCursor(true); - - // get selected task-name - GtkTreeIter iter; - GtkTreeSelection* selection; - selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(me->_taskView)); - if(!gtk_tree_selection_get_selected (selection, - (GtkTreeModel**)(&me->_store), - &iter)) { - return; - } - gchar *str; - gtk_tree_model_get(GTK_TREE_MODEL(me->_store), &iter, - TASK_NAME_COLUMN, &str, -1); - - // ask tasksel about the selected task - string cmd = _config->Find("Synaptic::taskHelperProg", "/usr/bin/tasksel") + " --task-desc " + string(str); - string taskDescr; - char buf[255]; - FILE *f=popen(cmd.c_str(), "r"); - while(fgets(buf, 254, f) != NULL) { - taskDescr += string(buf); - } - - // display the result in a nice dialog - RGGtkBuilderUserDialog dia(me, "task_descr"); - - //TRANSLATORS: Title of the task window - %s is the task (e.g. "desktop" or "mail server") - gchar *title = g_strdup_printf(_("Description %s"), str); - dia.setTitle(title); - - GtkWidget *tv = GTK_WIDGET(gtk_builder_get_object(dia.getGtkBuilder(), - "textview")); - GtkTextBuffer *tb = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); - gtk_text_buffer_set_text(tb, utf8(taskDescr.c_str()), -1); - - dia.run(); - - g_free(str); - g_free(title); - me->setBusyCursor(false); + start_task([me]() -> task { + me->setBusyCursor(true); + + // get selected task-name + GtkTreeIter iter; + GtkTreeSelection* selection; + selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(me->_taskView)); + if(!gtk_tree_selection_get_selected (selection, + (GtkTreeModel**)(&me->_store), + &iter)) { + co_return; + } + gchar *str; + gtk_tree_model_get(GTK_TREE_MODEL(me->_store), &iter, + TASK_NAME_COLUMN, &str, -1); + + // ask tasksel about the selected task + string cmd = _config->Find("Synaptic::taskHelperProg", "/usr/bin/tasksel") + " --task-desc " + string(str); + string taskDescr; + char buf[255]; + FILE *f=popen(cmd.c_str(), "r"); + while(fgets(buf, 254, f) != NULL) { + taskDescr += string(buf); + } + + // display the result in a nice dialog + RGGtkBuilderUserDialog dia(me, "task_descr"); + + //TRANSLATORS: Title of the task window - %s is the task (e.g. "desktop" or "mail server") + gchar *title = g_strdup_printf(_("Description %s"), str); + dia.setTitle(title); + + GtkWidget *tv = GTK_WIDGET(gtk_builder_get_object(dia.getGtkBuilder(), + "textview")); + GtkTextBuffer *tb = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + gtk_text_buffer_set_text(tb, utf8(taskDescr.c_str()), -1); + + co_await dia.co_run(); + + g_free(str); + g_free(title); + me->setBusyCursor(false); + }); } void RGTasksWin::cell_toggled_callback (GtkCellRendererToggle *cell, diff --git a/gtk/rgterminstallprogress.cc b/gtk/rgterminstallprogress.cc index f759daf95..3a0e72494 100644 --- a/gtk/rgterminstallprogress.cc +++ b/gtk/rgterminstallprogress.cc @@ -100,6 +100,9 @@ RGTermInstallProgress::RGTermInstallProgress(RGMainWindow *main) if(_config->FindB("Volatile::Non-Interactive", false)) gtk_widget_hide(_closeOnF); + + g_signal_connect(G_OBJECT(_win), "delete-event", + G_CALLBACK(windowCloseCallback), this); } @@ -113,7 +116,7 @@ void RGTermInstallProgress::child_exited(VteTerminal *vteterminal, me->child_has_exited=true; } -void RGTermInstallProgress::startUpdate() +task RGTermInstallProgress::startUpdate() { GtkWidget *win = GTK_WIDGET(gtk_builder_get_object (_builder, "window_zvtinstallprogress")); @@ -128,14 +131,14 @@ void RGTermInstallProgress::startUpdate() gtk_label_set_markup(GTK_LABEL(_statusL), _("Running...")); gtk_widget_set_sensitive(_closeB, false); - RGFlushInterface(); + co_await RGFlushInterface(); } -void RGTermInstallProgress::finishUpdate() +task RGTermInstallProgress::finishUpdate() { gtk_widget_set_sensitive(_closeB, true); - RGFlushInterface(); + co_await RGFlushInterface(); _updateFinished = true; _config->Set("Synaptic::closeZvt", @@ -145,13 +148,13 @@ void RGTermInstallProgress::finishUpdate() if (!RWriteConfigFile(*_config)) { _error->Error(_("An error occurred while saving configurations.")); RGUserDialog userDialog(this); - userDialog.showErrors(); + co_await userDialog.showErrors(); } if(res == 0 &&(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(_closeOnF)) || _config->FindB("Volatile::Non-Interactive", false))) { hide(); - return; + co_return; } const char *msg = _(getResultStr(res)); @@ -164,25 +167,27 @@ void RGTermInstallProgress::finishUpdate() void RGTermInstallProgress::stopShell(GtkWidget *self, void* data) { RGTermInstallProgress *me = (RGTermInstallProgress*)data; - - if(!me->_updateFinished) { - gtk_label_set_markup(GTK_LABEL(me->_statusL), - _("Can't close while running")); - return; - } - - RGFlushInterface(); - me->hide(); + start_task([me]() -> task { + if (!me->_updateFinished) { + gtk_label_set_markup(GTK_LABEL(me->_statusL), + _("Can't close while running")); + co_return; + } + + co_await RGFlushInterface(); + me->hide(); + }); } -bool RGTermInstallProgress::close() +gboolean RGTermInstallProgress::windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data) { - stopShell(NULL, this); + auto me = static_cast(data); + stopShell(NULL, me); return true; } -pkgPackageManager::OrderResult +task RGTermInstallProgress::start(pkgPackageManager *pm, int numPackages, int numPackagesTotal) @@ -194,7 +199,7 @@ RGTermInstallProgress::start(pkgPackageManager *pm, res = pm->DoInstallPreFork(); if (res == pkgPackageManager::Failed) - return res; + co_return res; int master; _child_id = forkpty(&master, NULL, NULL, NULL); @@ -220,7 +225,7 @@ RGTermInstallProgress::start(pkgPackageManager *pm, if (err != NULL) { std::cerr << "failed to create new pty: " << err->message << std::endl; g_error_free (err); - return pkgPackageManager::Failed; + co_return pkgPackageManager::Failed; } vte_terminal_set_pty(VTE_TERMINAL(_term), pty); @@ -228,27 +233,22 @@ RGTermInstallProgress::start(pkgPackageManager *pm, // we can set it? vte_terminal_watch_child(VTE_TERMINAL(_term), _child_id); - startUpdate(); + co_await startUpdate(); // make sure that the child has really exited and we catched the // return code while(!child_has_exited) - updateInterface(); + co_await updateInterface(); - finishUpdate(); + co_await finishUpdate(); ::close(master); - return res; + co_return res; } -void RGTermInstallProgress::updateInterface() -{ - if (gtk_events_pending()) { - while (gtk_events_pending()) gtk_main_iteration(); - } else { - // 0.1 secs - usleep(10000); - } +task RGTermInstallProgress::updateInterface() +{ + co_await RGFlushInterface(); } diff --git a/gtk/rgterminstallprogress.h b/gtk/rgterminstallprogress.h index 67ed0d5d4..bb4370464 100644 --- a/gtk/rgterminstallprogress.h +++ b/gtk/rgterminstallprogress.h @@ -51,11 +51,11 @@ class RGTermInstallProgress : public RInstallProgress, public RGGtkBuilderWindow bool child_has_exited; static void child_exited(VteTerminal *vteterminal, gint ret, gpointer data); - virtual void startUpdate(); - virtual void updateInterface(); - virtual void finishUpdate(); + [[nodiscard]] virtual task startUpdate(); + [[nodiscard]] virtual task updateInterface(); + [[nodiscard]] virtual task finishUpdate(); static void stopShell(GtkWidget *self, void* data); - virtual bool close(); + static gboolean windowCloseCallback(GtkWidget *window, GdkEvent *event, gpointer data); pid_t _child_id; @@ -63,7 +63,7 @@ class RGTermInstallProgress : public RInstallProgress, public RGGtkBuilderWindow RGTermInstallProgress(RGMainWindow *main); ~RGTermInstallProgress() {}; - virtual pkgPackageManager::OrderResult start(pkgPackageManager *pm, + [[nodiscard]] virtual task start(pkgPackageManager *pm, int numPackages = 0, int totalPackages = 0); diff --git a/gtk/rguserdialog.cc b/gtk/rguserdialog.cc index fa85163a6..c2408c1db 100644 --- a/gtk/rguserdialog.cc +++ b/gtk/rguserdialog.cc @@ -46,19 +46,13 @@ using namespace std; -static void actionResponse(GtkDialog *dialog, gint id, gpointer user_data) -{ - GtkResponseType *res = (GtkResponseType *) user_data; - *res = (GtkResponseType) id; -} - -bool RGUserDialog::showErrors() +task RGUserDialog::showErrors() { GtkWidget *dia; std::string err,warn; if (_error->empty()) - return false; + co_return false; while (!_error->empty()) { string message; @@ -128,19 +122,18 @@ bool RGUserDialog::showErrors() } } - gtk_dialog_run(GTK_DIALOG(dia)); + co_await co_run_dialog(GTK_DIALOG(dia)); gtk_widget_destroy(dia); - return true; + co_return true; } -bool RGUserDialog::message(const char *msg, +task RGUserDialog::message(const char *msg, RUserDialog::DialogType dialog, RUserDialog::ButtonsType buttons, bool defaultResponse) { GtkWidget *dia; - GtkResponseType res; GtkMessageType gtkmessage; GtkButtonsType gtkbuttons; @@ -200,9 +193,6 @@ bool RGUserDialog::message(const char *msg, } } - g_signal_connect(G_OBJECT(dia), "response", - G_CALLBACK(actionResponse), (gpointer) & res); - // honor foreign parent windows (to make embedding easy) int id = _config->FindI("Volatile::ParentWindowId", -1); if (id > 0) { @@ -214,9 +204,9 @@ bool RGUserDialog::message(const char *msg, } } - gtk_dialog_run(GTK_DIALOG(dia)); + auto res = co_await co_run_dialog(GTK_DIALOG(dia)); gtk_widget_destroy(dia); - return (res == GTK_RESPONSE_OK) || (res == GTK_RESPONSE_YES) || (res == GTK_RESPONSE_CLOSE); + co_return (res == GTK_RESPONSE_OK) || (res == GTK_RESPONSE_YES) || (res == GTK_RESPONSE_CLOSE); } // RGGtkBuilderUserDialog @@ -273,18 +263,18 @@ void RGGtkBuilderUserDialog::init(const char *name) g_free(main_widget); } -int RGGtkBuilderUserDialog::run(const char *name, bool return_gtk_response) +task RGGtkBuilderUserDialog::co_run(const char *name, bool return_gtk_response) { if(name != NULL) init(name); - res = (GtkResponseType) gtk_dialog_run(GTK_DIALOG(_dialog)); + res = (GtkResponseType) co_await co_run_dialog(GTK_DIALOG(_dialog)); gtk_widget_hide(_dialog); if(return_gtk_response) - return res; + co_return res; else - return (res == GTK_RESPONSE_OK) || (res == GTK_RESPONSE_YES) || (res == GTK_RESPONSE_CLOSE); + co_return (res == GTK_RESPONSE_OK) || (res == GTK_RESPONSE_YES) || (res == GTK_RESPONSE_CLOSE); } diff --git a/gtk/rguserdialog.h b/gtk/rguserdialog.h index f82a014cd..8b2ab065c 100644 --- a/gtk/rguserdialog.h +++ b/gtk/rguserdialog.h @@ -28,6 +28,7 @@ #include "config.h" // IWYU pragma: associated #include "rgwindow.h" +#include "rgutils.h" #include "ruserdialog.h" #include @@ -46,9 +47,9 @@ class RGUserDialog : public RUserDialog RGUserDialog(RGWindow *parent) : _parentWindow(parent->window()) {}; RGUserDialog(GtkWidget *parent) : _parentWindow(parent) {}; - virtual bool showErrors(); + [[nodiscard]] virtual task showErrors(); - virtual bool message(const char *msg, + [[nodiscard]] virtual task message(const char *msg, RUserDialog::DialogType dialog=RUserDialog::DialogInfo, RUserDialog::ButtonsType buttons=RUserDialog::ButtonsOk, bool defaultResponse=true); @@ -90,7 +91,7 @@ class RGGtkBuilderUserDialog : public RGUserDialog gtk_window_set_title(GTK_WINDOW(_dialog),title.c_str()); } - int run(const char *name=NULL, bool return_gtk_response=false); + [[nodiscard]] task co_run(const char *name=NULL, bool return_gtk_response=false); GtkBuilder *getGtkBuilder() { return builder; }; }; #endif diff --git a/gtk/rgutils.cc b/gtk/rgutils.cc index e94ff0ab4..7d01e99dd 100644 --- a/gtk/rgutils.cc +++ b/gtk/rgutils.cc @@ -64,11 +64,9 @@ GtkWidget *get_gtk_image(const gchar *name, int size) return gtk_image_new_from_pixbuf(buf); } -void RGFlushInterface() +task RGFlushInterface() { - while (gtk_events_pending()) { - gtk_main_iteration(); - } + co_await glib_idle {}; } static void AddRun0Environment(std::vector &prefix) @@ -337,4 +335,59 @@ const char *utf8(const char *str) } return escaped; } + +struct DialogResponseClosure +{ + int *response; + std::coroutine_handle<> h; +}; + +static void dialog_response_callback(GtkDialog *dialog, int response_id, gpointer data) +{ + g_signal_handlers_disconnect_by_data(dialog, data); + + auto *closure = static_cast(data); + *closure->response = response_id; + if (closure->h) { + closure->h.resume(); + closure->h = {}; + } +} + +void dialog_response::await_suspend(std::coroutine_handle<> h) +{ + g_signal_connect_data( + dialog, + "response", + G_CALLBACK(dialog_response_callback), + new DialogResponseClosure { response, h }, + [](gpointer data, GClosure *) { + delete static_cast(data); + }, + G_CONNECT_DEFAULT); +} + +dialog_response co_run_dialog(GtkDialog *dialog) +{ + gtk_window_present(GTK_WINDOW(dialog)); + return dialog_response { dialog }; +} + +static void window_hide_callback(GtkWindow *window, gpointer data) +{ + g_signal_handlers_disconnect_by_data(window, data); + if (auto h = std::coroutine_handle<>::from_address(data)) + h.resume(); +} + +void window_hide::await_suspend(std::coroutine_handle<> h) { + g_signal_connect(window, "hide", G_CALLBACK(window_hide_callback), h.address()); +} + +window_hide co_run_window(GtkWindow *window) +{ + gtk_window_present(window); + return window_hide { window }; +} + // vim:ts=3:sw=3:et diff --git a/gtk/rgutils.h b/gtk/rgutils.h index c0c64362d..8287d5007 100644 --- a/gtk/rgutils.h +++ b/gtk/rgutils.h @@ -31,6 +31,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include "coroutines.h" enum { PIXMAP_COLUMN, @@ -48,7 +55,7 @@ enum { N_COLUMNS }; -void RGFlushInterface(); +[[nodiscard]] task RGFlushInterface(); bool is_binary_in_path(const char *program); @@ -67,4 +74,161 @@ bool RunAsSudoUserCommand(std::vector cmd); std::string MarkupEscapeString(std::string str); std::string MarkupUnescapeString(std::string str); +struct [[nodiscard]] sleep_ms { + int ms; + + bool await_ready() const noexcept { return false; } + + void await_suspend(std::coroutine_handle<> h) + { + g_timeout_add_once( + ms, + [](gpointer data) { + auto h = std::coroutine_handle<>::from_address(data); + h.resume(); + }, + h.address() + ); + } + + void await_resume() const noexcept {} +}; + +struct [[nodiscard]] glib_idle { + bool await_ready() const noexcept { return false; } + + void await_suspend(std::coroutine_handle<> h) + { + g_idle_add_once( + [](gpointer data) { + auto h = std::coroutine_handle<>::from_address(data); + h.resume(); + }, + h.address() + ); + } + + void await_resume() const noexcept {} +}; + +struct [[nodiscard]] dialog_response { + GtkDialog *dialog; + int *response; + + explicit dialog_response(GtkDialog *dialog) + : dialog(dialog), response(new int { -1 }) {} + + bool await_ready() const { return false; } + void await_suspend(std::coroutine_handle<> h); + int await_resume() const noexcept { return *response; } +}; + +[[nodiscard]] dialog_response co_run_dialog(GtkDialog *dialog); + +struct [[nodiscard]] window_hide { + GtkWindow *window; + + bool await_ready() const { return false; } + void await_suspend(std::coroutine_handle<> h); + void await_resume() const noexcept { } +}; + +[[nodiscard]] window_hide co_run_window(GtkWindow *window); + +struct [[nodiscard]] glib_scheduler { + bool await_ready() const noexcept { return false; } + + void await_suspend(std::coroutine_handle<> h) { + g_main_context_invoke( + g_main_context_default(), + [](gpointer data) -> gboolean { + auto h = std::coroutine_handle<>::from_address(data); + h.resume(); + return G_SOURCE_REMOVE; + }, + h.address() + ); + } + + void await_resume() const noexcept {} +}; + +template +void destroy_later(T *&obj) noexcept { + if (!obj) + return; + T *ptr = obj; + obj = nullptr; + g_idle_add_once( + [](gpointer data) { + delete static_cast(data); + }, + ptr); +} + +struct DetachedTaskOperationBase { + virtual ~DetachedTaskOperationBase() = default; +}; + +struct DetachedTaskReceiver { + using receiver_concept = beman::execution::receiver_tag; + + DetachedTaskOperationBase *operation = nullptr; + + struct Env { + auto query(beman::execution::get_scheduler_t) const noexcept { + return beman::execution::inline_scheduler {}; + } + }; + + auto get_env() const noexcept { + return Env {}; + } + + void set_value() && noexcept { + destroy_later(operation); + } + + template + void set_error(Error&&) && noexcept { + destroy_later(operation); + } + + void set_stopped() && noexcept { + destroy_later(operation); + } +}; + +template +struct DetachedTaskOperation : DetachedTaskOperationBase { + std::move_only_function()> *func; + Operation operation; + + explicit DetachedTaskOperation(std::move_only_function()> *func) + : func(func), operation(beman::execution::connect((*func)(), DetachedTaskReceiver { this })) {} + + ~DetachedTaskOperation() { + delete func; + } + + void start() noexcept { + beman::execution::start(operation); + } +}; + +template +void start_task(F&& f) +{ + auto *ff = new std::move_only_function()> { + [fn = std::forward(f)]() -> task { + co_await glib_scheduler {}; + co_await fn(); + } + }; + + using Operation = decltype(beman::execution::connect(std::move((*ff)()), DetachedTaskReceiver {})); + auto *operation = new DetachedTaskOperation(ff); + operation->start(); +} + #endif diff --git a/gtk/rgwindow.cc b/gtk/rgwindow.cc index 5b33b9535..1b7f8940c 100644 --- a/gtk/rgwindow.cc +++ b/gtk/rgwindow.cc @@ -36,14 +36,6 @@ using namespace std; -bool RGWindow::windowCloseCallback(GtkWidget *window, GdkEvent * event) -{ - //cout << "windowCloseCallback" << endl; - RGWindow *rwin = (RGWindow *) g_object_get_data(G_OBJECT(window), "me"); - - return rwin->close(); -} - RGWindow::RGWindow(string name, bool makeBox) { //std::cout << "RGWindow::RGWindow(string name, bool makeBox)" << endl; @@ -53,8 +45,6 @@ RGWindow::RGWindow(string name, bool makeBox) gtk_window_set_icon(GTK_WINDOW(_win), icon); g_object_set_data(G_OBJECT(_win), "me", this); - g_signal_connect(G_OBJECT(_win), "delete-event", - G_CALLBACK(windowCloseCallback), this); if (makeBox) { _topBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); @@ -80,9 +70,6 @@ RGWindow::RGWindow(RGWindow *parent, string name, bool makeBox, bool closable) g_object_set_data(G_OBJECT(_win), "me", this); - g_signal_connect(G_OBJECT(_win), "delete-event", - G_CALLBACK(windowCloseCallback), this); - if (makeBox) { _topBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(_win), _topBox); @@ -111,11 +98,4 @@ void RGWindow::setTitle(string title) gtk_window_set_title(GTK_WINDOW(_win), (char *)title.c_str()); } -bool RGWindow::close() -{ - //cout << "RGWindow::close()" << endl; - hide(); - return true; -} - // vim:ts=3:sw=3:et diff --git a/gtk/rgwindow.h b/gtk/rgwindow.h index c072a3aee..631f154e1 100644 --- a/gtk/rgwindow.h +++ b/gtk/rgwindow.h @@ -36,9 +36,6 @@ class RGWindow { GtkWidget *_win; GtkWidget *_topBox; - static bool windowCloseCallback(GtkWidget *widget, GdkEvent *event); - virtual bool close(); - public: inline virtual GtkWidget *window() { return _win; diff --git a/meson.build b/meson.build index 8bb24a48d..370ca6a36 100644 --- a/meson.build +++ b/meson.build @@ -4,12 +4,15 @@ project( version: '0.91.7', default_options: [ 'warning_level=1', - 'cpp_std=gnu++17', + 'cpp_std=c++23', ], ) +add_project_arguments('-fcoroutines', language: ['cpp']) + i18n = import('i18n') gnome = import('gnome') +cmake = import('cmake') prefix = get_option('prefix') datadir = get_option('datadir') @@ -30,9 +33,15 @@ root_inc = include_directories('.') common_inc = include_directories('common') gtk_inc = include_directories('gtk') +task_options = cmake.subproject_options() +task_options.add_cmake_defines({'BEMAN_USE_MODULES': 'OFF'}) +task = cmake.subproject('task', options: task_options) +task_dep = task.get_variable('beman_task_dep') + threads_dep = dependency('threads') gtk_dep = dependency('gtk+-3.0', version: '>= 3.4.0') pango_dep = dependency('pango', version: '>= 1.0.0') +tbb_dep = dependency('tbb') glib_dep = dependency('glib-2.0') apt_pkg_dep = dependency('apt-pkg', required: true) x11_dep = cpp.find_library('X11', required: true) @@ -171,7 +180,7 @@ gtk_cpp_args = [ '-DGDK_PIXBUF_DISABLE_DEPRECATED', ] -common_deps = [apt_pkg_dep] +common_deps = [apt_pkg_dep, task_dep] common_deps += rpm_deps if apt_inst_dep.found() common_deps += [apt_inst_dep] @@ -181,6 +190,8 @@ if have_xapian endif gui_deps = [ + task_dep, + tbb_dep, gtk_dep, pango_dep, glib_dep, diff --git a/subprojects/.wraplock b/subprojects/.wraplock new file mode 100644 index 000000000..e69de29bb diff --git a/subprojects/task.wrap b/subprojects/task.wrap new file mode 100644 index 000000000..02aad0c23 --- /dev/null +++ b/subprojects/task.wrap @@ -0,0 +1,9 @@ +[wrap-git] +directory = task +url = https://github.com/bemanproject/task.git +revision = 167918522e1cc4bc44c04f97b204f46c1735d7a2 +depth = 1 +method = cmake + +[provide] +task = task_dep diff --git a/subprojects/task/.clang-format b/subprojects/task/.clang-format new file mode 100644 index 000000000..1fbd05eff --- /dev/null +++ b/subprojects/task/.clang-format @@ -0,0 +1,251 @@ +--- +BasedOnStyle: LLVM +Language: Json +IndentWidth: 2 +UseTab: Never +--- +Language: JavaScript +IndentWidth: 4 +UseTab: Never +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: true + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveDeclarations: + Enabled: true + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: true +AlignConsecutiveMacros: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: false +AlignConsecutiveShortCaseStatements: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCaseColons: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: + Kind: Always + OverEmptyLines: 0 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +AttributeMacros: + - __capability +BinPackArguments: false +BinPackParameters: false +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakAfterAttributes: Never +BreakAfterJavaFieldAnnotations: false +BreakArrays: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: Always +BreakBeforeBraces: Custom +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +# Please update .markdownlint.yaml if this line is to be updated +ColumnLimit: 119 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: false +IndentExternBlock: AfterExternBlock +IndentGotoLabels: true +IndentPPDirectives: None +IndentRequiresClause: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertBraces: false +InsertNewlineAtEOF: false +InsertTrailingCommas: None +IntegerLiteralSeparator: + Binary: 0 + BinaryMinDigits: 0 + Decimal: 0 + DecimalMinDigits: 0 + Hex: 0 + HexMinDigits: 0 +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +KeepEmptyLinesAtEOF: false +LambdaBodyIndentation: Signature +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 4 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PackConstructorInitializers: NextLine +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +PPIndentWidth: -1 +QualifierAlignment: Custom +QualifierOrder: + - inline + - static + - constexpr + - const + - volatile + - type +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RemoveParentheses: Leave +RemoveSemicolon: false +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SortIncludes: Never +SortJavaStaticImport: Before +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeJsonColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInContainerLiterals: true +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParens: Never +SpacesInParensOptions: + InCStyleCasts: false + InConditionalStatements: false + InEmptyParentheses: false + Other: false +SpacesInSquareBrackets: false +Standard: Auto +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseTab: Never +VerilogBreakBetweenInstancePorts: true +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... diff --git a/subprojects/task/.clang-format-ignore b/subprojects/task/.clang-format-ignore new file mode 100644 index 000000000..a6c57f5fb --- /dev/null +++ b/subprojects/task/.clang-format-ignore @@ -0,0 +1 @@ +*.json diff --git a/subprojects/task/.clang-tidy b/subprojects/task/.clang-tidy new file mode 100644 index 000000000..d85365769 --- /dev/null +++ b/subprojects/task/.clang-tidy @@ -0,0 +1,55 @@ +Checks: + -*, + boost-*, + bugprone-*, + cert-*, + -cert-dcl58-cpp, + clang-analyzer-*, + concurrency-*, + -cppcoreguidelines-*, + -google-*, + hicpp-*, + misc-*, + -misc-const-correctness, + -misc-include-cleaner, + -misc-non-private-member-variables-in-classes, + -modernize-*, + -modernize-use-nodiscard, + performance-*, + portability-*, + -readability-*, + -readability-identifier-*, + -readability-implicit-bool-conversion, + -readability-magic-numbers, + -*-named-parameter, + -*-uppercase-literal-suffix, + -*-use-equals-default, + -*-braces-around-statements +HeaderFilterRegex: '.*/execution/(include|src|example|tests)/.*\.(hpp)$' +WarningsAsErrors: 'clang*' +FormatStyle: file + +CheckOptions: + - { key: readability-identifier-naming.NamespaceCase, value: CamelCase } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.EnumCase, value: CamelCase } + - { key: readability-identifier-naming.MemberCase, value: camelBack } + - { key: readability-identifier-naming.MemberPrefix, value: m_ } + - { key: readability-identifier-naming.StructCase, value: lower_case } + - { key: readability-identifier-naming.UnionCase, value: lower_case } + - { key: readability-identifier-naming.TypedefCase, value: lower_case } + - { key: readability-identifier-naming.TypedefSuffix, value: _type } + - { key: readability-identifier-naming.FunctionCase, value: camelBack } + - { key: readability-identifier-naming.VariableCase, value: camelBack } + - { key: readability-identifier-naming.ParameterCase, value: camelBack } + - { key: readability-identifier-naming.LocalVariableCase, value: camelBack } + - { key: readability-identifier-naming.ConstexprFunctionCase, value: camelBack } + - { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack } + - { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE } + - { key: readability-identifier-naming.ClassConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.GlobalConstantPointerCase, value: UPPER_CASE } + - { key: readability-identifier-naming.LocalConstantPointerCase, value: UPPER_CASE } + - { key: readability-identifier-naming.ScopedEnumConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE } diff --git a/subprojects/task/.codespellignore b/subprojects/task/.codespellignore new file mode 100644 index 000000000..3f49838ef --- /dev/null +++ b/subprojects/task/.codespellignore @@ -0,0 +1,7 @@ +que +cancelled +copyable +pullrequest +snd +statics +Claus diff --git a/subprojects/task/.codespellrc b/subprojects/task/.codespellrc new file mode 100644 index 000000000..31a9163ab --- /dev/null +++ b/subprojects/task/.codespellrc @@ -0,0 +1,6 @@ +[codespell] +builtin = clear,rare,en-GB_to_en-US,names,informal,code +check-hidden = +skip = ./.git,./build/*,./stagedir/*,./docs/html/*,./docs/latex/*,*.log,.*.swp,*~,*.bak,Makefile +quiet-level = 2 +ignore-words = .codespellignore diff --git a/subprojects/task/.devcontainer/Dockerfile b/subprojects/task/.devcontainer/Dockerfile new file mode 100644 index 000000000..584eb2dbc --- /dev/null +++ b/subprojects/task/.devcontainer/Dockerfile @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-22.04 + +USER vscode + +# Install latest cmake +RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null +RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null +RUN sudo apt-get update && sudo apt-get install -y cmake + +# Install pre-commit +RUN sudo apt-get install -y python3-pip && pip3 install pre-commit + +# Avoid ASAN Stalling +# See: https://github.com/google/sanitizers/issues/1614 +# Alternative is to update to above clang-18 and gcc-13.2 +# Maybe crashing codespace??? +RUN sudo sysctl -w vm.mmap_rnd_bits=28 diff --git a/subprojects/task/.devcontainer/devcontainer.json b/subprojects/task/.devcontainer/devcontainer.json new file mode 100644 index 000000000..e0cbb9cf5 --- /dev/null +++ b/subprojects/task/.devcontainer/devcontainer.json @@ -0,0 +1,15 @@ +{ + "name": "Beman Project Generic Devcontainer", + "build": { + "dockerfile": "Dockerfile" + }, + "postCreateCommand": "bash .devcontainer/postcreate.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools" + ] + } + } +} diff --git a/subprojects/task/.devcontainer/postcreate.sh b/subprojects/task/.devcontainer/postcreate.sh new file mode 100644 index 000000000..4293f7e4d --- /dev/null +++ b/subprojects/task/.devcontainer/postcreate.sh @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# Setup pre-commit +pre-commit +pre-commit install diff --git a/subprojects/task/.github/CODEOWNERS b/subprojects/task/.github/CODEOWNERS new file mode 100644 index 000000000..0fc077b9c --- /dev/null +++ b/subprojects/task/.github/CODEOWNERS @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# Codeowners for reviews on PRs + +# Github Actions: +# .github/workflows/ @wusatosi # Add other project owners here +# +# Devcontainer: +# .devcontainer/ @wusatosi # Add other project owners here +# +# Pre-commit: +# .pre-commit-config.yaml @wusatosi # Add other project owners here +# .markdownlint.yaml @wusatosi # Add other project owners here + +* @bretbrownjr @dietmarkuehl @steve-downey @wusatosi diff --git a/subprojects/task/.github/workflows/ci_tests.yml b/subprojects/task/.github/workflows/ci_tests.yml new file mode 100644 index 000000000..df6ed072c --- /dev/null +++ b/subprojects/task/.github/workflows/ci_tests.yml @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: Continuous Integration Tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + schedule: + - cron: '30 15 * * *' + +jobs: + beman-submodule-check: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.3.0 + + preset-test: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.3.0 + with: + matrix_config: > + [ + {"preset": "gcc-debug", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "llvm-debug", "image": "ghcr.io/bemanproject/infra-containers-clang:21"}, + {"preset": "llvm-release", "image": "ghcr.io/bemanproject/infra-containers-clang:21"}, + {"preset": "msvc-debug", "runner": "windows-latest"}, + {"preset": "msvc-release", "runner": "windows-latest"} + ] + + build-and-test: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.3.0 + with: + matrix_config: > + { + "gcc": [ + { "versions": ["15"], + "tests": [ + { "cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.MaxSan", + "Debug.Coverage", "Debug.Werror", + "Debug.Dynamic", "Release.Dynamic" + ] + } + ] + }, + { "cxxversions": ["c++23"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + }, + { "versions": ["14"], + "tests": [ + { "cxxversions": ["c++26", "c++23"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "clang": [ + { "versions": ["21"], + "tests": [ + {"cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.MaxSan", + "Debug.Dynamic", "Release.Dynamic" + ] + } + ] + }, + { "cxxversions": ["c++23"], + "tests": [ + {"stdlibs": ["libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["20", "19"], + "tests": [ + { "cxxversions": ["c++26", "c++23"], + "tests": [ + {"stdlibs": ["libc++"], "tests": ["Release.Default"]} + ] + } + ] + } + ], + "msvc": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++23"], + "tests": [ + { "stdlibs": ["stl"], + "tests": ["Debug.Default", "Release.Default", "Release.MaxSan", + "Debug.Dynamic", "Release.Dynamic" + ] + } + ] + } + ] + } + ] + } + + create-issue-when-fault: + needs: [preset-test, build-and-test] + if: failure() && github.event_name == 'schedule' + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.3.0 diff --git a/subprojects/task/.github/workflows/pre-commit-check.yml b/subprojects/task/.github/workflows/pre-commit-check.yml new file mode 100644 index 000000000..2f9110380 --- /dev/null +++ b/subprojects/task/.github/workflows/pre-commit-check.yml @@ -0,0 +1,13 @@ +name: Lint Check (pre-commit) + +on: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: + push: + branches: + - main + +jobs: + pre-commit: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-pre-commit.yml@1.3.0 diff --git a/subprojects/task/.github/workflows/pre-commit-update.yml b/subprojects/task/.github/workflows/pre-commit-update.yml new file mode 100644 index 000000000..930b750c1 --- /dev/null +++ b/subprojects/task/.github/workflows/pre-commit-update.yml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: Weekly pre-commit autoupdate + +on: + workflow_dispatch: + schedule: + - cron: "0 16 * * 0" + +jobs: + auto-update-pre-commit: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.3.0 + secrets: + APP_ID: ${{ secrets.AUTO_PR_BOT_APP_ID }} + PRIVATE_KEY: ${{ secrets.AUTO_PR_BOT_PRIVATE_KEY }} diff --git a/subprojects/task/.github/workflows/pre-commit.yml b/subprojects/task/.github/workflows/pre-commit.yml new file mode 100644 index 000000000..2409d2f44 --- /dev/null +++ b/subprojects/task/.github/workflows/pre-commit.yml @@ -0,0 +1,13 @@ +name: Lint Check (pre-commit) + +on: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: + push: + branches: + - main + +jobs: + pre-commit: + uses: ./.github/workflows/reusable-beman-pre-commit.yml diff --git a/subprojects/task/.gitignore b/subprojects/task/.gitignore new file mode 100644 index 000000000..9c3b70cc6 --- /dev/null +++ b/subprojects/task/.gitignore @@ -0,0 +1,5 @@ +/.cache +/compile_commands.json +build/ +/docs/generated +/docs/wg21 diff --git a/subprojects/task/.meson-subproject-wrap-hash.txt b/subprojects/task/.meson-subproject-wrap-hash.txt new file mode 100644 index 000000000..1c8612e0e --- /dev/null +++ b/subprojects/task/.meson-subproject-wrap-hash.txt @@ -0,0 +1 @@ +ff4fd74fe71e4a2cbd49e972d7a3501849a474cfb410f9f06a29ecb1a6589196 diff --git a/subprojects/task/.pre-commit-config.yaml b/subprojects/task/.pre-commit-config.yaml new file mode 100644 index 000000000..5d28bd462 --- /dev/null +++ b/subprojects/task/.pre-commit-config.yaml @@ -0,0 +1,46 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + exclude: ^\.clang-(format|tidy)$ + - id: check-added-large-files + + # Config file: .codespellrc + - repo: https://github.com/codespell-project/codespell + rev: v2.4.2 + hooks: + - id: codespell + files: ^.*\.(cmake|cpp|cppm|hpp|txt|md|mds|json|js|in|yaml|yml|py|toml)$ + args: ["--write", "--ignore-words", ".codespellignore" ] + + - repo: https://github.com/bemanproject/beman-tidy + rev: v0.4.0 + hooks: + - id: beman-tidy + + # Clang-format for C++ + # This brings in a portable version of clang-format. + # See also: https://github.com/ssciwr/clang-format-wheel + # Config file: .clang-format + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v22.1.4 + hooks: + - id: clang-format + types_or: [c++, c, json, javascript] + exclude: docs/TODO.json + + # CMake linting and formatting + - repo: https://github.com/BlankSpruce/gersemi-pre-commit + rev: 0.27.2 + hooks: + - id: gersemi + name: CMake linting + exclude: ^.*/tests/.*/data/ # Exclude test data directories + +exclude: 'infra/' diff --git a/subprojects/task/CMakeLists.txt b/subprojects/task/CMakeLists.txt new file mode 100644 index 000000000..2c3f3c414 --- /dev/null +++ b/subprojects/task/CMakeLists.txt @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.30...4.3) + +project( + beman.task # CMake Project Name, which is also the name of the top-level + # targets (e.g., library, executable, etc.). + DESCRIPTION "A Beman library task (coroutine task)" + LANGUAGES CXX + VERSION 0.2.0 +) + +# [CMAKE.SKIP_TESTS] +option( + BEMAN_TASK_BUILD_TESTS + "Enable building tests and test infrastructure. Default: ON. Values: { ON, OFF }." + ${PROJECT_IS_TOP_LEVEL} +) + +# [CMAKE.SKIP_EXAMPLES] +option( + BEMAN_TASK_BUILD_EXAMPLES + "Enable building examples. Default: ON. Values: { ON, OFF }." + ${PROJECT_IS_TOP_LEVEL} +) + +include(FetchContent) + +FetchContent_Declare( + execution + # FETCHCONTENT_SOURCE_DIR_EXECUTION ${CMAKE_SOURCE_DIR}/../execution + GIT_REPOSITORY https://github.com/bemanproject/execution + GIT_TAG 7df0f75 + SYSTEM + FIND_PACKAGE_ARGS + 0.2.0 + EXACT + NAMES + beman.execution + COMPONENTS + execution_headers +) +FetchContent_MakeAvailable(execution) + +add_subdirectory(src/beman/task) + +#=============================================================================== +# NOTE: this must be done before tests! CK +include(infra/cmake/beman-install-library.cmake) +beman_install_library(${PROJECT_NAME} TARGETS ${PROJECT_NAME} beman.task_headers + DEPENDENCIES [===[beman.execution 0.2.0 EXACT]===] +) +#=============================================================================== + +enable_testing() + +if(BEMAN_TASK_BUILD_TESTS) + add_subdirectory(tests/beman/task) +endif() + +if(BEMAN_TASK_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/subprojects/task/CMakePresets.json b/subprojects/task/CMakePresets.json new file mode 100644 index 000000000..5f09777fa --- /dev/null +++ b/subprojects/task/CMakePresets.json @@ -0,0 +1,413 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "_root-config", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "BEMAN_USE_MODULES": false, + "BEMAN_USE_STD_MODULE": false, + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_EXTENSIONS": true, + "CMAKE_CXX_STANDARD_REQUIRED": true, + "CMAKE_EXPORT_COMPILE_COMMANDS": true, + "CMAKE_INSTALL_MESSAGE": "LAZY", + "CMAKE_SKIP_TEST_ALL_DEPENDENCY": false, + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "infra/cmake/use-fetch-content.cmake" + } + }, + { + "name": "_debug-base", + "hidden": true, + "warnings": { + "dev": true, + "deprecated": true, + "uninitialized": true, + "unusedCli": true, + "systemVars": false + }, + "errors": { + "dev": false, + "deprecated": false + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "_release-base", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "gcc-debug", + "displayName": "GCC Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" + } + }, + { + "name": "gcc-release", + "displayName": "GCC Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" + } + }, + { + "name": "llvm-debug", + "displayName": "Clang Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "BEMAN_USE_STD_MODULE": false, + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-libc++-toolchain.cmake" + }, + "environment": { + "CXX": "clang++", + "CMAKE_CXX_FLAGS": "-stdlib=libc++" + } + }, + { + "name": "llvm-release", + "displayName": "Clang Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "BEMAN_USE_STD_MODULE": false, + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-libc++-toolchain.cmake" + }, + "environment": { + "CXX": "clang++", + "CMAKE_CXX_FLAGS": "-stdlib=libc++" + } + }, + { + "name": "appleclang-debug", + "displayName": "Appleclang Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "BEMAN_USE_STD_MODULE": false, + "BEMAN_USE_MODULES": false, + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" + } + }, + { + "name": "appleclang-release", + "displayName": "Appleclang Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "BEMAN_USE_STD_MODULE": false, + "BEMAN_USE_MODULES": false, + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" + } + }, + { + "name": "msvc-debug", + "displayName": "MSVC Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "msvc-release", + "displayName": "MSVC Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + } + ], + "buildPresets": [ + { + "name": "_root-build", + "hidden": true, + "jobs": 0 + }, + { + "name": "gcc-debug", + "configurePreset": "gcc-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "gcc-release", + "configurePreset": "gcc-release", + "inherits": [ + "_root-build" + ] + }, + { + "name": "llvm-debug", + "configurePreset": "llvm-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "llvm-release", + "configurePreset": "llvm-release", + "inherits": [ + "_root-build" + ] + }, + { + "name": "appleclang-debug", + "configurePreset": "appleclang-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "appleclang-release", + "configurePreset": "appleclang-release", + "inherits": [ + "_root-build" + ] + }, + { + "name": "msvc-debug", + "configurePreset": "msvc-debug", + "inherits": [ + "_root-build" + ] + }, + { + "name": "msvc-release", + "configurePreset": "msvc-release", + "inherits": [ + "_root-build" + ] + } + ], + "testPresets": [ + { + "name": "_test_base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error", + "stopOnFailure": true + } + }, + { + "name": "gcc-debug", + "inherits": "_test_base", + "configurePreset": "gcc-debug" + }, + { + "name": "gcc-release", + "inherits": "_test_base", + "configurePreset": "gcc-release" + }, + { + "name": "llvm-debug", + "inherits": "_test_base", + "configurePreset": "llvm-debug" + }, + { + "name": "llvm-release", + "inherits": "_test_base", + "configurePreset": "llvm-release" + }, + { + "name": "appleclang-debug", + "inherits": "_test_base", + "configurePreset": "appleclang-debug" + }, + { + "name": "appleclang-release", + "inherits": "_test_base", + "configurePreset": "appleclang-release" + }, + { + "name": "msvc-debug", + "inherits": "_test_base", + "configurePreset": "msvc-debug" + }, + { + "name": "msvc-release", + "inherits": "_test_base", + "configurePreset": "msvc-release" + } + ], + "workflowPresets": [ + { + "name": "gcc-debug", + "steps": [ + { + "type": "configure", + "name": "gcc-debug" + }, + { + "type": "build", + "name": "gcc-debug" + }, + { + "type": "test", + "name": "gcc-debug" + } + ] + }, + { + "name": "gcc-release", + "steps": [ + { + "type": "configure", + "name": "gcc-release" + }, + { + "type": "build", + "name": "gcc-release" + }, + { + "type": "test", + "name": "gcc-release" + } + ] + }, + { + "name": "llvm-debug", + "steps": [ + { + "type": "configure", + "name": "llvm-debug" + }, + { + "type": "build", + "name": "llvm-debug" + }, + { + "type": "test", + "name": "llvm-debug" + } + ] + }, + { + "name": "llvm-release", + "steps": [ + { + "type": "configure", + "name": "llvm-release" + }, + { + "type": "build", + "name": "llvm-release" + }, + { + "type": "test", + "name": "llvm-release" + } + ] + }, + { + "name": "appleclang-debug", + "steps": [ + { + "type": "configure", + "name": "appleclang-debug" + }, + { + "type": "build", + "name": "appleclang-debug" + }, + { + "type": "test", + "name": "appleclang-debug" + } + ] + }, + { + "name": "appleclang-release", + "steps": [ + { + "type": "configure", + "name": "appleclang-release" + }, + { + "type": "build", + "name": "appleclang-release" + }, + { + "type": "test", + "name": "appleclang-release" + } + ] + }, + { + "name": "msvc-debug", + "steps": [ + { + "type": "configure", + "name": "msvc-debug" + }, + { + "type": "build", + "name": "msvc-debug" + }, + { + "type": "test", + "name": "msvc-debug" + } + ] + }, + { + "name": "msvc-release", + "steps": [ + { + "type": "configure", + "name": "msvc-release" + }, + { + "type": "build", + "name": "msvc-release" + }, + { + "type": "test", + "name": "msvc-release" + } + ] + } + ] +} diff --git a/subprojects/task/LICENSE b/subprojects/task/LICENSE new file mode 100644 index 000000000..f6db814c2 --- /dev/null +++ b/subprojects/task/LICENSE @@ -0,0 +1,219 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. diff --git a/subprojects/task/Makefile b/subprojects/task/Makefile new file mode 100644 index 000000000..3ee617d03 --- /dev/null +++ b/subprojects/task/Makefile @@ -0,0 +1,74 @@ +# Standard stuff + +.SUFFIXES: + +MAKEFLAGS+= --no-builtin-rules # Disable the built-in implicit rules. +# MAKEFLAGS+= --warn-undefined-variables # Warn when an undefined variable is referenced. +# MAKEFLAGS+= --include-dir=$(CURDIR)/conan # Search DIRECTORY for included makefiles (*.mk). + +#-dk: note to self: PATH=/opt/llvm-19.1.6/bin:$PATH LDFLAGS=-fuse-ld=lld + +.PHONY: config test default install examples clean distclean doc docs html pdf format clang-format tidy + +export CXX := g++ + +BUILDDIR = build +PRESET = gcc-release +UNAME = $(shell uname -s) +ifeq ($(UNAME),Darwin) + # PRESET = appleclang-release + ifeq ($(shell uname -m),arm64) + PRESET = appleclang-release + else + PRESET = gcc-release + endif +endif +BUILD = $(BUILDDIR)/$(PRESET) + +default: $(BUILD)/compile_commands.json + cmake --workflow --preset=$(PRESET) + cmake --install $(BUILD) --prefix=$(BUILDDIR)/statedir + +examples: default + cmake -S $@ -G Ninja -B build/examples -D CMAKE_BUILD_TYPE=RelWithDebInfo \ + -D CMAKE_PREFIX_PATH=$(CURDIR)/$(BUILDDIR)/statedir \ + --fresh --log-level=VERBOSE + ninja -C build/examples all + ninja -C build/examples test + +docs doc: + cd docs; $(MAKE) + +pdf html: + cd docs; $(MAKE) doc-$@ + +$(BUILD)/compile_commands.json: Makefile CMakePresets.json + cmake --preset=$(PRESET) --log-level=VERBOSE + ln -sf $(@) . + +list: + cmake --workflow --list-presets + +format: + pre-commit run --all + +clang-format: + git clang-format main + +$(BUILDDIR)/tidy/compile_commands.json: Makefile CMakeLists.txt + CC=$(CXX) cmake -G Ninja -S . -B $(BUILDDIR)/tidy \ + -D CMAKE_EXPORT_COMPILE_COMMANDS=1 \ + --fresh --log-level=VERBOSE + ln -sf $(@) . + +tidy: $(BUILDDIR)/tidy/compile_commands.json + run-clang-tidy -p $(BUILDDIR)/tidy tests examples + +test: $(BUILDDIR) + cd $(BUILDDIR); ctest + +clean: + cmake --build $(BUILD) --target clean + +distclean: + $(RM) -r $(BUILD) $(BUILDDIR) diff --git a/subprojects/task/README.md b/subprojects/task/README.md new file mode 100644 index 000000000..3dd2efff4 --- /dev/null +++ b/subprojects/task/README.md @@ -0,0 +1,124 @@ +# beman.task: Beman Library Implementation of `task` (P3552) + + + +![Continuous Integration Tests](https://github.com/bemanproject/task/actions/workflows/ci_tests.yml/badge.svg) +![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp26.svg) +![Coverage](https://coveralls.io/repos/github/bemanproject/task/badge.svg?branch=main) +![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) + +`beman::execution::task` is a class template which +is used as the the type of coroutine tasks. The corresponding objects +are senders. The first template argument (`T`) defines the result +type which becomes a `set_value_t(T)` completion signatures. The +second template argument (`Context`) provides a way to configure +the behavior of the coroutine. By default it can be left alone. + +**Implements**: `std::execution::task` proposed in [Add a Coroutine Lazy Type (P3552)](https://wg21.link/P3552). + +**Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#under-development-and-not-yet-ready-for-production-use) + +## License + +`beman.task` is licensed under the Apache License v2.0 with LLVM Exceptions. + +## Usage + +The following code snippet shows a basic use of `beman::task::task` +using sender/receiver facilities to implement version of `hello, +world`: + +```cpp +#include +#include +#include + +namespace ex = beman::execution; +namespace ly = beman::task; + +int main() { + return std::get<0>(*ex::sync_wait([]->ex::task { + std::cout << "Hello, world!\n"; + co_return co_await ex::just(0); + }())); +} +``` + +Full runnable examples can be found in [`examples/`](`./example`) (e.g., [`./examples/hello.cpp`](./examples/hello.cpp)). For some explanation see [`./docs/examples.md`](./docs/examples.md). + +## Help Welcome + +There are plenty of things which need to be done. See the +[contributions page](https://github.com/bemanproject/task/blob/main/docs/contributing.md) +for some ideas how to contribute. The [resources page](https://github.com/bemanproject/task/blob/main/docs/resources.md) +contains some links for general information about coroutines. + +## Building beman.task + +### Dependencies + +This project depends on +[`beman::execution`](https://github.com/bemanproject/execution) (which +will be automatically obtained using `cmake`'s `FetchContent*`). + +Build-time dependencies: + +- `cmake` +- `ninja`, `make`, or another CMake-supported build system + - CMake defaults to "Unix Makefiles" on POSIX systems + +### Supported Platforms + +| Compiler | Version | C++ Standards | Standard Library | +|----------|---------|---------------|------------------| +| GCC | 15-14 | C++26, C++23 | libstdc++ | +| Clang | 21-19 | C++26, C++23 | libc++ | +| MSVC | latest | C++23 | MSVC STL | + +### How to build beman.task + + + +This project strives to be as normal and simple a CMake project as +possible. This build workflow in particular will work, producing +a static `libbeman.task.a` library, ready to package with its +headers: + +```shell +# Build beman.task. +$ cmake --workflow --preset gcc-debug +$ cmake --workflow --preset gcc-release + +# Install beman.task into your system. +$ cmake --install build/gcc-release/ --prefix /opt/beman.task/ +-- Install configuration: "RelWithDebInfo" +-- Installing: /opt/beman.task/lib/libbeman.task.a +-- Installing: /opt/beman.task/include/beman/task/task.hpp +[...] + +$ tree /opt/beman.task/ +/opt/beman.task/ +├── include +│   └── beman +│   └── task +│   ├── detail +│   │   ├── affine_on.hpp +│   │   ├── ... +│   │   ├── task.hpp +│   │   └── with_error.hpp +│   └── task.hpp +└── lib + ├── cmake + │   └── beman + │   └── task + │   └── BemanTaskConfig.cmake + └── libbeman.task.a + +9 directories, 26 files +``` + +## Contributing + +Please do! Issues and pull requests are appreciated. diff --git a/subprojects/task/docs/Makefile b/subprojects/task/docs/Makefile new file mode 100644 index 000000000..36b9db487 --- /dev/null +++ b/subprojects/task/docs/Makefile @@ -0,0 +1,18 @@ +.PHONY: default doc doc-html doc-pdf + +default: doc + +doc: doc-html + +doc-html: P3552-task.html P3796-task-issues.html P3941-affinity.html P3980-allocation.html + +doc-pdf: P3552-task.pdf P3796-task-issues.pdf + +wg21/Makefile: + git clone https://github.com/mpark/wg21.git + +distclean: clean + $(RM) -r generated + $(RM) mkerr olderr + +include wg21/Makefile diff --git a/subprojects/task/docs/P3552-task.md b/subprojects/task/docs/P3552-task.md new file mode 100644 index 000000000..2e3263466 --- /dev/null +++ b/subprojects/task/docs/P3552-task.md @@ -0,0 +1,2108 @@ +--- +title: Add a Coroutine Task Type +document: P3552R3 +date: 2025-06-20 +audience: + - Concurrency Working Group (SG1) + - Library Evolution Working Group (LEWG) + - Library Working Group (LWG) +author: + - name: Dietmar Kühl (Bloomberg) + email: + - name: Maikel Nadolski + email: +source: + - https://github.com/bemanproject/task/doc/P3552-task.md +toc: false +--- + +C++20 added support for coroutines that can improve the experience +writing asynchronous code. C++26 added the sender/receiver model +for a general interface to asynchronous operations. The expectation +is that users would use the framework using some coroutine type. To +support that, a suitable class needs to be defined and this proposal +is providing such a definition. + +Just to get an idea what this proposal is about: here is a simple +`Hello, world` written using the proposed coroutine type: + +```c++ +#include +#include +#include + +namespace ex = std::execution; + +int main() { + return std::get<0>(*ex::sync_wait([]->ex::task { + std::cout << "Hello, world!\n"; + co_return co_await ex::just(0); + }())); +} +``` + +# Change History + +## R0 Initial Revision + +## R1 Hagenberg Feedback + +- Changed the name from `lazy` to `task` based on SG1 feedback and + dropped the section on why `lazy` was chosen. +- Changed the name of `any_scheduler` to `task_scheduler`. +- Added wording for the `task` specification. + +## R2 LEWG Feedback + +- Removed the use `decay_t` from the specification. +- Changed the wording to avoid `exception_ptr` when unavailable. +- Renamed `Context` to `Environment` to better reflect the argument's use. +- Made exposition-only "macros" use italics. +- Added a feature test macro. +- Fixed some typos. + +## R3 LWG Feedback + +# Prior Work + +This proposal isn't the first to propose a coroutine type. Prior proposals +didn't see any recent (post introduction of sender/receiver) update, although +corresponding proposals were discussed informally on multiple occasions. +There are also implementations of coroutine types based on a sender/receiver +model in active use. This section provides an overview of this prior work, +and where relevant, of corresponding discussions. This section is primarily +for motivating requirements and describing some points in the design space. + +## [P1056](https://wg21.link/P1056): Add lazy coroutine (coroutine task) type + +The paper describes a `task`/`lazy` type (in +[P1056r0](https://wg21.link/P1056r0) the name was `task`; the primary +change for [P1056r1](https://wg21.link/P1056r1) is changing the +name to `lazy`). The fundamental idea is to have a coroutine type +which can be `co_await`ed: the interface of `lazy` consists of move +constructor, deliberately no move assignment, a destructor, and +`operator co_await()`. The proposals don't go into much detail +on how to eventually use a coroutine, but it mentions that there could +be functions like `sync_await(task)` to await completion of a +task (similar to +[`execution::sync_wait(sender)`](https://eel.is/c++draft/exec.sync.wait)) +or a few variations of that. + +A fair part of the paper argues why `future.then()` is _not_ a good +approach to model coroutines and their results. Using `future` +requires allocation, synchronization, reference counting, and +scheduling which can all be avoided when using coroutines in a +structured way. + +The paper also mentions support for [symmetric +transfer](https://wg21.link/P0913) and allocator support. Both of these +are details on how the coroutine is implemented. + +[Discussion for P1056r0 in SG1](https://wiki.edg.com/bin/view/Wg21rapperswil2018/P1056R0) + +- The task doesn't really have anything to do with concurrency. +- Decomposing a task cheaply is fundamental. The + [HALO Optimizations](https://wg21.link/P0981R0) help. +- The `task` isn't move assignable because there are better approaches + than using containers to hold them. It is move constructible as there + are no issues with overwriting a potentially live task. +- Resuming where things complete is unsafe but the task didn't want to + impose any overhead on everybody. +- There can be more than one `task` type for different needs. +- Holding a mutex lock while `co_await`ing which may resume on a different + thread is hazardous. Static analyzers should be able to detect these cases. +- Votes confirmed the no move assignment and forwarding to LEWG assuming + the name is not `task`. +- Votes against deal with associated executors and a request to have + strong language about transfer between threads. + +## [P2506](https://wg21.link/P2506): std::lazy: a coroutine for deferred execution + +This paper is effectively restating what [P1056](https://wg21.link/P1056) +said with the primary change being more complete proposed wording. +Although sender/receiver were discussed when the paper was written +but [`std::execution`](https://wg21.link/P2300) hadn't made it into +the working paper, the proposal did _not_ take a sender/receiver +interface into account. + +Although there were mails seemingly scheduling a discussion in LEWG, +we didn't manage to actually locate any discussion notes. + +## [cppcoro](https://github.com/lewissbaker/cppcoro) + +This library contains multiple coroutine types, algorithms, and +some facilities for asynchronous work. For the purpose of this +discussion only the task types are of interest. There are two task +types +[`cppcoro::task`](https://github.com/lewissbaker/cppcoro?tab=readme-ov-file#taskt) +and +[`cppcoro::shared_task`](https://github.com/lewissbaker/cppcoro?tab=readme-ov-file#shared_taskt). +The key difference between `task` and `shared_task` is that the +latter can be copied and awaited by multiple other coroutines. As +a result `shared_task` always produces an lvalue and may have +slightly higher costs due to the need to maintain a reference count. + +The types and algorithms are pre-sender/receiver and operate entirely +in terms for awaiters/awaitables. The interface of both task types +is a bit richer than that from +[P1056](https://wg21.link/P1056)/[P2506](https://wg21.link/P2506). Below +`t` is either a `cppcoro::task` or a `cppcoro::shared_task`: + +- The task objects can be move constructed and move assigned; `shared_task` + object can also be copy constructed and copy assigned. +- Using `t.is_ready()` it can be queried if `t` has completed. +- Using `co_await t` awaits completion of `t`, yielding the result. The result + may be throwing an exception if the coroutine completed by throwing. +- Using `co_await t.when_ready()` allows synchronizing with the completion of + `t` without actually getting the result. This form of synchronization won't + throw any exception. +- `cpproro::shared_task` also supports equality comparisons. + +In both cases, the task starts suspended and is resumed when it is +`co_await`ed. This way a continuation is known when the task is +resumed, which is similar to `start(op)`ing an operation state `op`. +The coroutine body needs to use `co_await` or `co_return`. `co_await` +expects an awaitable or an awaiter as argument. Using `co_yield` +is not supported. The implementation supports symmetric transfer but +doesn't mention allocators. + +The `shared_task` is similar to +[`split(sender)`](https://eel.is/c++draft/exec.split): in both cases, +the same result is produced for multiple consumers. Correspondingly, +there isn't a need to support a separate `shared_task` in a +sender/receiver world. Likewise, throwing of results can be avoid +by suitably rewriting the result of the `set_error` channel avoiding +the need for an operation akin to `when_ready()`. + +## [libunifex](https://github.com/facebookexperimental/libunifex) + +`unifex` is an earlier implementation of the sender/receiver +ideas. Compared to `std::execution` it is lacking some of the +flexibilities. For example, it doesn't have a concept of environments +or domains. However, the fundamental idea of three completion +channels for success, failure, and cancellation and the general +shape of how these are used is present (even using the same names +for `set_value` and `set_error`; the equivalent of `set_stopped` +is called `set_done`). `unifex` is in production use in multiple +places. The implementation includes a +[`unifex::task`](https://github.com/facebookexperimental/libunifex/blob/main/include/unifex/task.hpp). + +As `unifex` is sender/receiver-based, its `unifex::task` is +implemented such that `co_await` can deal with senders in addition +to awaitables or awaiters. Also, `unifex::task` is _scheduler +affine_: the coroutine code resumes on the same scheduler even if a +sender completed on a different scheduler. The task's scheduler +is taken from the receiver it is `connect`ed to. The exception for +rescheduling on the task's scheduler is explicitly awaiting the +result of `schedule(sched)` for some scheduler `sched`: the operation +changes the task's scheduler to be `sched`. The relevant treatment +is in the promise type's `await_transform()`: + +- If a sender `sndr` which is the result of `schedule(sched)` is + `co_await`ed, the corresponding `sched` is installed as the task's + scheduler and the task resumes on the context completing `sndr`. + Feedback from people working with unifex suggests that this choice + for changing the scheduler is too subtle. While it is considered + important to be able to explicitly change the scheduler a task + executes on, doing so should be more explicit. +- For both senders and awaiters being awaited, the coroutine + will be resumed on the task's current scheduler when the task is + scheduler affine. In general that is done by continuing with the + senders result on the task's scheduler, similar to `continues_on(sender, + scheduler)`. The rescheduling is avoided when the sender is tagged + as not changing scheduler (using a `static constexpr` member named + `blocking` which is initialized to `blocking_kind::always_inline`). +- If a sender is `co_await`ed it gets `connect`ed to a receiver + provided by the task to form an awaiter holding an operation + state. The operation state gets `start`ed by the awaiter's + `await_suspend`. The receiver arranges for a `set_value` completion + to become a value returned from `await_resume`, a `set_error` + completion to become an exception, and a `set_done` completion + to resume a special "on done" coroutine handle rather than resuming + the task itself effectively behaving like an uncatchable exception + (all relevant state is properly destroyed and the coroutine is + never resumed). + +When `co_await`ing a sender `sndr` there can be at most one `set_value` +completion: if there are more than one `set_value` completions the +promise type's `await_transform` will just return `sndr` and the +result cannot be `co_await`ed (unless it is also given an awaitable +interface). The result type of `co_await sndr` depends on the number +of arguments to `set_value`: + +- If there are no arguments for `set_value` then the type of `co_await sndr` + will be `void`. +- If there is exactly one argument of type `T` for `set_value` + then the type of `co_await sndr` will be `T`. +- If there are more than one arguments for `set_value` then + the type of `co_await sndr` will be `std::tuple` + with the corresponding argument types. + +If a receiver doesn't have a scheduler, it can't be `connect()`ed +to a `unifex::task`. In particular, when using a `unifex::async_scope +scope` it isn't possible to directly call `scope.spawn(task)` with +a `unifex::task task` as the `unifex::async_scope` doesn't provide +a scheduler. The `unifex::async_scope` provides a few variations +of `spawn()` which take a scheduler as argument. + +`unifex` provides some sender algorithms to transform the sender +result into something which may be more suitable to be `co_await`ed. +For example, `unifex::done_as_optional(sender)` turns a successful +completion for a type `T` into an `std::optional` and the +cancellation completion `set_done` into a `set_value` completion +with a disengaged `std::optional`. + +The `unifex::task` is itself a sender and can be used correspondingly. +To deal with scheduler affinity a type erased scheduler +`unifex::any_scheduler` is used. + +The `unifex::task` doesn't have allocator support. When creating +a task multiple objects are allocated on the heap: it seems there +is a total of 6 allocations for each `unifex::task` being created. +After that, it seems the different `co_await`s don't use a separate +allocation. + +The `unifex::task` doesn't directly guard against stack overflow. +Due to rescheduling continuations on a scheduler when the completion +isn't always inline, the issue only arises when `co_await`ing many +senders with `blocking_kind::always_inline` or when the scheduler +resumes inline. + +## [stdexec](https://github.com/NVIDIA/stdexec) + +The +[`exec::task`](https://github.com/NVIDIA/stdexec/blob/main/include/exec/task.hpp) +in stdexec is somewhat similar to the unifex task with some choices +being different, though: + +- The `exec::task` is also scheduler affine. The chosen scheduler + is unconditionally used for every `co_await`, i.e., there is no + attempt made to avoid scheduling, e.g., when the `co_await`ed sender + completes inline. +- Unlike unifex, it is OK if the receiver's environment doesn't provide + a scheduler. In that case an inline scheduler is used. If an inline + scheduler is used there is the possibility of stack overflow. +- It is possible to `co_await just_error(e)` and `co_await just_stopped()`, + i.e., the sender isn't required to have a `set_value_t` completion. + +The `exec::task` also provides a _context_ `C`. An object of +this type becomes the environment for receivers `connect()`ed to +`co_await`ed senders. The default context provides access to the +task's scheduler. In addition an `in_place_stop_token` is provides +which forwards the stop requests from the environment of the receiver +which is connected to the task. + +Like the unifex task `exec::task` doesn't provide any allocator +support. When creating a task there are two allocations. + +# Objectives + +Also see [sender/receiver issue 241](https://github.com/cplusplus/sender-receiver/issues/241). + +Based on the prior work and discussions around corresponding coroutine +support there is a number of required or desired features (listed in +no particular order): + +1. A coroutine task needs to be awaiter/awaitable friendly, i.e., it + should be possibly to `co_await` awaitables which includes both + library provided and user provided ones. While that seems + obvious, it is possible to create an `await_transform` which + is deleted for awaiters and that should be prohibited. +2. When composing sender algorithms without using a coroutine it + is common to adapt the results using suitable algorithms and + the completions for sender algorithms are designed accordingly. + On the other hand, when awaiting senders in a coroutine it may + be considered annoying having to transform the result into a + shape which is friendly to a coroutine use. Thus, it may be + reasonable to support rewriting certain shapes of completion + signatures into something different to make the use of senders + easier in a coroutine task. See the section on the + [result type for `co_await`](#result-type-for-co_await) for a + discussion. + +3. A coroutine task needs to be sender friendly: it is expected that + asynchronous code is often written using coroutines awaiting + senders. However, depending on how senders are treated by a + coroutine some senders may not be awaitable. For example neither + unifex nor stdexec support `co_await`ing senders with more than + one `set_value` completion. +4. It is possibly confusing and problematic if coroutines resume on + a different execution context than the one they were suspended + on: the textual similarity to normal functions makes it look + as if things are executed sequentially. Experience also indicates + that continuing a coroutine on whatever context a `co_await`ed + operation completes frequently leads to issues. Senders could, + however, complete on an entirely different scheduler than where + they started. When composing senders (not using coroutines) + changing contexts is probably OK because it is done deliberately, + e.g., using `continues_on`, and the way to express things is + new with fewer attached expectations. + + To bring these two views + together a coroutine task should be scheduler affine by default, + i.e., it should normally resume on the same scheduler. There + should probably also be an explicit way to opt out of scheduler + affinity when the implications are well understood. + + Note that scheduler affinity does _not_ mean that a task is + always continuing on the same thread: a scheduler may refer to + a thread pool and the task will continue on one of the threads + (which also means that thread local storage cannot be used to + propagate contexts implicitly; see the [discussion on environments](#environment-support) + below). +5. When using coroutines there will probably be an allocation at + least for the coroutine frame (the [HALO optimizations](https://wg21.link/P0981R0) + can't always work). To support the use in environments + where memory allocations using `new`/`delete` aren't supported + the coroutine task should support allocations using allocators. +6. Receivers have associated environments which can support an open + set of queries. Normally, queries on an environment can be + forwarded to the environment of a `connect()`ed receiver. + Since the coroutine types are determined before the coroutine's + receiver is known and the queries themselves don't specify a + result type that isn't possible when a coroutine provides a + receiver to a sender in a `co_await` expression. It should still + be possible to provide a user-customisable environment from the + receiver used by `co_await` expressions. One aspect of this + environment is to forward stop requests to `co_await`ed child + operations. Another is possibly changing the scheduler to be + used when a child operation queries `get_scheduler` from the + receiver's environment. Also, in non-asynchronous code it is + quite common to pass some form of context implicitly using + thread local storage. In an asynchronous world such contexts + could be forwarded using the environment. +7. The coroutine should be able to indicate that it was canceled, + i.e., to get `set_stopped()` called on the task's receiver. + `std::execution::with_awaitable_senders` already provided this + ability senders being `co_await`ed but that doesn't necessarily + extend to the coroutine implementation. +8. Similar to indicating that a task got canceled it would be good + if a task could indicate that an error occurred without throwing + an exception which escapes from the coroutine. +9. In general a task has to assume that an exception escapes the + coroutine implementation. As a result, the task's completion + signatures need to include `set_error_t(std::exception_ptr)`. + If it can be indicated to the task that no exception will escape + the coroutine, this completion signature can be avoided. +10. When many `co_await`ed operations complete synchronously, there + is a chance for stack overflow. It may be reasonable to have + the implementation prevent stack overflow by using a suitable + scheduler sometimes. +11. In some situations it can be useful to somehow schedule an + asynchronous clean-up operation which is triggered upon + coroutine exit. See the section on [asynchronous clean-up](#asynchronous-clean-up) + below for more discussing +12. The `task` coroutine provided by the standard library may + not always fit user's needs although they may need/want various + of the facilities. To avoid having users implement all functionality + from scratch `task` should use specified components which can + be used by users when building their own coroutine. The components + `as_awaitable` and `with_awaitable_sender` are two parts of + achieving this objective but there are likely others. + + The algorithm `std::execution::as_awaitable` does turn a sender + into an awaitable and is expected to be used by custom written + coroutines. Likewise, it is intended that custom coroutines use + the CRTP class template `std::execution::with_awaitable_senders`. + It may be reasonable to adjust the functionality of these + components instead of defining the functionality specific to a + `task<...>` coroutine task. + +It is important to note that different coroutine task implementations +can live side by side: not all functionality has to be implemented +by the same coroutine task. The objective for this proposal is to +select a set of features which provides a coroutine task suitable +for most uses. It may also be reasonable to provide some variations +as different names. A future revision of the standard or third party +libraries can also provide additional variations. + +# Design + +This section discusses various design options for achieving the +listed objectives. Most of the designs are independent of each other +and can be left out if the consensus is that it shouldn't be used +for whatever reason. + +## Template Declaration for `task` + +Coroutines can use `co_return` to produce a value. The value returned can +reasonably provide the argument for the `set_value_t` completion +of the coroutines. As the type of a coroutine is defined even before +the coroutine body is given, there is no way to deduce the result +type. The result type is probably the primary customization and +should be the first template parameter which gets defaulted to +`void` for coroutines not producing any value. For example: + +```c++ +int main() { + ex::sync_wait([]->ex::task<>{ + int result = co_await []->ex::task { co_return 42; }(); + assert(result == 42); + }()); +} +``` + +The inner coroutines completes with `set_value_t(int)` which gets +translated to the value returned from `co_await` (see [`co_await` +result type below](#result-type-for-co_await) for more details). +The outer coroutine completes with `set_value_t()`. + +Beyond the result type there are a number of features for a coroutine +task which benefit from customization or for which it may be desirable +to disable them because they introduce a cost. As many template +parameters become unwieldy, it makes sense to combine these into a +[defaulted] context parameter. The aspects which benefit from +customization are at least: + +- [Customizing the environment](#environment-support) for child + operations. The context itself can actually become part of + the environment. +- Disable [scheduler affinity](#scheduler-affinity) and/or configure + the strategy for obtaining the coroutine's scheduler. +- Configure [allocator awareness](#allocator-support). +- Indicate that the coroutine should be [noexcept](#error-reporting). +- Define [additional error types](#error-reporting). + +The default context should be used such that any empty type provides +the default behavior instead of requiring a lot of boilerplate just +to configure a particular aspect. For example, it should be possible +to selectively enable [allocator support](#allocator-support) using +something like this: + +```c++ +struct allocator_aware_context { + using allocator_type = std::pmr::polymorphic_allocator; +}; +template +using my_task = ex::task; +``` + +Using various different types for task coroutines isn't a problem +as the corresponding objects normally don't show up in containers. +Tasks are mostly `co_await`ed by other tasks, used as child senders +when composing work graphs, or maintained until completed using +something like a [`counting_scope`](https://wg21.link/p3149). When +they are used in a container, e.g., to process data using a range +of coroutines, they are likely to use the same result type and +context types for configurations. + +## `task` Completion Signatures + +The discussion above established that `task` can have a +successful completion using `set_value_t(T)`. The coroutine completes +accordingly when it is exited using a matching `co_return`. When +`T` is `void` the coroutine also completes successfully using +`set_value()` when floating off the end of the coroutine or when +using a `co_return` without an expression. + +If a coroutine exits with an exception completing the corresponding +operation with `set_error(std::exception_ptr)` is an obvious choice. +Note that a `co_await` expression results in throwing an exception +when the awaited operation completes with `set_error(E)` +([see below](#result-type-for-co_await)), i.e., the coroutine itself +doesn't necessarily need to `throw` an exception itself. + +Finally, a `co_await` expression completing with `set_stoppped()` +results in aborting the coroutine immediately +([see below](#result-type-for-co_await)) and causing the coroutine +itself to also complete with `set_stopped()`. + +The coroutine implementation cannot inspect the coroutine body to +determine how the different asynchronous operations may complete. As +a result, the default completion signatures for `task` are + +```c++ +ex::completion_signatures< + ex::set_value_t(T), // or ex::set_value_t() if T == void + ex::set_error_t(std::exception_ptr), + ex:set_stopped_t() +>; +``` + +Support for [reporting an error without exception](#error-reporting) +may modify the completion signatures. + +## `task` constructors and assignments + +Coroutines are created via a factory function which returns the +coroutine type and whose body uses one of the `co_*` function, e.g. + +```c++ +task<> nothing(){ co_return; } +``` + +The actual object is created via the promise type's `get_return_object` +function and it is between the promise and coroutine types how +that actually works: this constructor is an implementation detail. +To be valid senders the coroutine type needs to be destructible and it +needs to have a move constructor. Other than that, constructors and +assignments either don't make sense or enable dangerous practices: + +1. Copy constructor and copy assignment don't make sense because there + is no way to copy the actual coroutine state. +2. Move assignment is rather questionable because it makes it easy to + transport the coroutine away from referenced entities. + + Previous papers [P1056](https://wg21.link/p1056) and + [P2506](https://wg21.link/p2506) also argued against a move + assignment. However, one of the arguments doesn't apply to the + `task` proposed here: There is no need to deal with cancellation + when assigning or destroying a `task` object. Upon `start()` + of `task` the coroutine handle is transferred to an operation + state and the original coroutine object doesn't have any + reference to the object anymore. +3. If there is no assignment, a default constructed object doesn't make + much sense, i.e., `task` also doesn't have a default constructor. + +Based on experience with [Folly](https://github.com/facebook/folly) +the suggestion was even stronger: `task` shouldn't even have move +construction! That would mean that `task` can't be a sender or that +there would need to be some internal interface enabling the necessary +transfer. That direction isn't pursued by this proposal. + +The lack of move assignment doesn't mean that `task` can't be held +in a container: it is perfectly fine to `push_back` objects of this +type into a container, e.g.: + +```c++ +std::vector> cont; +cont.emplace_back([]->ex::task<> { co_return; }()); +cont.push_back([]->ex::task<> { co_return; }()); +``` + +The expectation is that most of the time coroutines don't end up +in normal containers. Instead, they'd be managed by a +[`counting_scope`](https://wg21.link/p3149) or hold on to by objects +in a work graph composed of senders. + +Technically there isn't a problem adding a default constructor, move +assignment, and a `swap()` function. Based on experience with similar +components it seems `task` is better off not having them. + +## Result Type For `co_await` + +When `co_await`ing a sender `sndr` in a coroutine, `sndr` needs to +be transformed to an awaitable. The existing approach is to use +`execution::as_waitable(sndr)` [[exex.as.awaitable]](https://eel.is/c++draft/exec.as.awaitable) +in the promise type's `await_transform` and `task` uses that approach. +The awaitable returned from `as_awaitable(sndr)` has the following +behavior (`rcvr` is the receiver the sender `sndr` is connected to): + +1. When `sndr` completes with `set_stopped(std::move(rcvr))` the function `unhandled_stopped()` + on the promise type is called and the awaiting coroutine is never + resumed. The `unhandled_stopped()` results in `task` itself + also completing with `set_stopped_t()`. +2. When `sndr` completes with `set_error(std::move(rcvr), error)` the coroutine is resumed + and the `co_await sndr` expression results in `error` being thrown as an + exceptions (with special treatment for `std::error_code`). +3. When `sndr` completes with `set_value(std::move(rcvr), a...)` the expression `co_await sndr` + produces a result corresponding the arguments to `set_value`: + + 1. If the argument list is empty, the result of `co_await sndr` is `void`. + 2. Otherwise, if the argument list contains exactly one element the result of `co_await sndr` is `a...`. + 3. Otherwise, the result of `co_await sndr` is `std::tuple(a...)`. + +Note that the sender `sndr` is allowed to have no `set_value_t` +completion signatures. In this case the result type of the awaitable +returned from `as_awaitable(sndr)` is declared to be `void` but +`co_await sndr` would never return normally: the only ways to +complete without a `set_value_t` completion is to complete with +`set_stopped(std::move(rcvr)` or with `set_error(std::move(rcvr), +error)`, i.e., the expression either results in the coroutine to +be never resumed or an exception being thrown. + +Here is an example which summarizes the different supported result types: + +```c++ +task<> fun() { + co_await ex::just(); // void + auto v = co_await ex::just(0); // int + auto[i, b, c] = co_await ex::just(0, true, 'c'); // tuple + try { co_await ex::just_error(0); } catch (int) {} // exception + co_await ex::just_stopped(); // cancel: never resumed +} +``` + +The sender `sndr` can have at most one `set_value_t` completion +signature: if there are more than one `set_value_t` completion +signatures `as_awaitable(sndr)` is invalid and fails to compile: users +who want to `co_await` a sender with more than one `set_value_t` +completions need to use `co_await into_variant(s)` (or similar) to +transform the completion signatures appropriately. It would be possible +to move this transformation into `as_awaitable(sndr)`. + +Using effectively `into_variant(s)` isn't the only possible +transformation if there are multiple `set_value_t` transformations. +To avoid creating a fairly hard to use result object, `as_awaitable(sndr)` +could detect certain usage patterns and rather create a result which +is easier to use when being `co_await`ed. An example for this +situation is the `queue.async_pop()` operation for [concurrent +queues](https://wg21.link/P0260): this operation can complete +successfully in two ways: + +1. When an object was extracted the operation completes with `set_value(std::move(rcvr), value)`. +2. When the queue was closed the operation completes with `set_value(std::move(rcvr))`. + +Turning the result of `queue.async_pop()` into an awaitable +using the current `as_awaitable(queue.async_pop())` +([[exec.as.awaitable](https://eel.is/c++draft/exec#as.awaitable)]) +fails because the function accepts only senders with at most +one `set_value_t` completion. Thus, it is necessary to use something +like the below: + +```c++ +task<> pop_demo(auto& queue) { + // auto value = co_await queue.async_pop(); // doesn't work + std::optional v0 = co_await (queue.async_pop() | into_optional); + std::optional v1 = co_await into_optional(queue.async_pop()); +} +``` + +The algorithm `into_optional(sndr)` would determine that there is +exactly one `set_value_t` completion with arguments and produce an +`std::optional` if there is just one parameter of type `T` and +produce a `std::optional>` if there are more than +one parameter with types `T...`. It would be possible to apply this +transformation when a corresponding set of completions is detected. +The proposal [optional variants in sender/receiver](https://wg21.link/P3570) +goes into this direction. + +This proposal currently doesn't propose a change to `as_awaitable` +([[exec.as.awaitable](https://eel.is/c++draft/exec#as.awaitable)]). +The primary reason is that there are likely many different shapes +of completions each with a different desirable transformation. If +these are all absorbed into `as_awaitable` it is likely fairly hard +to reason what exact result is returned. Also, there are likely +different options of how a result could be transformed: `into_optional` +is just one example. It could be preferable to turn the two results +into an `std::expected` instead. However, there should probably be +some transformation algorithms like `into_optional`, `into_expected`, +etc. similar to `into_variant`. + +## Scheduler Affinity + +Coroutines look very similar to synchronous code with a few +`co`-keywords sprinkled over the code. When reading such code the +expectation is typically that all code executes on the same context +despite some `co_await` expressions using senders which may explicitly +change the scheduler. There are various issues when using `co_await` +naïvely: + +- Users may expect that work continues on the same context where it + was started. If the coroutine simply resumes when the `co_await`ed + senders calls a completion function code may execute some lengthy + operation on a context which is expected to keep a UI responsive + or which is meant to deal with I/O. +- Conversely, running a loop `co_await`ing some work may be seen as + unproblematic but may actually easily cause a stack overflow if + `co_await`ed work immediately completes (also + [see below](#avoiding-stack-overflow)). +- When `co_await`ing some work completes on a different context and + later a blocking call is made from the coroutine which also + ends up `co_await`ing some work from the same resource there + can be a dead lock. + +Thus, the execution should normally be scheduled on the original +scheduler: doing so can avoid the problems mentioned above (assuming +a scheduler is used which doesn't immediately complete without +actually scheduling anything). This transfer of the execution with +a coroutine is referred to as _scheduler affinity_. Note: a scheduler +may execute on multiple threads, e.g., for a pool scheduler: execution +would get to any of these threads, i.e., thread local storage is +_not_ guaranteed to access the same data even with scheduler affinity. +Also, scheduling work has some cost even if this cost can often be +fairly small. + +The basic idea for scheduler affinity consists of a few parts: + +1. A scheduler is determined when `start`ing an operation state + which resulted from `connect`ing a coroutine to a receiver. + This scheduler is used to resume execution of the coroutine. + The scheduler is determined based on the receiver `rcvr`'s + environment. + + ```c++ + auto scheduler = get_scheduler(get_env(rcvr)); + ``` + +2. The type of `scheduler` is unknown when the coroutine is created. + Thus, the coroutine implementation needs to operate in terms + of a scheduler with a known type which can be constructed from + `scheduler`. The used scheduler type is determined based on the + context parameter `C` of the coroutine type `task` using + `typename C::scheduler_type` and defaults to `task_scheduler` + if this type isn't defined. `task_scheduler` uses type-erasure + to deal with arbitrary schedulers (and small object optimizations + to avoid allocations). The used scheduler type can be parameterized + to allow use of `task` contexts where the scheduler type is + known, e.g., to avoid the costs of type erasure. + + Originally `task_scheduler` was called `any_scheduler` but there + was feedback from SG1 suggesting that a general `any_scheduler` + may need to cover various additional properties. To avoid dealing + with generalizing the facility a different name is used. The + name remains specified as it is still a useful component, at + least until an `any_scheduler` is defined by the standard + library. If necessary, the type erased scheduler type used by + `task` can be unspecified. + +3. When an operation which is `co_await`ed completes the execution + is transferred to the held scheduler using `continues_on`. + Injecting this operation into the graph can be done in the + promise type's `await_transform`: + + ```c++ + template + auto await_transform(Sender&& sndr) noexcept { + return ex::as_awaitable_sender( + ex::continues_on(std::forward(sndr), + this->scheduler); + ); + } + ``` + +There are a few immediate issues with the basic idea: + +1. What should happen if there is no scheduler, i.e., + `get_scheduler(get_env(rcvr))` doesn't exist? +2. What should happen if the obtained `scheduler` is incompatible with + the coroutine's scheduler? +3. Scheduling isn't free and despite the potential problems it should + be possible to use `task` without scheduler affinity. +4. When operations are known to complete inline the scheduler isn't + actually changed and the scheduling operation should be avoided. +5. It should be possible to explicitly change the scheduler used by + a coroutine from within this coroutine. + +All of these issues can be addressed although there are different +choices in some of these cases. + +In many cases the receiver can provide access to a scheduler via +the environment query. An example where no scheduler is available +is when starting a task on a [`counting_scope`](https://wg21.link/p3149). +The scope doesn't know about any schedulers and, thus, the receiver +used by `counting_scope` when `connect`ing to a sender doesn't +support the `get_scheduler` query, i.e., this example doesn't work: + +```c++ +ex::spawn([]->ex::task { co_await ex::just(); }(), token); +``` + +Using `spawn()` with coroutines doing the actual work is expected +to be quite common, i.e., it isn't just a theoretical possibility that +`task` is used together with `counting_scope`. The approach used by +[`unifex`](https://github.com/facebookexperimental/libunifex) is +to fail compilation when trying to `connect` a `Task` to a receiver +without a scheduler. The approach taken by +[`stdexec`](https://github.com/NVIDIA/stdexec) is to keep executing +inline in that case. Based on the experience that silently changing +contexts within a coroutine frequently causes bugs it seems failing +to compile is preferable. + +Failing to construct the scheduler used by a coroutine with the +`scheduler` obtained from the receiver is likely an error and should +be addressed by the user appropriately. Failing to compile is seems +to be a reasonable approach in that case, too. + +It should be possible to avoid scheduler affinity explicitly to +avoid the cost of scheduling. Users should be very careful when +pursuing this direction but it can be a valid option. One way to +achieve that is to create an "inline scheduler" which immediately +completes when it is `start()`ed and using this type for the +coroutine. Explicitly providing a type `inline_scheduler` implementing +this logic could allow creating suitable warnings. It would also +allow detecting that type in `await_transform` and avoiding the use +of `continues_on` entirely. + +When operations actually don't change the scheduler there shouldn't +be a need to schedule them again. In these cases it would be great +if the `continues_on` could be avoided. At the moment there is no +way to tell whether a sender will complete inline. Using a sender +query which determines whether a sender always completes inline could +avoid the rescheduling. Something like that is implemented for +[`unifex`](https://github.com/facebookexperimental/libunifex): +senders define a property `blocking` which can have the value +`blocking_kind::always_inline`. The proposal [A sender query for +completion behavior](https://wg21.link/P3206) proposes a +`get_completion_behaviour(sndr, env)` customization point to address +this need. The result can indicate that the `sndr` returns synchronously +(using `completion_behaviour::synchronous` or +`completion_behaviour::inline_completion`). If `sndr` returns synchronously +there isn't a need to reschedule it. + +In some situations it is desirable to explicitly switch to a different +scheduler from within the coroutine and from then on carry on using +this scheduler. +[`unifex`](https://github.com/facebookexperimental/libunifex) detects +the use of `co_await schedule(scheduler);` for this purpose. That is, +however, somewhat subtle. It may be reasonable to use a dedicated +awaiter for this purpose and use, e.g. + +```c++ +auto previous = co_await co_continue_on(new_scheduler); +``` + +Using this statement replaces the coroutine's scheduler with the +`new_scheduler`. When the `co_await` completes it is on `new_scheduler` +and further `co_await` operations complete on `new_scheduler`. The +result of `co_await`ing `co_continue_on` is the previously used +scheduler to allow transfer back to this scheduler. In +[stdexec](https://github.com/NVIDIA/stdexec) the corresponding +operation is called `reschedule_coroutine`. + +Another advantage of scheduling the operations on a scheduler instead +of immediately continuing on the context where the operation completed +is that it helps with stack overflows: when scheduling on a non-inline +scheduler the call stack is unwound. Without that it may be necessary +to inject scheduling just for the purpose of avoiding stack overflow +when too many operations complete inline. + +## Allocator Support + +When using coroutines at least the coroutine frame may end up being +allocated on the heap: the [HALO](https://wg21.link/P0981) optimizations +aren't always possible, e.g., when a coroutine becomes a child of +another sender. To control how this allocation is done and to support +environments where allocations aren't possible `task` should have +allocator support. The idea is to pick up on a pair of arguments of +type `std::allocator_arg_t` and an allocator type being passed and use +the corresponding allocator if present. For example: + +```c++ +struct allocator_aware_context { + using allocator_type = std::pmr::polymorphic_allocator; +}; + +template +ex::task fun(int value, A&&...) { + co_return value; +} + +int main() { + // Use the coroutine without passing an allocator: + ex::sync_wait(fun(17)); + + // Use the coroutine with passing an allocator: + using allocator_type = std::pmr::polymorphic_alloctor; + ex::sync_wait(fun(17, std::allocator_arg, allocator_type())); +} +``` + +The arguments passed when creating the coroutine are made available +to an `operator new` of the promise type, i.e., this operator can +extract the allocator, if any, from the list of parameters and use +that for the purpose of allocation. The matching `operator delete` +gets passed only the pointer to release and the originally requested +`size`. To have access to the correct allocator in `operator delete` +the allocator either needs to be stateless or a copy needs to be accessible +via the pointer passed to `operator delete`, e.g., stored at the +offset `size`. + +To avoid any cost introduced by type erasing an allocator +type as part of the `task` definition the expected allocator type +is obtained from the context argument `C` of `task`: + +```c++ +using allocator_type = ex::allocator_of_t; +``` + +This `using` alias uses `typename C::allocator_type` if present or +defaults to `std::allocator` otherwise. This +`allocator_type` has to be for the type `std::byte` (if necessary +it is possible to relax that constraint). + +The allocator used for the coroutine frame should also be used for +any other allocators needed for the coroutine itself, e.g., when +type erasing something needed for its operation (although in most +cases a small object optimization would be preferable and sufficient). +Also, the allocator should be made available to child operations +via the respective receiver's environment using the `get_allocator` +query. The arguments passed to the coroutine are also available to +the constructor of the promise type (if there is a matching on) and +the allocator can be obtained from there: + +```c++ +struct allocator_aware_context { + using allocator_type = pmr::polymorphic_allocator; +}; +fixed_resource<2048> resource; + +ex::sync_wait([](auto&&, auto* resource) + -> ex::task { + auto alloc = co_await ex::read_env(ex::get_allocator); + use(alloc); +}(allocator_arg, &resource)); +``` + +## Environment Support + +When `co_await`ing child operations these may want to access an +environment. Ideally, the coroutine would expose the environment +from the receiver it gets `connect`ed to. Doing so isn't directly +possible because the coroutine types doesn't know about the receiver +type which in turn determines the environment type. Also, the +queries don't know the type they are going to return. Thus, some +extra mechanisms are needed to provide an environment. + +A basic environment can be provided by some entities already known +to the coroutine, though: + +- The `get_scheduler` query should provide the scheduler maintained + for [scheduler affinity](#scheduler-affinity) whose type is + determined based on the coroutine's context using + `ex::scheduler_of_t`. +- The `get_allocator` query should provide the + [coroutine's allocator](#allocator-support) whose type is + determined based on the coroutine's context using `ex::allocator_of_t`. + The allocator gets initialized when constructing the promise type. +- The `get_stop_token` query should provide a stop token + from a stop source which is linked to the stop token obtained + from the receiver's environment. The type of the stop source + is determined from the coroutine's context using + `ex::stop_source_of_t` and defaults to `ex::inplace_stop_source`. + Linking the stop source can be delayed until the first stop + token is requested or omitted entirely if `stop_possible()` + returns `false` or if the stop token type of the coroutine's + receiver matches that of `ex::stop_source_of_t`. + +For any other environment query the context `C` of `task` can +be used. The coroutine can maintain an instance of type `C`. In many +cases queries from the environment of the coroutine's `receiver` need +to be forwarded. Let `env` be `get_env(receiver)` and `Env` +be the type of `env`. `C` gets optionally constructed +with access to the environment: + + 1. If `C::env_type` is a valid type the coroutine + state will contain an object `own_env` of this type which is + constructed with `env`. The object `own_env` will live at least + as long as the `C` object maintained and `C` is constructed + with a reference to `own_env`, allowing `C` to reference type-erased + representations for query results it needs to forward. + 2. Otherwise, if `C(env)` is valid the `C` object is constructed + with the result of `get_env(receiver)`. Constructing the context + with the receiver's environment provides the opportunity to + store whatever data is needed from the environment to later + respond to queries as well. + 3. Otherwise, `C` is default constructed. This option typically + applies if `C` doesn't need to provide any environment queries. + +Any query which isn't provided by the coroutine but is available +from the context `C` is forwarded. Any other query shouldn't be +part of the overload set. + +For example: + +```c++ +struct context { + int value{}; + int query(get_value_t const&) const noexcept { return this->value; } + context(auto const& env): value(get_value(env)) {} +}; + +int main() { + ex::sync_wait( + ex::write_env( + []->demo::task { + auto sched(co_await ex::read_env(get_scheduler)); + auto value(co_await ex::read_env(get_value)); + std::cout << "value=" << value << "\n"; + // ... + }(), + ex::make_env(get_value, 42) + ) + ); +} +``` + +## Support For Requesting Cancellation/Stopped + +When a coroutine task executes the actual work it may listen to +a stop token to recognize that it got canceled. Once it recognizes +that its work should be stopped it should also complete with +`set_stopped(rcvr)`. There is no special syntax needed as that is the +result of using `just_stopped()`: + +```c++ +co_await ex::just_stopped(); +``` + +The sender `just_stopped()` completes with `set_stopped()` causing +the coroutine to be canceled. Any other sender completing with +`set_stopped()` can also be used. + +## Error Reporting + +The sender/receiver approach to error reporting is for operations +to complete with a call to `set_error(rcvr, err)` for some receiver +object `rcvr` and an error value `err`. The details of the completions +are used by algorithms to decide how to proceed. For example, if +any of the senders of `when_all(sndr...)` fails with a `set_error_t` +completion the other senders are stopped and the overall operation +fails itself forwarding the first error. Thus, it should be possible +for coroutines to complete with a `set_error_t` completion. Using a +`set_value_t` completion using an error value isn't quite the same +as these are not detected as errors by algorithms. + +The error reporting used for +[`unifex`](https://github.com/facebookexperimental/libunifex) and +[stdexec](https://github.com/NVIDIA/stdexec) is to turn an exception +escaping from the coroutine into a `set_error_t(std::exception_ptr)` +completion: when `unhandled_exception()` is called on the promise +type the coroutine is suspended and the function can just call +`set_value(r, std::get_current_exception())`. There are a few +limitations with this approach: + +1. The only supported error completion is `set_error_t(std::exception_ptr)`. + While the thrown exception can represent any error type and + `set_error_t` completions from `co_await`ed operations resulting + in the corresponding error being thrown it is better if the + other error types can be reported, too. +2. To report an error an exception needs to be thrown. In some + environments it is preferred to not throw exception or exceptions + may even be entirely banned or disabled which means that there + isn't a way to report errors from coroutines unless a different + mechanism is provided. +3. To extract the actual error information from `std::exception_ptr` + the exception has to be rethrown. +4. The completion signatures for `task` necessarily contain + `set_error_t(std::exception_ptr)` which is problematic when + exceptions are unavailable: `std::exception_ptr` may also be + unavailable. Also, without exception as it is impossible to + decode the error. It can be desirable to have coroutine which + don't declare such a completion signature. + +Before going into details on how errors can be reported it is +necessary to provide a way for `task` to control the error +completion signatures. Similar to the return type the error types +cannot be deduced from the coroutine body. Instead, they can be +declared using the context type `C`: + +- If present, `typename C::error_signatures` is used to declare the + error types. This type needs to be a specialization of + `completion_signatures` listing the valid `set_error_t` + completions. +- If this nested type is not present, + `completion_signatures` is + used as a default. + +The name can be adjusted and it would be possible to use a different +type list template and listing the error types. The basic idea would +remain the same, i.e., the possible error types are declared via the +context type. + +Reporting an error by having an exception escape the coroutine is +still possible but it doesn't necessarily result in a `set_error_t`: +If an exception escapes the coroutine and `set_error_t(std::exception_ptr)` +isn't one of the supported the `set_error_t` completions, +`std::terminate()` is called. If an error is explicitly reported +somehow, e.g., using one of the approaches described below, +and the error type isn't supported by the context's `error_signatures`, the program is ill-formed. + +The discussion below assumes the use of the class template `with_error` +to indicate that the coroutine completed with an error. It can be as +simple as + +```c++ +template struct with_error{ E error; }; +``` + +The name can be different although it shouldn't collide with already +use names (like `error_code` or `upon_error`). Also, in some cases +there isn't really a need to wrap the error into a recognizable +class template. Using a marker type probably helps with readability +and avoiding ambiguities in other cases. + +Besides exceptions there are three possible ways how a coroutine +can be exited: + +1. The coroutine is exited when using `co_return`, optionally + with an argument. Flowing off the end of a coroutine is equivalent + to explicitly using `co_return;` instead of flowing off. It + would be possible to turn the use of + + ```c++ + co_return with_error{err}; + ``` + + into a `set_error(std::move(rcvr), err)` completion. + + One restriction with this approach is that for a + `task` the body can't contain `co_return with_error{e};`: the + `void` result requires that the promise type contains a function + `return_void()` and if that is present it isn't possible to + also have a `return_value(T)`. + +2. When a coroutine uses `co_await a;` the coroutine is in a suspended + state when `await_suspend(...)` of some awaiter is entered. + While the coroutine is suspended it can be safely destroyed. It + is possible to complete the coroutine in that state and have the + coroutine be cleaned up. This approach is used when the awaited + operation completes with `set_stopped()`. It is possible to + call `set_error(std::move(rcvr), err)` for some receiver `rcvr` + and error `err` obtained via the awaitable `a`. Thus, using + + ```c++ + co_await with_error{err}; + ``` + + could complete with `set_error(std::move(rcvr), err)`. + + Using the same notation for awaiting outstanding operations and + returning results from a coroutine is, however, somewhat + surprising. The name of the awaiter may need to become more + explicit like `exist_coroutine_with_error` if this approach should + be supported. + +3. When a coroutine uses `co_yield v;` the promise member + `yield_value(T)` is called which can return an awaiter `a`. + When `a`'s `await_suspend()` is called, the coroutine is suspended + and the operation can complete accordingly. Thus, using + + ```c++ + co_yield with_error{err}; + ``` + + could complete with `set_error(std::move(rcvr), err)`. Using + `co_yield` for the purpose of returning from a coroutine with + a specific result seems more expected than using `co_await`. + +There are technically viable options for returning an error from a +coroutine without requiring exceptions. Whether any of them is +considered suitable from a readability point of view is a separate +question. + +One concern which was raised with just not resuming the coroutine +is that the time of destruction of variables used by the coroutine +is different. The promise object can be destroyed before completing +which might address the concern. + +Using `co_await` or `co_yield` to propagate error results out of +the coroutine has a possibly interesting variation: in both of these +case the error result may be conditionally produced, i.e., it is +possible to complete with an error sometimes and to produce a value +at other times. That could allow a pattern (using `co_yield` for the +potential error return): + +```c++ +auto value = co_yield when_error(co_await into_expected(sender)); +``` + +The subexpression `into_expected(sender)` could turn the `set_value_t` +and `set_error_t` into a suitable `std::expected>` +always reported using a `set_value_t` completion (so the `co_await` +doesn't throw). The corresponding `std::expected` becomes the result +of the `co_await`. Using `co_yield` with `when_error(exp)` where `exp` +is an expected can then either produce `exp.value()` as the result +of the `co_yield` expression or it can result in the coroutine +completing with the error from `exp.error()`. Using this approach +produces a fairly compact approach to propagating the error retaining +the type and without using exceptions. + +## Avoiding Stack Overflow + +It is easy to use a coroutine to accidentally create a stack overflow +because loops don't really execute like loops. For example, a +coroutine like this can easily result in a stack overflow: + +```c++ +ex::sync_wait(ex::write_env( + []() -> ex::task { + for (int i{}; i < 1000000; ++i) + co_await ex::just(i); + }(), + ex::make_env(ex::get_scheduler, ex::inline_scheduler{}) +)); +``` + +The reason this innocent looking code creates a stack overflow is +that the use of `co_await` results in some function calls to suspend +the coroutine and then further function calls to resume the coroutine +(for a proper explanation see, e.g., Lewis Baker's +[Understanding Symmetric Transfer](https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer)). +As a result, the stack grows with each iteration of the loop until +it eventually overflows. + +With senders it is also not possible to use symmetric transfer to +combat the problem: to achieve the full generality and composing +senders, there are still multiple function calls used, e.g., when +producing the completion signal. Using `get_completion_behaviour` +from the proposal [A sender query for completion behavior](https://wg21.link/P3206) +could allow detecting senders which complete synchronously. In these +cases the stack overflow could be avoided relying on symmetric transfer. + +When using scheduler affinity the transfer of control via a scheduler +which doesn't complete immediately does avoid the risk of stack +overflow: even when the `co_await`ed work immediately completes as +part of the `await_suspend` call of the created awaiter the coroutine +isn't immediately resumed. Instead, the work is scheduled and the +coroutine is suspended. The thread unwinds its stack until it +reaches its own scheduling and picks up the next entity to execute. + +When using `sync_wait(sndr)` the `run_loop`'s scheduler is used and +it may very well just resume the just suspended coroutine: when +there is scheduling happening as part of scheduler affinity it +doesn't mean that work gets scheduled on a different thread! + +The problem with stack overflows does remain when the work resumes +immediately despite using scheduler affinity. That may be the case +when using an inline scheduler, i.e., a scheduler with an operation +state whose `start()` immediately completes: the scheduled work gets +executed as soon as `set_value(std::move(rcvr))` is called. + +Another potential for stack overflows is when optimizing the behavior +for work which is known to not move to another scheduler: in that case +there isn't really any need to use `continue_on` to get back to the +scheduler where the operation was started! The execution remained +on that scheduler all along. However, not rescheduling the work +means that the stack isn't unwound. + +Since `task` uses scheduler affinity by default, stack overflow +shouldn't be a problem and there is no separate provision required +to combat stack overflow. If the implementation chooses to avoid +rescheduling work it will need to make sure that doing so doesn't +cause any problems, e.g., by rescheduling the work sometimes. When +using an inline scheduler the user will need to be very careful to +not overflow the stack or cause any of the various other problems +with executing immediately. + +## Asynchronous Clean-Up + +Asynchronous clean-up of objects is an important facility. Both +[`unifex`](https://github.com/facebookexperimental/libunifex) and +[stdexec](https://github.com/NVIDIA/stdexec) provide some facilities +for asynchronous clean-up in their respective coroutine task. Based +on the experience the recommendation is to do something different! + +The recommended direction is to support asynchronous resources +independent of a coroutine task. For example the +[async-object](https://wg21.link/p2849) proposal is in this direction. +There is similar work ongoing in the context of +[Folly](https://github.com/facebook/folly). Thus, there is currently +no plan to support asynchronous clean-up as part of the `task` +implementation. Instead, it can be composed based on other facilities. + +# Caveats + +The use of coroutines introduces some issues which are entirely +independent of how specific coroutines are defined. Some of these +were brought up on prior discussions but they aren't anything which +can be solved as part of any particular coroutine implementation. +In particular: + +1. As `co_await`ing the result of an operation (or `co_yield`ing a + value) may suspend a coroutine, there is a potential to + introduce problems when resources which are meant to be held temporarily + are held when suspending. For example, holding a lock to a mutex + while suspending a coroutine can result in a different thread + trying to release the lock when the coroutine is resumed + (scheduler affinity will move the resumed coroutine + to the same scheduler but not to the same thread). +2. Destroying a coroutine is only safe when it is suspended. For + the task implementation that means that it shall only call a + completion handler once the coroutine is suspended. That part + is under the control of the coroutine implementation. However, + there is no way to guard against users explicitly destroying a + coroutine from within its implementation or from another thread + while it is not suspended: that's akin to destroying an object + while it being used. +3. Debugging asynchronous code doesn't work with the normal approaches: + there is generally no suitable stack as work gets resumed from + some run loop which doesn't tell what set up the original work. + To improve on this situation, _async stack traces_ linking + different pieces of outstanding work together can help. At + [CppCon 2024](https://cppcon.org/cppcon-2024-timeline/) Ian + Petersen and Jessica Wong presented how that may work ([watch + the video](https://youtu.be/nHy2cA9ZDbw?si=RDFty43InNoJxJNN)). + Implementations should consider adding corresponding support + and enhance tooling, e.g., debuggers, to pick up on async stack + traces. However, async stack support itself isn't really something + which one coroutine implementation can enable. + +While these issues are important this proposal isn't the right place +to discuss them. Discussion of these issues should be delegated +to suitable proposals wanting to improve this situation in some +form. + +# Questions + +This section lists questions based on the design discussion +above. Each one has a recommendation and a vote is only needed +if there opinions deviating from the recommendation. + +- Result type: expand `as_awaitable(sndr)` to support more than + one `set_value_t(T...)` completion? Recommendation: no. +- Result type: add transformation algorithms like `into_optional`, + `into_expected`? Recommendation: no, different proposals. +- Scheduler affinity: should `task` support scheduler affinity? + Recommendation: yes. +- Scheduler affinity: require a `get_scheduler()` query on the + receiver's environments? Recommendation: yes. +- Scheduler affinity: add a definition for `inline_scheduler` + (using whatever name) to support disabling scheduler affinity? + Recommendation: yes. +- Allocator support: should `task` support allocators (default + `std::allocator`)? Recommendation: yes. +- Error reporting: should it be possible to return an error + without throwing an exception? Recommendation: yes. +- Error reporting: how should errors be reported? Recommendation: + using `co_yield with_error(e). +- Error reporting: should `co_yield when_error(expected)` be + supported? Recommendation: yes (although weakly). +- Clean-up: should asynchronous clean-up be supported? Recommendation: + no. + +# Implementation + +An implementation of `task` as proposed in this document is available +from [`beman::task`](https://github.com/bemanproject/task). This +implementation hasn't received much use, yet, as it is fairly new. It +is setup to be buildable and provides some examples as a starting +point for experimentation. + +Coroutine tasks very similar although not identical to the one +proposed are used in multiple projects. In particular, there are +three implementations in wide use: + +- [`Folly::Task`](https://github.com/facebook/folly/blob/main/folly/coro/Task.h) +- [`unifex::Task`](https://github.com/facebookexperimental/libunifex/blob/main/include/unifex/task.hpp) +- [`stdexec::task`](https://github.com/NVIDIA/stdexec/blob/main/include/exec/task.hpp) + +The first one +([`Folly::Task`](https://github.com/facebook/folly/blob/main/folly/coro/Task.h)) +isn't based on sender/receiver. Usage experience from all three +have influenced the design of `task`. + +# Acknowledgments + +We would like to thank Ian Petersen, Alexey Spiridonov, and Lee +Howes for comments on drafts of this proposal and general guidance. + +# Proposed Wording + +In [version.syn], add a row + +:::add + +```c++ +#define __cpp_lib_task YYYMML // @_also in_@ +``` + +::: + +In [except.terminate] paragraph 1 add this bullet at the end of Note 1: + +:::note +These situations are + +- ... + +- when unhandled_stopped is called on a with_awaitable_senders object ([exec.with.awaitable.senders]) whose continuation is not a handle to a coroutine whose promise type has an unhandled_stopped member function[.]{.rm}[, or]{.add} + +:::add + +- when an exception is thrown from a coroutine `std::execution::task` + which doesn't support a `std::execution::set_error_t(std::execption_ptr)` completion. + +::: + +::: + +In [execution.syn]{.sref} add declarations for the new classes: + +```c++ +namespace std::execution { + ... + // [exec.with.awaitable.senders] + template<@_class-type_@ Promise> + struct with_awaitable_senders; + +``` + +:::add + +```c++ + // @[exec.affine.on]{.sref}@ + struct affine_on_t { @_unspecified_@ }; + inline constexpr affine_on_t affine_on; + + // @[exec.inline.scheduler]{.sref}@ + class inline_scheduler; + + // @[exec.task.scheduler]{.sref}@ + class task_scheduler; + + // @[exec.task]{.sref}@ + template + class task; +``` + +::: + +```c++ +} +``` + +Add new subsections for the different classes at the end of [exec]{.sref}: + +::: draftnote +Everything below is text meant to go to the end of the [exec]{.sref} +section without any color highlight of what it being added. +::: + +## `execution::affine_on` [exec.affine.on] + +[1]{.pnum} `affine_on` adapts a sender into one that completes on + the specified scheduler. If the algorithm determines that the + adapted sender already completes on the correct scheduler it + can avoid any scheduling operation. + +[2]{.pnum} The name `affine_on` denotes a pipeable sender adaptor + object. For subexpressions `sch` and `sndr`, if `decltype((sch))` + does not satisfy `scheduler`, or `decltype((sndr))` does not + satisfy `sender`, `affine_on(sndr, sch)` is ill-formed. + +[3]{.pnum} Otherwise, the expression `affine_on(sndr, sch)` is + expression-equivalent to: + +```c++ + transform_sender(@_get-domain-early_@(sndr), @_make-sender_@(affine_on, sch, sndr)) +``` + +except that `sndr` is evaluated only once. + +[4]{.pnum} The exposition-only class template `@_impls-for_@` is specialized + for `affine_on_t` as follows: + +```c++ + namespace std::execution { + template <> + struct @_impls-for_@: @_default-impls_@ { + static constexpr auto @_get-attrs_@ = + [](const auto& data, const auto& child) noexcept -> decltype(auto) { + return @_JOIN-ENV_@(@_SCHED-ATTRS_@(data), @_FWD-ENV_@(get_env(child))); + }; + }; + } +``` + +[5]{.pnum} Let `out_sndr` be a subexpression denoting a sender returned + from `affine_on(sndr, sch)` or one equal to such, and let + `OutSndr` be the type `decltype((out_sndr))`. Let `out_rcvr` + be a subexpression denoting a receiver that has an environment + of type `Env` such that `sender_in` is `true`. + Let `op` be an lvalue referring to the operation state that + results from connecting `out_sndr` to `out_rcvr`. Calling + `start(op)` will start `sndr` on the current execution agent + and execute completion operations on `out_rcvr` on an execution + agent of the execution resource associated with `sch`. If the + current execution resource is the same as the execution resource + associated with `sch`, the completion operation on `out_rcvr` + may be called before `start(op)` completes. If scheduling onto + `sch` fails, an error completion on `out_rcvr` shall be executed + on an unspecified execution agent. + +## `execution::inline_scheduler` [exec.inline.scheduler] + +```c++ +namespace std::execution { + class inline_scheduler { + class @_inline-sender_@; // exposition only + template + class @_inline-state_@; // exposition only + + public: + using scheduler_concept = scheduler_t; + + constexpr @_inline-sender_@ schedule() noexcept { return {}; } + constexpr bool operator== (const inline_scheduler&) const noexcept = default; + }; +} +``` + +[1]{.pnum} `inline_scheduler` is a class that models `scheduler` +[exec.scheduler]{.sref}. All objects of type `inline_scheduler` are equal. + +[2]{.pnum} `@_inline-sender_@` is an exposition-only type that satisfies +`sender`. The type
+`completion_signatures_of_t<@_inline-sender_@>` +is `completion_signatures`. + +[3]{.pnum} Let `@_sndr_@` be an expression of type `@_inline-sender_@`, let `@_rcvr_@` +be an expression such that `receiver_of` is `true` where +`CS` is `completion_signatures`, then: + +- [3.1]{.pnum} the expression `connect(@_sndr_@, @_rcvr_@)` has + type `@_inline-state_@>` and is potentially-throwing if and + only if + `((void)@_sndr_@, auto(@_rcvr_@))` is potentially-throwing, and +- [3.2]{.pnum} the expression `get_completion_scheduler(get_env(@_sndr_@))` + has type `inline_scheduler` and is potentially-throwing if and only if `get_env(@_sndr_@)` is potentially-throwing. + +[4]{.pnum} Let `@_o_@` be a non-`const` lvalue of type `@_inline-state_@`, and + let `REC(@_o_@)` be a non-`const` lvalue reference to an object of type `Rcvr` + that was initialized with the expression `@_rcvr_@` passed to an invocation + of `connect` that returned `@_o_@`, then: + +- [4.1]{.pnum} the object to which `REC(@_o_@)` refers remains valid for the lifetime + of the object to which `@_o_@` refers, and +- [4.2]{.pnum} the expression `start(@_o_@)` is equivalent to `set_value(std::move(REC(@_o_@)))`. + +## `execution::task_scheduler` [exec.task.scheduler] + +```c++ +namespace std::execution { + class task_scheduler { + class @_sender_@; // exposition only + template + class @_state_@; // exposition only + + public: + using scheduler_concept = scheduler_t; + + template > + requires (!same_as>) + && scheduler + explicit task_scheduler(Sch&& sch, Allocator alloc = {}); + + @_sender_@ schedule(); + + friend bool operator== (const task_scheduler& lhs, const task_scheduler& rhs) + noexcept; + template + requires (!same_as) + && scheduler + friend bool operator== (const task_scheduler& lhs, const Sch& rhs) noexcept; + + private: + shared_ptr @_sch_@_; // @_exposition only_@ + }; +} +``` + +[1]{.pnum} `task_scheduler` is a class that models `scheduler` + [exec.scheduler]{.sref}. + Given on object `s` of type `task_scheduler`, let `@_SCHED_@(s)` be the object owned + by `s.@_sch_@_`. + +```c++ +template > + requires(!same_as>) && scheduler +explicit task_scheduler(Sch&& sch, Allocator alloc = {}); +``` + +[2]{.pnum} _Effects_: Initialize `@_sch_@_` with + `allocate_shared>(alloc, std::forward(sch))`. + +[3]{.pnum} _Recommended practice_: Implementations should avoid the + use of dynamically allocated memory for small scheduler objects. + +[4]{.pnum} _Remarks_: + Any allocations performed by construction of `@_sender_@` or `@_state_@` + objects resulting from calls on `*this` are performed using a copy + of `alloc`. + +```c++ +@_sender_@ schedule(); +``` + +[5]{.pnum} _Effects_: Returns an object of type `@_sender_@` containing + a sender initialized with `schedule(@_SCHED_@(*this))`. + +```c++ +bool operator== (const task_scheduler& lhs, const task_scheduler& rhs) noexcept; +``` + +[6]{.pnum} _Effects_: Equivalent to: `return lhs == @_SCHED_@(rhs);` + +```c++ +template + requires (!same_as) + && scheduler +bool operator== (const task_scheduler& lhs, const Sch& rhs) noexcept; +``` + +[7]{.pnum} _Returns_: `false` if type of `@_SCHED_@(lhs)` is not +`Sch`, otherwise `@_SCHED_@(lhs) == rhs;` + +```c++ +class task_scheduler::@_sender_@ { // @_exposition only_@ +public: + using sender_concept = sender_t; + + template + @_state_@ connect(Rcvr&& rcvr); +}; +``` + +[8]{.pnum} `@_sender_@` is an exposition-only class that models + `sender` [exec.sender]{.sref} and for which + `completion_signatures_of_t<@_sender_@>` denotes: + +```c++ +completion_signatures< + set_value_t(), + set_error_t(error_code), + set_error_t(exception_ptr), + set_stopped_t()> +``` + +[9]{.pnum} Let `sch` be an object of type `task_scheduler` and + let `sndr` be an object of type `@_sender_@` obtained from + `schedule(sch)`. Then + `get_completion_scheduler(get_env(sndr)) == sch` + is `true`. The object `@_SENDER_@(sndr)` is the sender object + contained by `sndr` or an object move constructed from it. + +```c++ +template +@_state_@ connect(Rcvr&& rcvr); +``` + +[10]{.pnum} _Effects_: Let `r` be an object of a type that models +`receiver` and whose completion handlers result in invoking the +corresponding completion handlers of `rcvr` or copy thereof. +Returns an object of type `@_state_@` containing an operation +state object initialized with + `connect(@_SENDER_@(*this), std::move(r))`. + +```c++ +template +class task_scheduler::@_state_@ { // @_exposition only_@ +public: + using operation_state_concept = operation_state_t; + + void start() & noexcept; +}; +``` + +[11]{.pnum} `@_state_@` is an exposition-only class template whose specializations + model `operation_state` [exec.opstate]{.sref}. + +```c++ +void start() & noexcept; +``` + +[12]{.pnum} _Effects_: Equivalent to `start(st)` where `st` is + the operation state object contained by `*this`. + +## `execution::task` [exec.task] + +### `task` Overview [task.overview] + +[1]{.pnum} The `task` class template represents a sender that can +be used as the return type of coroutines. The first template parameter +`T` defines the type value completion datum ([exec.async.ops]{.sref}) +if `T` is not `void`. Otherwise, there are no value completion +datums. Inside coroutines returning `task` the operand of +`co_return` (if any) becomes the argument of `set_value`. The +second template parameter `Environment` is used to customize the +behavior of `task`. + +### Class template task [task.class] + +```c++ +namespace std::execution { + template + class task { + // [task.state] + template + class @_state_@; // @_exposition only_@ + + public: + using sender_concept = sender_t; + using completion_signatures = @_see below_@; + using allocator_type = @_see below_@; + using scheduler_type = @_see below_@; + using stop_source_type = @_see below_@; + using stop_token_type = decltype(declval().get_token()); + using error_types = @_see below_@; + + // [task.promise] + class promise_type; + + task(task&&) noexcept; + ~task(); + + template + @_state_@ connect(Rcvr&& rcvr); + + private: + coroutine_handle @_handle_@; // @_exposition only_@ + }; +} +``` + +[1]{.pnum} `task` models `sender` [exec.snd]{.sref} if `T` is `void`, a reference type, or an cv-unqualified non-array object type and `E` is class type. Otherwise a program that instantiates the definition of `task` is ill-formed. + +[2]{.pnum} The nested types of `task` template specializations +are determined based on the `Environment` parameter: + +- [2.1]{.pnum} `allocator_type` is `Environment::allocator_type` +if that qualified-id is valid and denotes a type, +`allocator` otherwise. +- [2.2]{.pnum} `scheduler_type` is `Environment::scheduler_type` if that qualified-id is valid and denotes a type, `task_scheduler` +otherwise. +- [2.3]{.pnum} `stop_source_type` is `Environment::stop_source_type` +if that qualified-id is valid and denotes a type, `inplace_stop_source` +otherwise. +- [2.4]{.pnum} `error_types` is `Environment::error_types` + if that qualified-id is valid and denotes a type, + `completion_signatures` otherwise. + +[3]{.pnum} A program is ill-formed if + `error_types` is not a + specialization of `completion_signatures` or + `ErrorSigs` contains an element which is not of the form `set_error_t(E)` + for some type `E`. + +[4]{.pnum} The type alias `completion_signatures` + is a specialization of `execution::completion_signatures` with + the template arguments (in unspecified order): + +- [4.1]{.pnum} `set_value_t()` if `T` is `void`, and `set_value_t(T)` otherwise; +- [4.2]{.pnum} template arguments of the specialization of `execution::completion_signatures` denoted by `error_types`; and +- [4.3]{.pnum} `set_stopped_t()`. + +[5]{.pnum} `allocator_type` shall meet the _Cpp17Allocator_ requirements. + +### Task Members [task.members] + +```c++ +task(task&& other) noexcept; +``` + +[1]{.pnum} _Effects:_ Initializes `@_handle_@` with `exchange(other.@_handle_@, {})`. + +```c++ +~task(); +``` + +[2]{.pnum} _Effects:_ Equivalent to: + +```c++ + if (@_handle_@) + @_handle_@.destroy(); +``` + +```c++ +template +@_state_@ connect(R&& recv); +``` + +[3]{.pnum} _Preconditions:_ `bool(@_handle_@)` is `true`. + +[4]{.pnum} _Effects:_ Equivalent to: `return @_state_@(exchange(@_handle_@, {}), std::forward(recv));` + +### Class template task::state [task.state] + +```c++ +namespace std::execution { + template + template + class task::@_state_@ { // @_exposition only_@ + public: + using operation_state_concept = operation_state_t; + + template + @_state_@(coroutine_handle h, Rcvr&& rr); + ~@_state_@(); + void start() & noexcept; + +private: + using @_own-env-t_@ = @_see below_@; // @_exposition only_@ + coroutine_handle @_handle_@; // @_exposition only_@ + remove_cvref_t @_rcvr_@; // @_exposition only_@ + @_own-env-t_@ @_own-env_@; // @_exposition only_@ + Environment @_environment_@; // @_exposition only_@ + }; +} +``` + +[1]{.pnum} The type `@_own-env-t_@` is + `Environment::template env_type()))>` + if that qualified-id is valid and denotes a type, + `env<>` otherwise. + +```c++ +template +@_state_@(coroutine_handle h, Rcvr&& rr); +``` + +[2]{.pnum} _Effects:_ Initializes + +- [2.1]{.pnum} `@_handle_@` with `std::move(h)`; +- [2.2]{.pnum} `@_rcvr_@` with `std::forward(rr)`; +- [2.3]{.pnum} `@_own-env_@` + with `@_own-env-t_@(get_env(@_rcvr_@))` if that expression + is valid and `@_own-env-t_@()` otherwise; + If neither of these expressions is valid, the program is ill-formed. +- [2.4]{.pnum} `@_environment_@` with `Environment(@_own-env_@)` if that expression is + valid, otherwise `Environment(get_env(@_rcvr_@))` + if this expression is valid, otherwise `Environment()`. + If neither of these expressions is valid, the program is ill-formed. + +```c++ +~@_state_@(); +``` + +[3]{.pnum} _Effects:_ Equivalent to: + +```c++ + if (@_handle_@) + @_handle_@.destroy(); +``` + +```c++ +void start() & noexcept; +``` + +[4]{.pnum} _Effects:_ Let `prom` be the object `@_handle_@.promise()`. +Associates `@_STATE_@(prom)`, `@_RCVR_@(prom)`, and `@_SCHED_@(prom)` with `*this` +as follows: + +- [4.1]{.pnum} `@_STATE_@(prom)` is `*this`. +- [4.2]{.pnum} `@_RCVR_@(prom)` is `@_rcvr_@`. +- [4.3]{.pnum} `@_SCHED_@(prom)` is the object initialized with + `scheduler_type(get_scheduler(get_env(@_rcvr_@)))` if that expression is + valid and `scheduler_type()` otherwise. If neither of these + expressions is valid, the program is ill-formed. + +Let `st` be `get_stop_token(get_env(@_rcvr_@))`. Initializes `prom.@_token_@` + and `prom.@_source_@` such that + +- [4.4]{.pnum} `prom.@_token_@.stop_requested()` returns `st.stop_requested()`; +- [4.5]{.pnum} `prom.@_token_@.stop_possible()` returns `st.stop_possible()`; and +- [4.6]{.pnum} for types `Fn` and `Init` such that both `invocable` and `constructible_from` are modeled, `stop_token_type::callback_type` models `@_stoppable-callback-for_@`. + +After that invokes `@_handle_@.resume()`. + +### Class task::promise_type [task.promise] + +```c++ +namespace std::execution { + template + struct with_error { + using type = remove_cvref_t; + type error; + }; + template + with_error(E) -> with_error; + + template + struct change_coroutine_scheduler { + using type = remove_cvref_t; + type scheduler; + }; + template + change_coroutine_scheduler(Sch) -> change_coroutine_scheduler; + + template + class task::promise_type { + public: + template + promise_type(const Args&... args); + + task get_return_object() noexcept; + + auto initial_suspend() noexcept; + auto final_suspend() noexcept; + + void uncaught_exception(); + coroutine_handle<> unhandled_stopped(); + + void return_void(); // present only if is_void_v is true; + template + void return_value(V&& value); // present only if is_void_v is false; + + template + @_unspecified_@ yield_value(with_error error); + + template + auto await_transform(A&& a); + template + auto await_transform(change_coroutine_scheduler sch); + + @_unspecified_@ get_env() const noexcept; + + template + void* operator new(size_t size, Args&&... args); + + void operator delete(void* pointer, size_t size) noexcept; + + private: + using @_error-variant_@ = @_see below_@; // @_exposition only_@ + + allocator_type @_alloc_@; // @_exposition only_@ + stop_source_type @_source_@; // @_exposition only_@ + stop_token_type @_token_@; // @_exposition only_@ + optional @_result_@; // @_exposition only_@; present only if is_void_v is false; + @_error-variant_@ @_errors_@; // @_exposition only_@ + }; +} +``` + +[1]{.pnum} Let `prom` be an object of `promise_type` and let `tsk` + be the `task` object created by `prom.get_return_object()`. The + description below refers to objects `@_STATE_@(prom)`, + `@_RCVR_@(prom)`, and `@_SCHED_@(prom)` associated with `tsk` + during evaluation of `task::@_state_@::start` for some + receiver `Rcvr` [task.state]{.sref}. + +[2]{.pnum} `@_error-variant_@` is a `variant...>`, with duplicate +types removed, where `E...` are template arguments of the +specialization of `execution::completion_signatures` denoted by +`error_types`. + +```c++ +template +promise_type(const Args&... args); +``` + +[3]{.pnum} _Mandates:_ The first parameter of type `allocator_arg_t` (if any) is not the last parameter. + +[4]{.pnum} _Effects:_ If `Args` contains an element of type + `allocator_arg_t` then `@_alloc_@` is initialized with the + corresponding next element of `args`. Otherwise, `@_alloc_@` + is initialized with `allocator_type()`. + +```c++ +task get_return_object() noexcept; +``` + +[5]{.pnum} _Returns:_ A `task` object whose member `@_handle_@` is + `coroutine_handle::from_promise(*this)`. + +```c++ +auto initial_suspend() noexcept; +``` + +[6]{.pnum} _Returns:_ An awaitable object of unspecified type + ([expr.await]) whose member functions arrange for + +- [6.1]{.pnum} the calling coroutine to be suspended, +- [6.2]{.pnum} the coroutine to be resumed on an execution agent of the execution resource associated with `@_SCHED_@(*this)`. + +```c++ +auto final_suspend() noexcept; +``` + +[7]{.pnum} _Returns:_ An awaitable object of unspecified type + ([expr.await]) whose member functions arrange for the completion + of the asynchronous operation associated with `@_STATE_@(*this)` by invoking: + +- [7.1]{.pnum} + `set_error(std::move(@_RCVR_@(*this)),` `std::move(e))` + if `@_errors_@.index()` is greater than zero and + `e` is the value held by `@_errors_@`, otherwise +- [7.2]{.pnum} + `set_value(std::move(@_RCVR_@(*this)))` if `is_void` is `true`, + and otherwise +- [7.3]{.pnum} + `set_value(std::move(@_RCVR_@(*this)), *@_result_@)`. + +```c++ +template +auto yield_value(with_error err); +``` + +[8]{.pnum} _Mandates_ `std::move(err.error)` is convertible to + exactly one of the `set_error_t` argument types of `error_types`. Let `Cerr` + be that type. + +[9]{.pnum} _Returns:_ An awaitable object of unspecified type + ([expr.await]) whose member functions arrange for the calling + coroutine to be suspended and then completes + the asynchronous operation associated with `@_STATE_@(*this)` by invoking + `set_error(std::move(@_RCVR_@(*this)), Cerr(std::move(err.error)))`. + +```c++ +template +auto await_transform(Sender&& sndr) noexcept; +``` + +[10]{.pnum} _Returns_: If `same_as` is + `true` returns `as_awaitable(std::forward(sndr),` `*this)`; + otherwise returns + `as_awaitable(affine_on(std::forward(sndr), @_SCHED_@(*this)), *this)`. + +```c++ +template +auto await_transform(change_coroutine_scheduler sch) noexcept; +``` + +[11]{.pnum} _Effects:_ Equivalent to: `returns await_transform(just(exchange(@_SCHED_@(*this), scheduler_type(sch.scheduler))), *this);` + +```c++ +void uncaught_exception(); +``` + +[12]{.pnum} _Effects:_ If the signature `set_error_t(exception_ptr)` is + not an element of `error_types`, calls `terminate()` ([except.terminate]{.sref}). Otherwise, stores + `current_exception()` into `@_errors_@`. + +```c++ +coroutine_handle<> unhandled_stopped(); +``` + +[13]{.pnum} _Effects:_ Completes the asynchronous operation + associated with `@_STATE_@(*this)` by invoking + `set_stopped(std::move(@_RCVR_@(*this)))`. + +[14]{.pnum} _Returns:_ `noop_coroutine()`. + +```c++ +@_unspecified_@ get_env() const noexcept; +``` + +[15]{.pnum} _Returns:_ An object `env` + such that queries are forwarded as follows: + +- [15.1]{.pnum} `env.query(get_scheduler)` returns `scheduler_type(@_SCHED_@(*this))`. +- [15.2]{.pnum} `env.query(get_allocator)` returns `@_alloc_@`. +- [15.3]{.pnum} `env.query(get_stop_token)` returns `@_token_@`. +- [15.4]{.pnum} For any other query `q` and arguments `a...` a + call to `env.query(q, a...)` returns + `@_STATE_@(*this).environment.query(q, a...)` if this expression + is well-formed and `forwarding_query(q)` is well-formed and is `true`. Otherwise + `env.query(q, a...)` is ill-formed. + +```c++ +template +void* operator new(size_t size, const Args&... args); +``` + +[16]{.pnum} If there is no parameter with type `allocator_arg_t` + then let `alloc` be `Allocator()`. Let `arg_next` be the + parameter following the first `allocator_arg_t` parameter (if + any) and let `alloc` be `Allocator(arg_next)`. Then `PAlloc` + is `allocator_traits::template rebind_alloc` where + `U` is an unspecified type whose size and alignment are both + `__STDCPP_DEFAULT_NEW_ALIGNMENT__`. + +[17]{.pnum} _Mandates:_ + +- [17.1]{.pnum} The first parameter of type `allocator_arg_t` (if any) is not the last parameter. +- [17.2]{.pnum} `Allocator(arg_next)` is a valid expression if there is a parameter + of type `allocator_arg_t`. +- [17.3]{.pnum} `allocator_traits::pointer` is a pointer type. + +[18]{.pnum} _Effects:_ Initializes an allocator `palloc` of type `PAlloc` + with `alloc`. Uses `palloc` to allocate storage for the smallest + array of `U` sufficient to provide storage for a coroutine state + of size `size`, and unspecified additional state necessary to + ensure that `operator delete` can later deallocate this memory + block with an allocator equal to `palloc`. + +[19]{.pnum} _Returns:_ A pointer to the allocated storage. + +```c++ +void operator delete(void* pointer, size_t size) noexcept; +``` + +[20]{.pnum} _Preconditions:_ `pointer` was returned from an invocation + of the above overload of `operator new` with a size argument + equal to `size`. + +[21]{.pnum} _Effects:_ Deallocates the storage pointed to by `pointer` + using an allocator equal to that used to allocate it. diff --git a/subprojects/task/docs/P3796-task-issues.md b/subprojects/task/docs/P3796-task-issues.md new file mode 100644 index 000000000..df53c939c --- /dev/null +++ b/subprojects/task/docs/P3796-task-issues.md @@ -0,0 +1,1560 @@ +--- +title: Coroutine Task Issues +document: P3796R1 +date: 2025-08-14 +audience: + - Concurrency Working Group (SG1) + - Library Evolution Working Group (LEWG) + - Library Working Group (LWG) +author: + - name: Dietmar Kühl (Bloomberg) + email: +source: + - https://github.com/bemanproject/task/doc/issues.md +toc: true +--- + +After the [task proposal](https://wg21.link/p3552) was voted to be +forwarded to plenary for inclusion into C++26 a number of issues +were brought up. The issues were reported using various means. This +paper describes the issues and proposes potential changes to address +them. + +# Change History + +## R0 Initial Revision + +## R1: added more issues and discussion + +- Added the proposal to support `co_return { ... };` ([link](#consider-supporting-co_return-args)) +- Added a response to [P3801r0](https://wg21.link/P3801r0) "Concerns about the design of std::execution::task" ([link](#response-to-concerns-about-the-design-of-stdexecutiontask)) +- Added a concrete reason why `co_yield with_error(e)` is "clunky" ([link](#response-to-concerns-about-the-design-of-stdexecutiontask)) +- Added a discussion of `change_coroutine_scheduler` requiring the scheduler to be assignable ([link](#co_await-change_coroutine_schedulersched-requires-assignable-scheduler)) +- Added a discussion of adding `operator co_await` ([link](#sender-unaware-coroutines-should-be-able-to-co_await-a-task)) +- Added a discussion of `bulk` vs. `task_scheduler` ([link](#bulk-vs.-task_scheduler)) +- Added discussion of missing rvalue qualification ([link](#missing-rvalue-qualification)) +- Added issue about missing specification for `return_value` and `return_void` ([link](#return_value-and-return_void-have-no-specification)) +- Various minor fixes + +# General + +After LWG voted to forward the [task +proposal](https://wg21.link/p3552r3) +to be forwarded to plenary for inclusion into C++26 some issues +were brought up. The concerns range from accidental omissions (e.g., +`unhandled_stopped` lacking `noexcept`) via wording issues and +performance concerns to some design questions. In particular the +behavior of the algorithm `affine_on` used to implement scheduler +affinity for `task` received some questions which may lead to some +design changes and/or clarifications. This document discusses the +various issues raised and proposes ways to address some of them. + +One statement from the plenary was that `task` is the obvious name +for a coroutine task and we should get it right. There are certainly +improvements which can be applied. Although I'm not aware of anything +concrete beyond the raised issues there is probably potential for +improvements. If the primary concern is that there may be better +task interfaces in the future and a better task should get the name +`task`, it seems reasonable to rename the component. The original +name used for the component was `lazy` and feedback from Hagenberg +was to rename it `task`. In principle, any name could work, e.g., +`affine_task`. It seems reasonable to keep `task` in the name somehow +if `task` is to be renamed. + +Specifically for `task` it is worth pointing out that multiple +coroutine task components can coexist. Due to the design of coroutines +and sender/receiver different coroutine tasks can interoperate. + +# The Concerns + +Some concerns were posted to the LWG chat on Mattermost (they were +originally raised on a Discord chat where multiple of the authors +of sender/receiver components coordinate the work). Other concerns +were raised separately (and the phrasing isn't exactly that of the +originally, e.g., because it was stated in a different language). I +became aware of all but one of these concerns after the poll by LWG +to forward the proposal for inclusion into C++26. The rest of this +section discusses these concerns: + +## `affine_on` Concerns + +This section covers different concerns around the specification, +or rather lack thereof, of `affine_on`. The algorithm `affine_on` +is at the core of the scheduler affinity implementation: this +algorithm is used to establish the invariant that a `task` executes +on the currently installed scheduler. Originally, the `task` proposal +used `continue_on` in the specification but during the SG1 discussion +it was suggested that a differently named algorithm is used. The +original idea was that `affine_on(@_sndr_@, @_sch_@)` behaves like +`continues_on(@_sndr_@, @_sch_@)` but it is customized to avoid +actual scheduling when it knows that `@_sndr_@` completes on +`@_sch_@`. When further exploring the direction of using a different +algorithm than `continues_on` some additional potential for changed +semantics emerged (see below). + +The name `affine_on` was _not_ discussed by SG1. The +direction was "come up with a name". The current name just concentrates +on the primary objective of implementing scheduler affinity for +`task`. It can certainly use a different name. For example the +algorithm could be called `continues_inline_or_on` or +`affine_continues_on`. + +To some extend `affine_on`'s specification is deliberately vague +because currently the `execution` specification is lacking some +tools which would allow omission of scheduling, e.g., for `just` +which completes immediately with a result. While users can't +necessarily tap into the customisations, yet, implementation could +use tools like those proposed by [P3206](https://wg21.link/p3206) +"A sender query for completion behavior" using a suitable hidden +name while the proposal isn't adopted. + +### `affine_on` Default Implementation Lacks a Specification + +The wording of [`affine_on`](https://eel.is/c++draft/exec#affine.on) +doesn't have a specification for the default implementation. For +other algorithms the default implementation is specified. To resolve +this concern add a new paragraph in +[[exec.affine.on]](https://eel.is/c++draft/exec#affine.on) to provide +a specification for the default implementation (probably in front of +the current paragraph 5): + +::: add +> [5]{.pnum} Let `sndr` and `env` be subexpressions such that `Sndr` +> is `decltype((sndr))`. If `@_sender-for_@` is +> `false`, then the expression `affine_on.transform_sender(sndr, env)` is +> ill-formed; otherwise, it is equivalent to: +> +> ``` +> auto [_, sch, child] = sndr; +> return transform_sender( +> @_query-with-default_@(get_domain, sch, default_domain()), +> continues_on(std::move(child), std::move(sch))); +> ``` +> +> except that `sch` is only evaluated once. +::: + +The intention for `affine_on` was to all optimization/customization +in a way reducing the necessary scheduling: if the implementation +can determine if a sender completed already on the correct execution +agent it should be allowed to avoid scheduling. That may be achieved +by using `get_completion_scheduler` of the sender, +using (for now implementation specific) queries like those proposed +by +[P3206 "A sender query for completion behavior"](https://wg21.link/P3206), +or some other means. Unfortunately, the specification proposed above +seems to disallow implementation specific techniques to avoid +scheduling. Future revisions of the standard could require some of +the techniques to avoid scheduling assuming the necessary infrastructure +gets standardized. + +### `affine_on` Semantics Are Not Clear + +The wording in +[`affine_on` p5](https://eel.is/c++draft/exec#affine.on-5) says: + +> ... Calling `start(op)` will start `sndr` on the current execution +> agent and execution completion operations on `out_rcvr` on an +> execution agent of the execution resource associated with `sch`. +> If the current execution resource is the same as the execution +> resource associated with `sch`, the completion operation on +> `out_rcvr` may be invoked on the same thread of execution before +> `start(op)` completes. If scheduling onto `sch` fails, an error +> completion on `out_rcvr` shall be executed on an unspecified +> execution agent. + +The sentence "If the current execution resource is the same as the +execution resource associated with `sch` is not clear to which +execution resource is the "current execution resource". It could +be the "current execution agent" that was used to call `start(op)`, +or it could be the execution agent that the predecessor completes +on. + +The intended meaning for "current execution resource" was to refer +to the execution resource of the execution agent that was used to +call `start(op)`. The specification could be clarified by changing +the wording to become: + +> ... Calling `start(op)` will start `sndr` on the current execution +> agent and execution completion operations on `out_rcvr` on an +> execution agent of the execution resource associated with `sch`. +> If the [current]{.rm} execution resource [of the execution agent +> that was used to invoke `start(op)` ]{.add} is the same as the +> execution resource associated with `sch`, the completion operation +> on `out_rcvr` may be called before `start(op)` completes. If +> scheduling onto `sch` fails, an error completion on `out_rcvr` +> shall be executed on an unspecified execution agent. + +It is also not clear what the actual difference in semantics +between `continues_on` and `affine_on` is. The `continues_on` +semantics already requires that the resulting sender completes on +the specified schedulers execution agent. It does not specify that +it must evaluate a `schedule()` (although that is what the default +implementation does), and so in theory it already permits an +implementation/customization to skip the schedule (e.g. if the child +senders completion scheduler was equal to the target scheduler). + +The key semantic that we want here is to specify one of two possible +semantics that differ from `continues_on`: + +1. That the completion of an `affine_on` sender will occur on the + same scheduler that the operation started on. This is a slightly + stronger requirement than that of `continues_on`, in that it + puts a requirement on the caller of `affine_on` to ensure that + the operation is started on the scheduler passed to `affine_on`, + but then also grants permission for the operation to complete + inline if it completes synchronously. +2. That the completion of an `affine_on` sender will either complete + inline on the execution agent that it was started on, or it + will complete asynchronously on an execution agent associated + with the provided scheduler. This is a slightly more permissive + than option 1. in that it permits the caller to start on any + context, but also is no longer able to definitively advertise + a completion context, since it might now complete on one of two + possible contexts (even if in many cases those two contexts + might be the same). This weaker semantic can be used in conjunction + with knowledge by the caller that they will start the operation + on a context associated with the same scheduler passed to + `affine_on` to ensure that the operation will complete on the + given scheduler. + +The description in the paper at the Hagenberg meeting assumed that +`task` uses `continues_on` directly to achieve scheduler affinity. +During the SG1 discussion it was requested that the approach to +scheduler affinity doesn't use `continues_on` directly but rather +uses a different algorithm which can be customized separately. This +is the algorithm now named `affine_on`. The intention was that +`affine_on` can avoid scheduling in more cases than `continues_on`. + +It is worth noting that an implementation can't determine the +execution resource of the execution agent which invoked `start(op)` +directly. If `rcvr` is the receiver used to create `op` it can be +queried for `get_scheduler(get_env(rcvr))` which is intended to +yield this execution resource. However, there is no guarantee that +this is the case [yet?]. + +The intended direction here is to pursue the second possibility +mentioned above. Compared to `continues_on` that would effectively +relax the requirement that `affine_on` completes on `sch` if `sender` +doesn't actually change schedulers and completes inline: if `start(op)` +gets invoked on an execution agents which matches `sch`'s execution +resources the guarantee holds but `affine_on` would be allowed to +complete on the execution agent `start(op)` was invoked on. It is upon +the user to invoke `start(op)` on the correct execution agent. + +Another difference to `continues_on` is that `affine_on` can be +separately customized. + +### `affine_on`'s Shape May Not Be Correct + +The `affine_on` algorithm defined in +[[exec.affine.on]](https://eel.is/c++draft/exec#affine.on) takes two arguments; a +[`sender`](https://eel.is/c++draft/exec.snd.concepts) and a +[`scheduler`](https://eel.is/c++draft/exec.sched). + +As mentioned above, the semantic that we really want for the purpose +in coroutines is that the operation completes on the same execution +context that it started on. This way, we can ensure, by induction, +that the coroutine which starts on the right context, and resumes +on the same context after each suspend-point, will itself complete +on the same context. + +This then also begs the question: Could we just take the scheduler +that the operation will be started on from the +[`get_scheduler`](https://eel.is/c++draft/exec.get.scheduler) query +on the receivers environment and avoid having to explicitly pass +the scheduler as an argument? + +To this end, we should consider potentially simplifying the `affine_on` +algorithm to just take an input sender and to pick up the scheduler +that it will be started on from the receivers environment and promise +to complete on that context. + +For example, the +[`await_transform`](https://eel.is/c++draft/exec#task.promise-10) +function could potentially be changed to return: +`as_awaitable(affine_on(std::forward(sndr)))`. Then we could +define the `affine_on.transform_sender(sndr, env)` expression (which +provides the default implementation) to be equivalent to +`continues_on(sndr, get_scheduler(env))`. + +Such an approach would also require a change to the semantic +requirements of `get_scheduler(get_env(rcvr))` to require that +`start(op)` is called on that context. + +This change isn't strictly necessary, though. The interface of +`affine_on` as is can be used. Making this change does improve the +design. Nothing outside of `task` currently uses `affine_on` +and as it is an algorithm tailored for `task`'s needs it seems +reasonable to make it fit this use case exactly. This change would +also align with the direction that the execution agent used to +invoke `start(op)` matches the execution resource of +`get_scheduler(get_env(rcvr))`. + +### `affine_on` Shouldn't Forward Stop Requests To Scheduling Operations + +The `affine_on` algorithm is used by the `task` coroutine to ensure +that the coroutine always resumes back on its associated scheduler +by applying the `affine_on` algorithm to each awaited value in a +`co_await` expression. + +In cases where the awaited operation completes asynchronously, +resumption of the coroutine will be scheduled using the coroutines +associated scheduler via a +[`schedule`](https://eel.is/c++draft/exec.schedule) operation. + +If that schedule operation completes with `set_value` then the +coroutine successfully resumes on its associated execution context. +However, if it completes with `set_error` or `set_stopped` then resuming +the coroutine on the execution context of the completion is not +going to preserve the invariant that the coroutine is always going +to resume on its associated context. + +For some cases this may not be an issue, but for other cases, +resuming on the right execution context may be important for +correctness, even during exception unwind or due to cancellation. +For example, destructors may require running on a UI thread in order +to release UI resources. Or the associated scheduler may be a strand +(which runs all tasks scheduled to it sequentially) in order to +synchronize access to shared resources used by destructors. + +Thus, if a stop-request has been sent to the coroutine, that +stop-request should be propagated to child operations so that the +child operation it is waiting on can be cancelled if necessary, but +should probably not be propagated to any schedule operation created +by the implicit `affine_on` algorithm as this is needed to complete +successfully in order to ensure the coroutine resumes on its +associated context. + +One option to work around this with the status-quo would be to +define a scheduler adapter that adapted the underlying `schedule()` +operation to prevent passing through stop-requests from the parent +environment (e.g. applying the +[`unstoppable`](https://eel.is/c++draft/exec.unstoppable) adapter). +If failing to reschedule onto the associated context was a fatal +error, you could also apply a `terminate_on_error` adaptor as well. + +Then the user could apply this adapter to the scheduler before +passing it to the `task`. + +For example: + +``` +template +struct infallible_scheduler { + using scheduler_concept = std::execution::scheduler_t; + S scheduler; + auto schedule() { + return unstoppable(terminate_on_error(std::execution::schedule(scheduler))); + } + bool operator==(const infallible_scheduler&) const noexcept = default; +}; +template +infallible_scheduler(S) -> infallible_scheduler; + +std::execution::task> example() { + co_await some_cancellable_op(); +} + +std::execution::task> caller() { + std::execution::scheduler auto sched = co_await std::execution::read_env(get_scheduler); + co_await std::execution::on(infallible_scheduler{sched}, example()); +} +``` + +However, this approach has the downside that this scheduler behavior +now also applies to all other uses of the scheduler - not just the +uses required to ensure the coroutines invariant of always resuming +on the associated context. + +Other ways this could be tackled include: + +- Making it the default behavior of `affine_on` to suppress stop + requests for the scheduling operations. That would mean that + `affine_on` won't delegate to [`continues_on`](). +- Somehow making the behavior a policy decision specified via the + `Environment` template parameter of the `task`. +- Somehow using domain-based customization to allow the coroutine + to customize the behavior of `affine_on`. +- Making the `task::promise_type::await_transform` apply this adapter + to the scheduler passed to `affine_on`. i.e. it calls + `affine_on(std::forward(sndr), infallible_scheduler{SCHED(*sched)})`. + Taking this route would mean that the shape of `affine_on` should + not be changed. + +### `affine_on` Customization For Other Senders + +Assuming the the `affine_on` algorithm semantics are changed to +just require that it completes either inline or on the context of +the receiver environments `get_scheduler` query, then there are +probably some other algorithms that could either make use of +this, or provide customisations for it that short-circuit the need +to schedule unnecessarily. + +For example: + +- `affine_on(just(args...))` could be simplified to `just(args...)` +- `affine_on(on(sch, sndr))` can be simplified to `on(sch, sndr)` + as on already provides `affine_on`-like semantics +- The [`counting_scope::join`](https://eel.is/c++draft/exec.counting.scopes.general) sender currently already provides + `affine_on`-like semantics. + - We could potentially simplify this sender to just complete + inline unless the join-sender is wrapped in `affine_on`, in + which case the resulting `affine_on(scope.join())` sender would + have the semantics that `scope.join()` has today. + - Alternatively, we could just customize `affine_on(scope.join())` + to be equivalent to `scope.join()`. +- Other similar senders like those returned from + `bounded_queue::async_push` and `bounded_queue::async_pop` which + are defined to return a sender that will resume on the original + scheduler. + +The intended use of `affine_on` is to avoid scheduling where the +algorithm already resumes on a suitable execution agent. However, +as the proposal was late it didn't require potential optimizations. +The intend was to leave the specification of the default implementation +vague enough to let implementations avoid scheduling where they +know it isn't needed. Making these a requirement is intended for +future revisions of the standard. + +It is also a bit unclear how algorithm customization is actually +implemented in practice. Algorithms can advertise a domain via the +`get_domain` query which can then be used to transform algorithms: +`transform_sender(dom, sender, env...)` +[[exec.snd.transform]](https://eel.is/c++draft/exec#snd.transform) +uses `dom.transform_sender(sender, env...)` to transform the sender +if this expression is valid (otherwise the transformation from +`default_domain` is used which doesn't transform the sender). One +way to allow special transformations for `affine_on` is to defined +the `get_domain` query for `affine_on` (well, the environment +obtained by `get_env(a)` from an `affine_on` sender `a`) to yield a +custom domain `affine_on_domain` which delegates transformations +of `affine_on(sndr, sch)` the sender `sndr` or the scheduler `sch`: + +Let `affine_on_domain.transform_sender(affsndr, env...)` (where +`affsndr` is the result of `affine_on(sch, sndr)`) be + +- the result of the expression `sndr.affine_on(sch, env...)` if it + is valid, otherwise +- the result of the expression `sch.affine_on(sndr, env...)` if it + is valid, otherwise +- not defined. + +A similar approach would be used for other algorithms which can be +customized. Currently, no algorithm defines the exact ways it can +be customized in an open form and the intended design for customisations +may be different. The above outlines one possible way. + +## Task Operation + +This section groups three concerns relating to the way `task` gets +started and stopped: + +1. `task` shouldn't reschedule upon `start()`. +2. `task`s `co_await`ing `task`s shouldn't reschedule. +3. `task` doesn't support symmetric Transfer. + +### Starting A `task` Should Not Unconditionally Reschedule + +In +[[task.promise] p6](https://eel.is/c++draft/exec#task.promise-6), +the wording for `initial_suspend` says that it unconditionally +suspends and reschedules onto the associated scheduler. The intent +of this wording seems to be that the coroutine ensures execution +initially starts on the associated scheduler by executing a schedule +operation and then resuming the coroutine from the initial suspend +point inside the `set_value` completion handler of the schedule +operation. The effect of this would be that every call to a coroutine +would have to round-trip through the scheduler, which, depending +on the behavior of the schedule operation, might relegate it to the +back of the schedulers queue, greatly increasing latency of the +call. + +The specification of `initial_suspend()` (assuming it is rephrased +to not resume the coroutine immediately) doesn't really say it +unconditionally reschedules. It merely states that an awaiter is +returned which arranges for the coroutine to get resumed on the +correct scheduler. In general, i.e., when the coroutine gets resumed +via `start(os)` on an operation state `os` obtained from `connect(tsk, +rcvr)` it will need to reschedule. However, an implementation can +pass additional context information, e.g., via implementation +specific properties on the receiver's environment: while the +`get_scheduler` query may not provide the necessary guarantee that +the returned scheduler is the scheduler the operation was started +on a different, a non-forwardable query could provide this guarantee. + +A possible alternative is to require that `start(op)`, where `op` +is the result of `connect(sndr, rcvr)`, is invoked on an execution +agent associated with `get_scheduler(get_env(rcvr))`, at least when +`sndr` is a `task<...>`. Such a requirement could be desirable more +general but it would also be part of the general sender/receiver +contract. The current specification in +[[exec.async.ops](https://eel.is/c++draft/exec.async.ops)] doesn't +have a corresponding constraint. `task<...>` is a sender and +where it can be used with standard library algorithms this constraint +would hold. + +A different way to approach the issue is to use a `task`-specific +awaitable when a `task` is `co_await`ed by another `task`. This use +of an awaiter shouldn't be observable but it may be better to +explicitly spell out that `task` has a domain with custom transformation +of the `affine_on` algorithm and `as_awaitable` operation. + +### Resuming After A `task` Should Not Reschedule + +Similar to the previous issue, a `task` needs to resume on the +expected scheduler. The definition of `await_transform` which is used +for a `task` is defined in [[task.promise] p10](https://eel.is/c++draft/exec#task.promise-10) +(the `co_await`ed `task` would be the `sndr` parameter): + +``` +as_awaitable(affine_on(std::forward(sndr), SCHED(*this)), *this) +``` + +As defined there is no additional information about the scheduler +on which the `sndr` completes. Thus, it is likely that a scheduling +operation is needed after a `co_await`ed `task` completes when the +outer `task` resumes, even if the `co_await`ed `task` completed on +the same execution agent. + +As `task` is scheduler affine, it is likely that it completes on +the same scheduler it was started on, i.e., there shouldn't be a +need to reschedule (if the scheduler used by the `task` was changed +using `co_await change_coroutine_scheduler(sch)` it still needs to +be rescheduled). The implementation can provide a query on the state +used to execute the `task` which identifies the scheduler on which +it completed and the `affine_on` implementation can use this +information to decide whether it is necessary to reschedule after +the `task`'s completion. It would be preferable if a corresponding +query were specified by the standard library to have user-defined +senders provide similar information but currently there is no such +query. + +A different approach is to transform the `affine_on(task, sch)` +operation into a `task`-specific implementation which arranges to +always complete on `sch` using `task` specific information. It may +be necessary to explicit specify or, at least, allow that `task` +provides a domain with a custom transformation for `affine_on`. + +### No Support For Symmetric Transfer + +The specification doesn't mention any use of symmetric transfer. +Further, the `task` gets adapted by `affine_on` in `await_transform` +([[task.promise] p10](https://eel.is/c++draft/exec#task.promise-10) +) which produces a different sender than `task` which needs +special treatment to use symmetric transfer. With symmetric transfer +stack overflow can be avoided when operation complete immediately, e.g. + +```c++ +template +task test() { + for (std::size_t i{}; i < 1000000; ++i) { + co_await std::invoke([]()->task> { + co_return; + }); + } +} +``` + +When using a scheduler which actually schedules the work (rather +than immediately completing when a corresponding sender gets started) +there isn't a stack overflow but the scheduling may be slow. With +symmetric transfer the it can be possible to avoid both the expensive +scheduling operation and the stack overflow, at least in some cases. +When the inner `task` actually `co_await`s any work which synchronously +completes, e.g., `co_await just()`, the code could still result in +a stack overflow despite using symmetric transfer. There is a general +issue that stack size needs to be bounded when operations complete +synchronously. The general idea is to use a trampoline scheduler +which bounds stack size and reschedules when the stack size or the +recursion depth becomes too big. + +Except for the presence or absence of stack overflows it shouldn't +be observable whether an implementation invokes the nested coroutine +directly through an awaiter interface or using the default +implementations of `as_awaitable` and `affine_on`. To address the +different scheduling problems (schedule on `start`, schedule on +completion, and symmetric transfer) it may be reasonable to mandate +that `task` customizes `affine_on` and that the result of this +customization also customizes `as_awaitable`. + +## Allocation + +### Unusual Allocator Customization + +The allocator customization mechanism is inconsistent with the +design of `generator` allocator customization: with `generator`, +when you don't specify an allocator in the template arg, then you +can use any `allocator_arg` type. With `task` if no allocator is +specified in the `Environment` the allocator type defaults to +`std::allocator` and using an allocator with an incompatible +type results in an ill-formed program. + +For example: + +```c++ +struct default_env {}; +struct allocator_env { + using allocator_type = std::pmr::polymorphic_allocator<>; +}; + +template +ex::task test(int i, auto&&...) { + co_return co_await ex::just(i); +} +``` + +Depending on how the coroutine is invoked this may fail to compile: + +- `test(17)`: OK - use default allocator +- `test(17, std::allocator_arg, std::allocator())`: OK - allocator is convertible +- `test(17, std::allocator_arg, std::pmr::polymorphic_allocator<>())`: compile-time error +- `test(17, std::allocator_arg, std::pmr::polymorphic_allocator<>())`: OK + +The main motivator for always having an `allocator_type` is it to +support the `get_allocator` query on the receiver's environments +when `co_await`ing another sender. The immediate use of the allocator +is the allocation of the coroutine frame and these two needs can be +considered separate. + +Instead of using the `allocator_type` both for the environment and +the coroutine frame, the coroutine frame could be allocated with +any allocator specified as an argument (i.e., as the argument +following an `std::allocator_arg` argument). The cost of doing so +is that the allocator stored with the coroutine frame would need +to be type-erased which is, however, fairly cheap as only the +`deallocate(ptr, size)` operation needs to be known and certainly +no extra allocation is needed to do so. If no `allocator_type` is +specified in the `Environment` argument to `task`, the +`get_allocator` query would not be available from the environment +of received used with `co_await`. + +### Issue: Flexible Allocator Position + +For `task` the position of an `std::allocator_arg` can be +anywhere in the argument list except it can't be the last argument +(as it needs to be followed by an allocator). For `generator` the +`std::allocator_arg` argument needs to be the first argument. That +is consistent with other uses of `std::allocator_arg`. `task` should +also require the `std::allocator_arg` argument to be the first +argument. + +The reason why `task` deviates from the approach normally taken is +that the consistency with non-coroutines is questionable: the primary +reason why the `std::allocator_arg` is needed in the first position +is that allocation is delegated to `std::uses_allocator` construction. +This approach, however, doesn't apply to coroutines at all: the +coroutine frame is allocated from an `operator new()` overloaded +for the `promise_type`. In the context of coroutines the question +is rather how to make it easy to optionally support allocators. + +Any coroutine which wants to support allocators for the allocation +of the coroutine frame needs to allow that allocators show up on +the parameter list. As coroutines normally have other parameters, +too, requiring that the optional `std::allocator_arg`/allocator +pair of arguments comes first effectively means that a forwarding +function is provided, e.g., for a coroutine taking an `int` parameter +for its work (`Env` is assumed to be a environment type with a +suitable definition of `allocator_type`): + +``` +task work(std::allocator_arg_t, auto, int value) { + co_await just(value); +} +task work(int value) { + return work(std::allocator_arg, std::allocator()); +} +``` + +If the `allocator_arg` argument can be at an arbitrary location of +the parameter list, defining a coroutine with optional allocator +support amounts to adding a suitable trailing list of parameters, +e.g.: + +``` +task work(int value, auto&&...) { + co_await just(value); +} +``` + +Constraining `task` to match the use of `generator` is entirely +possible. It seems more reasonable to rather consider relaxing the +requirement on `generator` in a future revision of the C++ standard. + +### Shadowing The Environment Allocator Is Questionable + +The `get_allocator` query on the environment of the receiver passed to +`co_await`ed senders always returns the allocator determined when +the coroutine frame is created. The allocator provided by the +environment of the receiver the `task` is `connect`ed to is hidden. + +Creating a coroutine (calling the coroutine function) chooses the +allocator for the coroutine frame, either explicitly specified or +implicitly determined. Either way the coroutine frame is allocated +when the function is called. At this point the coroutine isn't +`connect`ed to a receiver, yet. The fundamental idea of the allocator +model is that children of an entity use the same allocator as their +parent unless an allocator is explicitly specified for a child. +The `co_await`ed entities are children of the coroutine and should +share the coroutine's allocator. + +In addition, to forward the allocator from the receiver's environment +in an environment, its static type needs to be convertible to the +coroutine's allocator type: the coroutine's environment types are +determined when the coroutine is created at which point the receiver +isn't known, yet. Whether the allocator from the receiver's environment +can be used with the statically determined allocator type can't be +determined. It may be possible to use a type-erase allocator which +could be created in the operation state when the `task` is `connect`ed. + +On the flip side, the receiver's environment can contain the +configuration from the user of a work-graph which is likely better +informed about the best allocator to use. The allocator from the +receiver's environment could be forwarded if the `task`'s allocator +can be initialized with it, e.g., because the `task` use +`std::pmr::polymorphic_allocator<>`. It isn't clear what should +happen if the receiver's environment has an allocator which can't +be converted to the allocator based `task`'s environment: at least +ignoring a mismatching allocator or producing a compile time error +are options. It is likely possible to come up with a way to configure +the desired behavior using the environment. + +## Stop Token Management + +### A Stop Source Always Needs To Be Created + +The specification of the `promise_type` contains exposition-only +members for a stop source and a stop token. It is expected that in +may situations the upstream stop token will be an `inplace_stop_token` +and this is also the token type exposed downstream. In these cases +the `promise_type` should store neither a stop source nor a stop +token: instead the stop token should be obtained from the upstream +environment. Necessarily storing a stop source and a stop +token would increase the size of the promise type by multiple +pointers. On top of that, to forward the state of an upstream stop +token via a new stop source requires registration/deregistration +of a stop callback which requires dealing with synchronization. + +The expected implementation is, indeed, to get the stop token from +the operation state: when the operation state is created, it is +known whether the upstream stop token is compatible with the +statically determined stop token exposed by `task` to `co_await`ed +operations via the respective receiver's environment. If the type +is compatible there is no need to store anything beyond the receiver +from which a stop token can be obtained using the `get_stop_token` +query when needed. Otherwise, the operation state can store an +optional stop source which gets initialized and connected to the +upstream stop token via a stop callback when the first stop token +is requested. + +The exposition-only members are not meant to imply that a corresponding +object is actually stored or where they are. Instead, they are +merely meant to talk about the corresponding entities where the +behavior of the environment is described. If the current wording +is considered to imply that these entities actually exist or it can +be misunderstood to imply that, the wording may need some massaging +possibly using specification macros to refer to the respective +entities in the operation state. + +### The Wording Implies The Stop Token Is Default Constructible + +Using an exposition-only member for the stop token implies that the +stop token type is default constructible. The stop token types are +generally not default constructible and are, instead, created via +the stop source and always refer to the corresponding stop source. + +The intent here is, indeed, that the stop token type isn't actually +stored at all: the operation state either stores a stop source which +is used to get the stop token or, probably in the comment case, the +stop token is obtained from the upstream receiver's environment by +querying `get_stop_token`. The rewording for the previous concern +should also address this concern. + +## Miscellaneous Concerns + +The remaining concerns aren't as coupled to other concerns and +discussed separately. + +### Task Is Not Actually Lazily Started + +The wording for `task<...>::promise_type::initial_suspend` in +[[task.promise] p6](https://eel.is/c++draft/exec#task.promise-6) +may imply that a `task` is eagerly started: + +> `auto initial_suspend() noexcept;` +> +> _Returns:_ An awaitable object of unspecified type ([expr.await]) +> whose member functions arrange for +> +> - the calling coroutine to be suspended, +> - the coroutine to be resumed on an execution agent of the execution +> resource associated with `SCHED(*this)`. + +In particular the second bullet can be interpreted to mean that the +task gets resumed immediately. That wouldn't actually work because +`SCHED(*this)` only gets initialized when the `task` gets `connect`ed +to a suitable receiver. The intention of the current specification +is to establish the invariant that the coroutine is running on the +correct scheduler when the coroutine is resumed (see discussion of +`affine_on` below). The mechanisms used to achieve that are not +detailed to avoid requiring that it gets scheduled. The formulation +should, at least, be improved to clarify that the coroutine isn't +resumed immediately, possibly changing the text like this: + +> - the coroutine [to be resumed]{.rm}[resuming]{.add} on an execution +> agent of the execution resource associated with `SCHED(*this)` +> [when it gets resumed]{.add}. + +The proposed fix for the issue is to specify that `initial_suspend()` +always returns `suspend_always{}` and require that `start(...)` +calls `handle.resume()` to resume the coroutine on the appropriate +scheduler after `SCHED(*this)` has been initialized. The corresponding +change could be + +Change [[task.promise] p6](https://eel.is/c++draft/exec#task.promise-6): + +> `auto initial_suspend() noexcept;` +> +::: rm +> _Returns:_ An awaitable object of unspecified type ([expr.await]) +> whose member functions arrange for: +> +> - the calling coroutine to be suspended, +> - the coroutine to be resumed on an execution agent of the execution +> resource associated with `SCHED(*this)`. +::: +::: add +> _Returns:_ `suspend_always{}`. +::: + +The suggestion is to ensure that the task gets resumed on the +correct associated context via added requirements on the receiver's +`get_scheduler()` (see below). + +In separate discussions it was suggested to relax the specification +of `initial_suspend()` to allow returning an awaiter which does +semantically what `suspend_always` does but isn't necessary +`suspend_always`. The proposal was to copy the wording of the +`suspend_always` specification +[[coroutine.trivial.awaitables]](https://eel.is/c++draft/coroutine.trivial.awaitables#lib:suspend_always). +This specification just shows the class completion definition. + +### The Coroutine Frame Is Destroyed Too Late + +When the `task` completes from a suspended coroutine without ever +reaching `final_suspend` the coroutine frame lingers until the +operation state object is destroyed. This happens when the `task` +isn't resumed because a `co_await`ed sender completed with +`set_stopped()` or when the `task` completes using `co_yield when_error(e)`. +In these cases the order of destruction of objects may be unexpected: +objects held by the coroutine frame may be destroyed only long after +the respective completion function was called. + +This behavior is an oversight in the specification and not intentional +at all. Instead, there should be a statement that the coroutine +frame is destroyed before any of the completion functions is invoked. +The implication is that the results can't be stored in the promise +type but that isn't a huge constraint as the best place store them +is in the operation state. + +### `task` Has No Default Arguments + +The current specification of `task` doesn't have any default arguments +in its first declaration in [[execution.syn]](https://eel.is/c++draft/execution.syn). The intent was to default the +type `T` to `void` and the environment `E` to `env<>`. That is, this change +should be applied to [[execution.syn]](https://eel.is/c++draft/execution.syn): + +> ``` +> ... +> // [exec.task] +> template ]{.add}@> +> class task; +> ... +> ``` + +It isn't catastrophic if that change isn't made but it seems to +improve usability without any drawbacks. + +### `bulk` vs. `task_scheduler` + +Normally, the scheduler type used by an operation can be deduced +when a sender is `connect`ed to a receiver from the receiver's +environment. The body of a coroutine cannot know about the `receiver` +the `task` sender gets `connect`ed to. The implication is that the +type of the scheduler used by the coroutine needs to be known when +the `task` is created. To still allow custom schedulers used when +`connect`ing, the type-erase scheduler `task_scheduler` is used. +However, that leads to surprises when algorithms are customized +for a scheduler as is, e.g., the case for `bulk` when used with a +`parallel_scheduler`: if `bulk` is `co_await`ed within a coroutine +using `task_scheduler` it will use the default implementation of +`bulk` which sequentially executes the work, even if the `task_scheduler` +was initialized with a `parallel_scheduler` (the exact invocation may +actually be slightly different or need to use `bulk_chunked` or +`bulk_unchunked` but that isn't the point being made): + +``` +struct env { + auto query(ex::get_scheduler_t) const noexcept { return ex::parallel_scheduler(); } +}; +struct work { + auto operator()(std::size_t s){ /*...*/ }; +}; + +ex::sync_wait( + ex::write_env(ex::bulk(ex::just(), 16u, work{}), + env{} +)); +ex::sync_wait(ex::write_env( + []()->ex::task> { co_await ex::bulk(ex::just(), 16u, work{}); }(), + env{} +)); +``` + +The two invocations should probably both execute the work in parallel +but the coroutine version doesn't: it uses the `task_scheduler` +which doesn't have a specialized version of `bulk` to potentially +delegate in a type-erased form to the underlying scheduler. It is +straight forward to move the `write_env` wrapper inside the coroutine +which fixes the problem in this case but this need introduces the +potential for a subtle performance bug. The problem is sadly not +limited to a particular scheduler or a particular algorithm: any +scheduler/algorithm combination which may get specialized can suffer +from the specialized algorithm not being picked up. + +There are a few ways this problem can be addressed (this list of +options is almost certainly incomplete): + +1. Accept the situation as is and advise users to be careful about +customized algorithms like `bulk` when using `task_scheduler`. +2. Extend the interface of `task_scheduler` to deal with a set of +algorithms for which it provides a type-erased interface. The +interface would likely be more constrained and it would use virtual +dispatch at run-time. However, the set of covered algorithms would +necessarily be limited in some form. +3. To avoid the trap, make the use of known algorithms incompatible +with the use of `task_scheduler`, i.e., "customize" these algorithms +for `task_scheduler` such that a compile-time error is produced. + +A user who knows that the main purpose of a coroutine is to executed +an algorithm customized for a certain scheduler can use `task` with an environment `E` specifying exactly that scheduler type. +However, this use may be nested within some sender being `co_await`ed +and users need to be aware that the customization wouldn't be picked +up. Any approach I'm currently aware of will have the problem that +customized versions of an algorithm are not used for algorithms we +are currently unaware of. + +### `unhandled_stopped()` Isn't `noexcept` + +The `unhandled_stopped()` member function of `task::promise_type` +[[task.promise]](https://eel.is/c++draft/exec#task.promise) +is not currently marked as `noexcept`. + +As this method is generally called from the `set_stopped` +completion-handler of a receiver (such as in [[exec.as.awaitable] p4.3](https://eel.is/c++draft/exec.as.awaitable#4.3)) +and is invoked without handler from a `noexcept` function, we should +probably require that this function is marked `noexcept` as well. + +The equivalent method defined as part of the `with_awaitable_senders` +base-class +([[exec.with.awaitable.senders]](https://eel.is/c++draft/exec.with.awaitable.senders#1) +p1) is also marked `noexcept`. + +This change should be applied to [[task.promise]](https://eel.is/c++draft/exec#task.promise): + +> ``` +> ... +> void uncaught_exception(); +> coroutine_handle<> unhandled_stopped()@[ noexcept]{.add}@; +> +> void return_void(); // present only if is_void_v is true; +> ... +> ``` +> +> ... +> +> ``` +> coroutine_handle<> unhandled_stopped()@[ noexcept]{.add}@; +> ``` +> +> [13]{.pnum} _Effects_: Completes the asynchronous operation associated with `STATE(*this)` by invoking `set_stopped(std::move(RCVR(*this)))`. + +### The Environment Design May Be Inefficient + +The environment returned from `get_env(rcvr)` for the receiver used +to `connect` to `co_await`ed sender is described in terms of members +of the `promise_type::@_state_@` in +[[task.promise] p15](https://eel.is/c++draft/exec#task.promise-15). +In particular the `@_state_@` contains a member `@_own-env_@` which +gets initialized via the upstream receiver's environment (if the +corresponding expression is valid). As the environment `@_own-env_@` +is initialized with a temporary, `@_own-env_@` will need to create +a copy of whatever it is holding. + +As the environment exposed to `co_await`ed senders via the +`get_env(rcvr)` is statically determined when the `task` is created, +not when the `task` is `connect`ed to an upstream receiver, the +environment needs to be type erase. Some of the queries (`get_scheduler`, +`get_stop_token`, and `get_allocator`) are known and already receive +treatment by the `task` itself. However, the `task` cannot know about +user-defined properties which may need to be type-erased as well. To +allow the `Environment` to possibly use type-erase properties from the +upstream receiver, an `own-env-t` object is created and a reference to +the object is passed to the `Environment` constructor (if there are +suitable constructors). Thus, the entire use of `own-env-t` is about +enabling storage of whatever is needed to type-erase the result of +user-defined queries. Ideally, this functionality isn't needed and +the `Environment` parameter doesn't specify an `env_type`. If it is +needed the type-erasure will likely need to store some data. + +### No Completion Scheduler + +The concern raised is that `task` doesn't define a `get_env` that +returns an environment with a `get_completion_scheduler` query. +As a result, it will be necessary to reschedule in some situations although +the `task` already completes on the correct scheduler. + +The query `get_completion_scheduler(env)` operates on the sender's +environment, i.e., it would operate on the `task`'s environment. +However, when the `task` is created it has no information about the +scheduler, yet, it is going to use. The entire information the +`task` has is what is passed to the coroutine [factory] function. +The information about any scheduling gets provided when the `task` +is `connect`ed to a receiver: the receiver provides the queries on +where the task is started (`get_scheduler`). The `task` may complete +on this scheduler assuming the scheduler isn't changed (using +`co_await change_coroutine_scheduler(sch)`). + +The only place where a `task` actually knows its completion scheduler +is once it has completed. However, there is no query or environment +defined for completed operation states. Thus, it seems there is no +way where a `get_completion_scheduler` would be useful. A future +evolution of the sender/receiver interface may change that in which +case `task` should be revisited. + +### Awaitable non-`sender`s Are Not Supported + +The overload of `await_transform` described in +[[task.promise]](https://eel.is/c++draft/exec#task.promise-10) +is constrained to require arguments to satisfy the +[`sender`](https://eel.is/c++draft/exec.snd.concepts) concept. +However, this precludes awaiting types that implement the +[`as_awaitable()`](https://eel.is/c++draft/exec.as.awaitable) +customization point but that do not satisfy the +[`sender`](https://eel.is/c++draft/exec.snd.concepts) concept from +being able to be awaited within a `task` coroutine. + +This is inconsistent with the behavior of the [`with_awaitable_senders`]() +base class defined in +[[exec.with.awaitable.senders]](https://eel.is/c++draft/exec.with.awaitable.senders), +which only requires that the awaited value supports the +[`as_awaitable()`](https://eel.is/c++draft/exec.as.awaitable) +operation. + +The rationale for this is that the argument needs to be passed to +the +[`affine_on`](https://eel.is/c++draft/exec#exec.affine.on) +algorithm which currently requires its argument to model +[`sender`](https://eel.is/c++draft/exec.snd.concepts). This requirement in turn is +there because it is unclear how to guarantee scheduler affinity with an awaitable-only +interface without wrapping a coroutine around the awaitable. + +Do we want to consider relaxing this constraint to be consistent with the constraints +on +[[exec.with.awaitable.senders]](https://eel.is/c++draft/exec.with.awaitable.senders)? + +This would require either: + +- relaxing the [`sender`](https://eel.is/c++draft/exec.snd.concepts) + constraint on the `affine_on` algorithm to also allow an argument + that has only an + [`as_awaitable()`](https://eel.is/c++draft/exec.as.awaitable) but + that did not satisfy the + [`sender`](https://eel.is/c++draft/exec.snd.concepts) concept. +- extending the [`sender`](https://eel.is/c++draft/exec.snd.concepts) + concept to match types that provide the `.as_awaitable()` + member-function similar to how it supports types with `operator + co_await()` (see [[exec.connect]](https://eel.is/c++draft/exec.connect)). + +### Should `task::promise_type` Use `with_awaitable_senders`? + +The existing [[exec]](https://eel.is/c++draft/exec) wording added +the +[`with_awaitable_senders`](https://eel.is/c++draft/exec#with.awaitable.senders) +helper class with the intention that it be usable as the base-class +for asynchronous coroutine promise-types to provide the ability to +await senders. It does this by providing the necessary `await_transform` +overload and also the `unhandled_stopped` member-function necessary +to support the `as_awaitable()` adaptor for senders. + +However, the current specification of `task::promise_types` does not +use this facility for a couple of reasons: + +- It needs to apply the `affine_on` adaptor to awaited senders. +- It needs to provide a custom implementation of `unhandled_stopped` + that reschedules onto the original scheduler in the case that it + is different from the current scheduler (e.g. due to + `co_await change_coroutine_scheduler{other}`). + +This raises a couple of questions: + +- Should there also be a `with_affine_awaitable_senders` that + implements the scheduler affinity logic? +- Is `with_awaitable_senders` actually the right design if the first + coroutine type we add to the standard library doesn't end up using it? + +These are valid questions. It seems the `with_awaitable_senders` class +was designed at time when there was no consensus that a `task` coroutine +should be scheduler affine by default. However, these questions don't +really affect the design of `task` and they can be answered in a future +revision of the standard. The `task` specification also uses a few other +tools which could be useful for users who want to implement their own +custom `task`-like coroutine. Such tools should probably be proposed for +inclusion into a future revision of the C++ standard, too. + +However, what is relevant to the `task` specification is that the +wording for `unhandled_stopped` in +[[task.promise]](https://eel.is/c++draft/exec#task.promise) +paragraph 13 doesn't spell out that the `set_stopped` completion +is invoked on the correct scheduler. + +### A Future Coroutine Feature Could Avoid `co_yield` For Errors + +The mechanism to complete with an error without using an exception +uses [`co_yield +with_error(x)`](https://eel.is/c++draft/exec#task.promise-9). This +use may surprise users as `co_yield`ing is normally assumed not to +be a final operation. A future language change may provide a better +way to achieve the objective of reporting errors without using +exceptions. + +Using not, yet, proposed features which may become part of a future +revision of the C++ standard may always provide facilities which +result in a better design for pretty much anything. There is no +current `task` implementation and certainly none which promises ABI +stability. Assuming a corresponding language change gets proposed +reasonably early for the next cycle, implementations can take it into +account for possibly improving the interface without the need to +break ABI. The shape of the envisioned language change is unknown +but most likely it is an extension which hopefully integrates +with the existing design. The functionality to use `co_yield` +would, however, continue to exist until it eventually gets +deprecated and removed (assuming a better alternative emerges). + +### There Is No Hook To Capture/Restore TLS + +This concern is the only one I was aware of for a few weeks prior +to the Sofia meeting. I believe the necessary functionality can be +implemented although it is possibly harder to do than with more +direct support. The concern raised is that existing code accesses +thread local storage (TLS) to store context information. When a +`co_await` actually suspends it is possible that a different task +running on the same thread replaces used TLS data or the task gets +resumed on a different thread of a thread pool. In that case it is +necessary to capture the TLS variables prior to suspending and +restoring them prior to resuming the coroutine. There is currently +no special way to actually do that. + +The sender/receiver approach to propagating context information +isn't using TLS but rather the environments accessed via the receiver. +However, existing software does use TLS and at least for a migration +period corresponding support may be necessary. One way to implement +this functionality is using a scheduler adapter with a customized +`affine_on` algorithm: + +- When the `affine_on` algorithm is started, it captures the relevant + TLS data into the operation state and then starts `affine_on` + for the adapted scheduler with a receiver observing the + completions. +- When the adapted scheduler invokes any of the completion signals + the TLS data is restored before invoke the algorithm's completion + signal. + +Support for optionally capturing some state before starting a child +operation and restoring the captured started before resuming could +be added to the `task`, avoiding the need to use custom scheduling. +Doing so would yield a nicer and easier to use interface. In +environments where TLS is currently used and developers move to an +asynchronous model, failure to capture and restore data in TLS is a +likely source of errors. However, it will be necessary to specify +what data needs to be stored, i.e., the problems can't be automatically +avoided. + +### `return_value` And `return_void` Have No Specification + +The functions `return_value` and `return_void` declared for the +`task<...>::promise_type` in +[[task.promise]](https://eel.is/c++draft/task.promise) (only one +of them is present in a given `promise_type`) are entirely missing +from the specification. To fix this problem the specifications need +to be added: + +::: add +``` +void return_void(); // present only if is_void_v is true; +``` + +[x]{.pnum} _Effects_: does nothing. + +``` +template + void return_value(V&& value); // present only if is_void_v is false; +``` + +[x+1]{.pnum} _Effects_: Equivalent to _result_.emplace(std::forward(value)); + +::: + + +### Consider Supporting co_return { args... }; + +The current [declaration of `return_value`](https://eel.is/c++draft/task.promise) in the +`promise_type` specifies the function without a default type for +the template parameter: + +``` +template +void return_value(V&& value); // present only if is_void is false +``` + +As a result it isn't possible to use aggregate initialization without +mentioning the type. For example, the following code isn't valid: + +``` +struct aggregate { int value{0} }; +[]() -> task> { co_return {}; }; +[]() -> task> { co_return { 42 }; }; +``` + +To better match the normal functions, the template +parameter should be defaulted which would make the code above valid: + +``` +template +void return_value(V&& value); // present only if is_void is false +``` + +There isn't anything broken with the specification but adding the +default type improves usability. If necessary, this change can be +applied in a future revision of the standard. It would be nice to +fix it for C++26. + +### `co_await change_coroutine_scheduler(sched)` Requires Assignable Scheduler + +The specification of `change_coroutine_scheduler(sched)` uses `std::exchange` to +put the scheduler into place (in [[task.promise] p11](https://eel.is/c++draft/exec#task.promise-11)): + +> ``` +> auto await_transform(change_coroutine_scheduler sch) noexcept;` +> ``` +> [11]{.pnum} Effects: Equivalent to: +> ``` +> return await_transform(just(exchange(SCHED(*this), scheduler_type(sch.scheduler))), *this); +> ``` + +The problem is that `std::exchange(x, v)` expects `x` to be assignable from `v` +but there is no requirement for scheduler to be assignable. It would be possible to +specify the operation in a way avoiding the assignment: + +> ``` +> @[`return await_transform(just(exchange(SCHED(*this), scheduler_type(sch.scheduler))), *this);`]{.rm}@ +> @[`auto* s{address_of(SCHED(*this))};`]{.add}@ +> @[`auto rc{std::move(*s)};`]{.add}@ +> @[`s->~scheduler_type();`]{.add}@ +> @[`new(s) scheduler_type(std::move(sch));`]{.add}@ +> @[`return std::move(rc);`]{.add}@ +> ``` + +An alternative is to consider the presence of the assignment to be +an indicator of whether the scheduler can be replaced: the possibility +of changing the scheduler used for scheduler affinity means that +the body of the coroutine may actually complete on a different +scheduler than the scheduler it was originally started on. It can +be determined statically whether `co_await change_coroutine_scheduler(s)` +was used. As a result, when a `task` awaits another `task` it is +potentially necessary to scheduler the continuation for the outer +`task`. This possibility and the corresponding check may have a +cost. However, when the scheduler isn't assignable, it couldn't +be replaced and that feature is statically checkable, i.e., any +cost associated with the potential of the inner `task` changing the +scheduler is avoided. + +It should be possible to relax the requirements for replacing +schedulers in a future revision of the standard, i.e., this issues +doesn't necessarily need to be resolved one way or the other for +C++26. + +### Sender Unaware Coroutines Should Be Able To `co_await` A `task` + +The request here is to add an `operator co_await()` to `task` +which returns an awaiter used to start the `task`. On the surface +doing so should allow any coroutine to `co_await` a `task`. Unfortunately, +here are a few complications: + +1. If the `task` is used with an environment `E` whose +`scheduler_type` can't be default constructed the environment +associated with the promise type `P` from the `std::coroutine_handle

` +passed to `await_suspend` needs to have a `get_env` providing an +environment with a query for `get_scheduler`. The default used +(`task_scheduler`) is not default constructible because by +default `task` should be scheduler affine and to implement +that the scheduler needs to be known. The implication is that the +`operator co_await` can only be used when either the `task` isn't scheduler affine or the `co_await`ing coroutine knows at +least about the `get_scheduler` query, i.e., it isn't entirely +sender agnostic. + +2. When `co_await`ing a sender within a `task` it is +always possible that a sender completes with `set_stopped()`, i.e., +the coroutine gets cancelled. However, to actually implement that, +the `co_await`ing coroutine needs to have a hook which can be invoked +to notify it about this cancellation. When using `as_awaitable(s)` +this hook is to call `unhandled_stopped()` on the promise object. +Thus, a sender agnostic coroutine trying to `co_await` a `st::task` would need to know about this protocol or only support `task` objects for which the `co_await`s cannot result in completing +with `set_stopped()`. + +3. Other senders are not `co_await`able by sender agnostic coroutines. +If a `task` wants to support `co_await`ing senders it needs to provide +an `await_transform` which turns a sender into an awaitable, e.g., using +`as_awaitable`. Why should that be different for `task`? As `task` +is already a coroutine it seems this is a fairly weak argument, though. + +Given these constraints it is unclear whether it is worth adding +an `operator co_await()` to `task`. If a user really wants to +`co_await` a `task`, it should be possible by adapting +the object to provide a `get_scheduler` query (e.g., using +`std::write_env`), to deal with the potential of a `set_stopped()` completion +(e.g., using `upon_stopped`), and then doing something similar +to `as_awaitable` with the result, just without the need to have +an `unhandled_stopped`. As there are various choices of what +schedulers should be provided and how to deal with the `set_stopped()` +completion it seems reasonable to not add support for `operator +co_await()` at this time. + +### Missing Rvalue Qualification + +The nested sender of a `task_scheduler` isn't necessary copyable. +As the operation is type erased, the +task_scheduler::_ts-sender_::connect should be rvalue +qualified, i.e., in [[exec.task.scheduler] +p8](https://eel.is/c++draft/exec#task.scheduler-8) add rvalue +qualification to `connect`: + +``` +namespace execution { + class task_scheduler::@_ts-sender_@ { // @_exposition only_@ + public: + using sender_concept = sender_t; + + template + state connect(R&& rcvr)@[` &&`]{.add}@; + }; +} +``` + +In [[exec.task.scheduler] p10](https://eel.is/c++draft/exec#task.scheduler-10) add +rvalue qualification to `connect`: + +``` +template + state connect(Rcvr&& rcvr)@[` &&`]{.add}@; +``` + +Coroutines aren't copyable. When using objects holding a +`coroutine_handle

` such that the underlying `coroutine_handle

` +gets transferred to a different object, the operation should be +rvalue qualified and not all such operations currently are rvalue +qualified. The `task::connect` function should be rvalue +qualified, i.e., change [[task.class] p1](https://eel.is/c++draft/exec#task.class): + +``` +namespace std::execution { + template + class task { + ... + template + state connect(R&& recv)@[` &&`]{.add}@; + ... + }; +} +``` + +Change [[task.members] p3](https://eel.is/c++draft/exec#task.members-3): + +``` +template +state connect(R&& recv)@[` &&`]{.add}@; +``` + +If an `as_awaitable` member is added to `task` (see [this issue](#starting-a-task-should-not-unconditionally-reschedule)) this member +should also be rvalue qualified. + +# Response to ["Concerns about the design of std::execution::task"](https://wg21.link/P3801r0) + +The paper ["Concerns about the design of std::execution::task"](P3801r0) +raises three major points and three minor points. The three major points +are: + +1. Synchronous completions can result in stack overflow. This problem + was discussed in the proposal ["Add a Coroutine Task + Type"](https://wg21.link/p3552). If a coroutine uses an actually + scheduling scheduler there is no issue with stack overflow + unless an implementation "optimizes" `affine_on` to complete + synchronously in some cases and they don't guard against stack + overflow, e.g., using a "trampoline scheduler". Also, the danger + of stack overflow isn't actually specific to `task` but applies + to any synchronous completion, especially if it happens from a + loop-like construct. In that sense the issue exists without + `task` although using a `task` may make it more likely that + users encounter this issue. + + The concern also mentioned that there is no support for symmetric + transfer between tasks. This point is already discussed above + in section [No Support For Symmetric + Transfer](#no-support-for-symmetric-transfer). + +2. One concern addresses that the life-time of variables local to + coroutines is surprising: there is no requirement to destroy + the coroutine frame before completing the operation. This issue + is discussed in [The Coroutine Frame Is Destroyed Too + Late](#the-coroutine-frame-is-destroyed-too-late) and the fix + is to explicitly specify that the coroutine frame has to be + destroyed before completing the operation. The only implication + is that the result of the operation needs to be stored in the + operation state rather than the promise type which is, however, + a reasonable approach anyway. + +3. It is raised as a concern that that `task` doesn't defined against + capturing reference which may be out of scope before the `task` + is executed. Here is a minimal example demonstrating the problem + (the example in the paper uses `co` instead of `t` and omitted + the `move` but it is clear what is intended): + + task> g() { + auto t = [](const int& v) -> task> { co_return v; }(42); + auto v = co_await std::move(t); + } + + The subtle problem with this code is the argument `42` is turned + into a temporary `int` which is captured by `const int&` in the + coroutine frame. Once the expression completes this temporary + goes out of scope and is destroyed. When `t` gets `co_await`ed + the body of the function accesses an already destroyed object. + It is worth noting that the relevant references can be hidden, + i.e., they don't necessarily show up in the signature of the + coroutine (preventing `const&` parameters isn't a solution). + + This problem and an approach to avoid it is explained at length + in Aaron Jacobs's C++Now 2024 talk ["Coroutines at + Scale"](https://youtu.be/k-A12dpMYHo?si=oZUownmZrbolDfJS). The + proposed solution is to return an object which can only be + `co_await`ed immediately. While having such a coroutines would + certainly be beneficial and it could be the recommended coroutine + for use from within another coroutine, it isn't a viable + replacement for `task` in all contexts: + + 1. It is intended that `task`s can be used as arguments to sender + algorithms, e.g., to `when_all` to execute multiple asynchronous + operations potentially concurrently, e.g.: + + task> do_work(std::string value) { /* work */ co_return; } + task> execute_all() { + co_await when_all( + do_work("arguments 1"), + do_work("arguments 2") + ); + }; + + 2. It is intended that `task`s can be used with `counting_scope` + (although to do so it is necessary to provide an environment + with a scheduler). There is no scoping relationship between + the `counting_scope` and the argument to `spawn`. + + 3. It is intended that `task` is used to encapsulate + the definition of an asynchronous operation into a function + which can be implemented in a separate translation unit and + that corresponding objects are returned from functions. + + While the idea of limiting the scope of `task` was considered + (admittedly that isn't reflected in the proposal paper), I don't + think there is a way to incorporate the proposed safety mechanism + into `task`. It can be incorporated into a different coroutine + type which can be `co_await`ed by `task`, though. + +The three minor points are: + +1. The use of `co_yield with_error(e)` is "clunky". There is no +disagreement here. A concrete reason why `co_yield with_error(e)` +isn't ideal is that `co_yield` cannot be used within a `catch` +block. In a future revision of the C++ standard it may become +possible that `co_return with_error(e)` can be supported (currently +that isn't possible in general because a promise type cannot provide +both `return_value` and `return_void` functions). However, using +`co_return` for both errors and normal returns introduces a small +problem when returning an object of type `with_error` as normal a +result (this is, however, a rather weird case): + + struct error_env { + using error_types = completion_signatures; + }; + []() -> task, error_env> { + co_return with_error(17); // error or success? + } + +2. The use of `co_await scheduler(sched);` does not result in the +coroutine being resumed on `sched`: due to scheduler affinity the +coroutine is resumed on the coroutine's scheduler, not on `sched` +(unless, of course, these two scheduler are identical). To change +a scheduler something else, specifically `co_await change_coroutine_scheduler(sched)`, +needs to be used. The use of `co_await scheduler(sched);` was used +by a sender/receiver library to replace the coroutine's scheudler +and the recommendation from this experience was to not use this +exact approach. The hinted at proposal is to use `co_yield sched;` +to change the scheduler: just because `co_yield` is used for returning +errors using `with_error(e)` doesn't mean it can't be used for other +uses and `co_yield x;` where `x` is a scheduler could change the +scheduler. However, it is quite intentional to use an explicitly +visible type like `change_coroutine_scheduler` to make changing the +scheduler visible. I have no objection to using `co_yield +change_coroutine_scheduler(sched);` instead of using `co_await` if +that is the preference. + +3. Coroutine cancellation is ad-hoc: the +[`std::execution`](https://wg21.link/p2300) proposal introduced the +use of `unhandled_stopped` to deal with cancellation. This use +wasn't part of the [`task` proposal](https://wg21.link/P3552). I +don't think it is reasonable to not have a coroutine `task` because +there could be a nicer language level approach in the future. + +I think I understand the concerns raised. I don't think that any +of them warrants removing the `task` from the standard document. +The argument that we should only standardize perfect design would +actually lead to hardly anything getting standardized because pretty +much everything is imperfect on some level. + +# Conclusion + +There are some parts of the specification which can be misunderstood. +These should be clarified correspondingly. In most of these cases +the change only affects the wording and doesn't affect the design. + +For the issues around `affine_on` there are some potential design +changes. In particular with respect to the exact semantics of +`affine_on` the design was possibly unclear. Likewise, explicitly +specifying that `task` customizes `affine_on` and provides an +`as_awaitable` function is somewhat in design space. The intention +was that doing so would be possible with the current specification +and it just didn't spell out how `co_await` a `task` from a `task` +actually works. + +However, some fixes of the specification are needed. + +# Acknowledgment + +The issue descriptions are largely based on [this +draft](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org) +written by Lewis Baker. Lewis Baker and Tomasz Kamiński contributed +to the discussions towards addressing the issues. diff --git a/subprojects/task/docs/P3941-affinity.md b/subprojects/task/docs/P3941-affinity.md new file mode 100644 index 000000000..7e9d72f3a --- /dev/null +++ b/subprojects/task/docs/P3941-affinity.md @@ -0,0 +1,1401 @@ +--- +title: Scheduler Affinity +document: P3941R4 +date: 2026-03-26 +audience: + - Concurrency Working Group (SG1) + - Library Evolution Working Group (LEWG) + - Library Working Group (LWG) +author: + - name: Dietmar Kühl (Bloomberg) + email: +source: + - https://github.com/bemanproject/task/doc/P3941-affinity.md +toc: true +--- + +

+One important design of `std::execution::task` is that a coroutine +resumes after a `co_await` on the same scheduler as the one it was +executing on prior to the `co_await`. To achieve this, `task` +transforms the awaited object `@_obj_@` using +`affine_on(@_obj_@, @_sched_@)` where `@_sched_@` is the corresponding +scheduler. There were multiple concerns raised against the specification +of `affine_on` and discussed as part of +[P3796R1](https://wg21.link/P3796R1). This proposal is intended +to specifically address the concerns raised relating to `task`'s +scheduler affinity and in particular `affine_on`. The gist of this +proposal is impose constraints on `affine_on` to guarantee it can +meet its objective at run-time. +

+ +# Change History + +## R4 + +- remove the alternative wording using `get_scheduler` +- remove fallback `get_scheduler` for `get_start_scheduler` +- use `get_start_scheduler` instead of `get_scheduler` to refer to + the scheduler an operation got started on (either for queries or + for defining environments) +- use `start_scheduler_type` instead of `scheduler_type` in `task` +- give implementations permission to define `@_sndr_@.affine_on()` + and `@_sndr_@.as_awaitable(@_p_@)` + +## R3 + +- rebase changes on the customization changes [P3826r3](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html) +- use `transform_sender` in `as_awaitable` to locate possible customization of nested + senders in [[exec.as.awaitable](https://wg21.link/exec.as.awaitable#7)] + +## R2 + +- added requirement on `get_scheduler`/`get_start_scheduler` +- fixed typo in the wording for `affine_on::transform_sender` +- fixed various typos in the text + +## R1 + +- added wording + +## R0 + +- initial revision + +# Overview of Changes + +

+There are a few NB comments raised about the way `affine_on` works: +

+
    +
  • [US 232-366](https://github.com/cplusplus/nbballot/issues/941): specify customization of `affine_on` when the scheduler doesn't change.
  • +
  • [US 233-365](https://github.com/cplusplus/nbballot/issues/940): clarify `affine_on` vs. `continues_on`.
  • +
  • [US 234-364](https://github.com/cplusplus/nbballot/issues/939): remove scheduler parameter from `affine_on`.
  • +
  • [US 235-363](https://github.com/cplusplus/nbballot/issues/938): `affine_on` should not forward the stop token to the scheduling operation.
  • +
  • [US 236-362](https://github.com/cplusplus/nbballot/issues/937): specify default implementation of `affine_on`.
  • +
+

+The discussion on `affine_on` revealed some aspects which were not +quite clear previously and taking these into account points towards +a better design than was previously specified: +

+
    +
  1. + To implement scheduler affinity the algorithm needs to know the + scheduler on which it was started itself. The correct scheduler + may actually be hard to determine while building the work graph. + However, this scheduler can be communicated using + `get_scheduler(get_env(@_rcvr_@))` when an algorithm + is `start`ed. This requirement is more general than just + `affine_on` and is introduced by + [P3718R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html): + with this guarantee in place, `affine_on` only needs one + parameter, i.e., the sender for the work to be executed. + [P3718R0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html) + is, however, discontinued (for unrelated reasons) and adding the guarantee + to get the current scheduler from a receiver query is proposed + here. +
  2. +
  3. + The scheduler `@_sched_@` on which the work needs + to resume has to guarantee that it is possible to resume on the + correct execution agent. The implication is that scheduling work needs + to be infallible, i.e., the completion signatures of + `scheduler(@_sched_@)` cannot contain a + `set_error_t(E)` completion signature. This requirement should + be checked statically. +
  4. +
  5. + The work needs to be resumed on the correct scheduler even when + the work is stopped, i.e., the scheduling operation shall be + `connect`ed to a receiver whose environment's `get_stop_token` + query yields an `unstoppable_token`. In addition, the + scheduling operation shall not have a `set_stopped_t()` completion + signature if the environment's `get_stop_token` query yields + an `unstoppable_token`. This requirement should also be checked + statically. +
  6. +
  7. + When a sender knows that it will complete on the scheduler it + was started on, it should be possible to customize the `affine_on` + algorithm to avoid rescheduling. This customization can be + achieved by `connect`ing to the result of an `affine_on` member + function called on the child sender, if such a member function + is present, when `connect`ing an `affine_on` sender. +
  8. +
+ +

+None of these changes really contradict any earlier design: the +shape and behavior of the `affine_on` algorithm wasn't fully fleshed +out. Tightening the behavior of scheduler affinity and the `affine_on` +algorithm has some implications on some other components: +

+
    +
  1. + If `affine_on` requires an infallible scheduler at least + `inline_scheduler`, `task_scheduler`, and `run_loop::scheduler` + should be infallible (i.e., they always complete successfully + with `set_value()`). `parallel_scheduler` can probably not be + made infallible. +
  2. +
  3. + The scheduling semantics when changing a `task`'s scheduler + using `co_await change_coroutine_scheduler(@_sch_@)` + become somewhat unclear and this functionality should be removed. + Similar semantics are better modeled using + `co_await on(@_sch_@, @_nested-task_@)`. +
  4. +
  5. + The name `affine_on` isn't particular good and wasn't designed. + It may be worth renaming the algorithms to something different. +
  6. +
+ +# Discussion of Changes + +## `affine_on` Shape + +

+The original proposal for `task` used `continues_on` to schedule +the work back on the original scheduler. This algorithm takes the +work to be executed and the scheduler on which to continue as +arguments. When SG1 requested that a similar but different algorithms +is to be used to implement scheduler affinity, `continues_on` was +just replaced by `affine_on` with the same shape but the potential +to get customized differently. +

+

+The scheduler used for affinity is the scheduler communicated via +the `get_scheduler` query on the receiver's environment: the scheduler +argument passed to the `affine_on` algorithm would need to match +the scheduler obtained from `get_scheduler` query. In the context +of the `task` coroutine this scheduler can be obtained via the +promise type but in general it is actually not straight forward to +get hold of this scheduler because the receiver and hence its +associated scheduler is only provided by `connect`. It is much more +reasonable to have `affine_on` only take the work, i.e., a sender, +as argument and determine the scheduler to resume on from the +receiver's +environment in `connect`. +

+

+Thus, instead of using +```c++ +affine_on(@_sndr_@, @_sch_@) +``` +the algorithm is used just with the sender: +```c++ +affine_on(@_sndr_@) +``` +

+

+Note that this change implies that an operation state resulting +from `connect`ing `affine_on` to a receiver `@_rcvr_@` +is `start`ed on the execution agent associated with the scheduler obtained +from `get_scheduler(get_env(@_rcvr_@))`. The same +requirement is also assumed to be met when `start`ing the operation +state resulting from `connect`ing a `task`. While it is possible to +statically detect whether the query is valid and provides a scheduler +it cannot be detected if the scheduler matches the execution agent on which +`start` was called. +[P3718r0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html) +proposed to add this exact requirement to +[[exec.get.scheduler]](https://wg21.link/exec.get.scheduler) which is no moved +to this proposal. +

+

+This change addresses [US 234-364](https://github.com/cplusplus/nbballot/issues/939) ([LWG4331](https://cplusplus.github.io/LWG/issue4331)). +

+ +## Scheduler An Operation Was Started On + +

+The `on` and `affine_on` algorithms both use the scheduler returned +from the `get_scheduler` query on the receiver to determine where +to resume execution after some other work completed. When using +these algorithms it is not always possible to determine which +scheduler an operation gets started on when creating a work graph. +Thus, the algorithms determine the scheduler from context. To +actually do that, it needs to be required that the scheduler is +made accessible when using these algorithms. +[P3718r0](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3718r0.html) +proposed that `get_scheduler` should be required to yield the +scheduler an operation was started on. + +

+ +

+It was pointed out that the meaning for the `get_scheduler` query isn't clear +and it is used for two separate purposes: +

+
    +
  1. The `get_scheduler` query is used to get a scheduler to schedule new work on. +
  2. The `get_scheduler` query is used to get the scheduler an operation was `start`ed on. +
+

+This dual use doesn't necessary align. It would be reasonable to +add a new query, e.g., `get_start_scheduler` which is used to +determine the scheduler an operation was `start`ed on. The requirement +for getting the scheduler an operation was started on would be on +the `get_start_scheduler` query rather than on the `get_scheduler` +query. At the same time the `get_start_scheduler` query could be +defaulted to use the `get_scheduler` query. +

+ +## Infallible Schedulers +

+The objective of `affine_on(@_sndr_@)` is to execute `@_sndr_@` and +to complete on the execution agent on which the operation was +`start`ed. Let `sch` be the scheduler obtained from +`get_scheduler(get_env(@_rcvr_@))` where `@_rcvr_@` is the receiver +used when `connect`ing `affine_on(@_sndr_@)` (the discussion in +this section also applies if the scheduler would be taken as a +parameter, i.e., if the [previous change](#affine_on-shape) isn't +applied this discussion still applies). If `connect`ing the result +of `schedule(@_sch_@)` fails (i.e., `connect(schedule(@_sch_@), +@_rcvr_@)` throws where `@_rcvr_@` is a suitable receiver), `affine_on` +can avoid `start`ing the main work and fail on the execution agent +where it was `start`ed. Otherwise, if it obtained an operation +state `@_os_@` from `connect(scheduler(@_sch_@), @_rcvr_@)`, +`affine_on` would `start` its main work and would `start(@_os_@)` +on the execution agent where the main work completed. If `start(@_os_@)` +is always successful, `affine_on` can achieve its objective. However, +if this scheduling operation fails, i.e., it completes with +`set_error(@_e_@)`, or if it gets cancelled, i.e., it completes +with `set_stopped()`, the execution agent on which the scheduling +operation resumes is unclear and `affine_on` cannot guarantee its +promise. Thus, it seems reasonable to require that a scheduler used +with `affine_on` is infallible, at least when used appropriately +(i.e., when providing a receiver whose associated stop token is an +`unstoppable_token`). + +

+

+The current working draft specifies 4 schedulers: +

+
    +
  1. + [`inline_scheduler`](https://wg21.link/exec.inline.scheduler) which + just completes with `set_value()` when `start()`ed, i.e., this + scheduler is already infallible. +
  2. +
  3. + [`task_scheduler`](https://wg21.link/exec.task.scheduler) is a + type-erased scheduler delegating to another scheduler. If the + underlying scheduler is infallible, the only error case for + `task_scheduler` is potential memory allocation during `connect` + of its `@_ts-sender_@`. If `affine_on` creates an operation state + for the scheduling operation during `connect`, it can guarantee + that any necessary scheduling operation succeeds. Thus, this + scheduler can be made infallible. +
  4. +
  5. + The [`run_loop::@_run-loop-scheduler_@`](https://wg21.link/exec.run.loop) + is used by [`run_loop`](https://wg21.link/exec.run.loop). The + current specification allows the scheduling operation to fail + with `set_error_t(std::exception_ptr)`. This permission allows + an implementation to use [`std::mutex`](https://wg21.link/thread.mutex) + and [`std::condition_variable`](https://wg21.link/thread.condition) + whose operations may throw. It is possible to implement the logic + using atomic operations which can't throw. The `set_stopped()` + completion is only used when the receiver's stop token, i.e. the + result of `get_stop_token(get_env(@_rcvr_@))`, was stopped. This + receiver is controlled by `affine_on`, i.e., it can provide a + [`never_stoptoken`](https://wg21.link/stoptoken.never) and this + scheduler won't complete with `set_stopped()`. If the + [`get_completion_signatures`](https://wg21.link/exec.getcomplsigs) for + the corresponding sender takes the environment into account, this + scheduler can also be made infallible. +
  6. +
  7. + The [`parallel_scheduler`](https://wg21.link/exec.par.scheduler) + provides an interface to a replaceable implementation of a thread + pool. The current interface allows + [`parallel_scheduler`](https://wg21.link/exec.par.scheduler) to + complete with `set_error_t(std::exception_ptr)` as well as with + `set_stopped_t()`. It seems unlikely that this interface can be + constrained to make it infallible. +
  8. +
+

+In general it seems unlikely that all schedulers can be constrained +to be infallible. As a result `affine_on` and, by extension, `task` +won't be usable with all schedulers if `affine_on` insists on using +only infallible schedulers. If there are fallible schedulers, there +aren't any good options for using them with a `task`. Note that +`affine_on` can fail and get cancelled (due to the main work failing +or getting cancelled) but `affine_on` can still guarantee that +execution resumes on the expect execution agent when it uses an +infallible scheduler. +

+

+This change addresses +[US 235-363](https://github.com/cplusplus/nbballot/issues/938) +([LWG4332](https://cplusplus.github.io/LWG/issue4332)). This change +goes beyond the actual issue and clarifies that the scheduling +operation used be `affine_on` needs to be always successful. +

+ +### Require Infallible Schedulers For `affine_on` + +

+If `affine_on` promises in all cases that it resumes on the +original scheduler it can only work with infallible schedulers. +If a users wants to use a fallible scheduler with `affine_on` or +`task` the scheduler will need to be adapted. The adapted scheduler +can define what it means when the underlying scheduler fails. There +are conceptually only two options (the exact details may vary) on how +to deal with a failed scheduling operation: +

+
    +
  1. +The user can transform the scheduling failure into a call to +`std::terminate`. +
  2. +
  3. +The user can consider resuming on an execution agent where the +adapting scheduler can schedule to infallibly (e.g., the execution +agent on which operation completed) but which is different from +execution agent associated with the adapted scheduler to be suitable +to continue running. In that case the scheduling operation would +just succeed without necessarily running on the correct execution +agent. However, there is no indication that scheduling to the adapted +scheduler failed and the scheduler affinity may be impacted in this +failure case. +
  4. +
+ +The standard library doesn't provide a way to adapt schedulers +easily. However, it can certainly be done. + +### Allow Fallible Schedulers For `affine_on` + +

+If the scheduler used with `affine_on` is allowed to fail, `affine_on` +can't guarantee that it completes on the correct scheduler in case of +an error completion. It could be specified that `affine_on` completes +with `set_error(@_rcvr_@, scheduling_error{@_e_@})` when the scheduling +operation completes with `set_error(@_r_@, @_e_@)` to make it detectable +that it didn't complete on the correct scheduler. This situation is +certainly not ideal but, at least, only affects the error completion and +it can be made detectable. +

+

+A use of `affine_on` which always needs to complete on a specific scheduler +is still possible: in that case the user will need to make sure that the +used scheduler is infallible. The main issue here is that there is no +automatic static checking whether that is the case. +

+ +### Considerations On Infallible Schedulers + +In an ideal world, all schedulers would be infallible. It is unclear +if that is achievable. If schedulers need to be allowed to be fallible, +it may be viable to require that all standard library schedulers +are infallible. As outlined above that should be doable for all current +schedulers except, possibly, `parallel_scheduler`. So, the proposed +change is to require schedulers to be infallible when being used with +`affine_on` (and, thus, being used by `task`) and to change as many of +the standard C++ libraries to be infallible as possible. + +If constraining `affine_on` to only infallible schedulers turns out +to be too strong, the constraint can be relaxed in a future revision +of the standard by explicitly opting out of that constraints, e.g., +using an additional argument. For `task` to make use of it, it too +would need an explicit mechanisms to indicate that its `affine_on` +use should opt out of the constraint, e.g., by adding a suitable +`static` member to the environment template argument. + +## `affine_on` Customization + +Senders which don't cause the execution agent to be changed like +`just` or the various queries should be able to customize `affine_on` +to avoid unnecessary scheduling. Sadly, a proposal +([P3206](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3206r0.pdf)) +to standardize properties which could be used to determine how a +sender completes didn't make much progress, yet. An implementation +can make use of similar techniques using an implementation-specific +protocol. If a future standard defines a standard approach to +determine the necessary properties the implementation can pick up +on those. + +The idea is to have `affine_on` define a `transform_sender(s)` +member function which determines what sender should be returned. +By default the argument is returned but if the child sender indicates +that it doesn't actually change the execution agent the function +would return the child sender. There are a number of senders for +which this can be done: + +- `just`, `just_error`, and `just_stopped` +- `read_env` and `write_env` +- `then`, `upon_error`, and `upon_stopped` if the child sender + doesn't change the execution agent + +The proposal is to define a `transform_sender` member which uses +an implementation-specific property to determine that a sender +completes on the same execution agent as the one it was started on. +In addition, it is recommended that this property gets defined by +the various standard library senders where it can make a difference. + +This change addresses +[US 232-366](https://github.com/cplusplus/nbballot/issues/941) +([LWG4329](https://cplusplus.github.io/LWG/issue4329)), although +not in a way allowing application code to plug into this mechanism. +Such an approach can be designed in a future revision of the standard. + +## Removing `change_coroutine_scheduler` + +The current working paper specifies `change_coroutine_scheduler` to change +the scheduler used by the coroutine for scheduler affinity. It turns out that +this use is somewhat problematic in two ways: + +1. Changing the scheduler affects the coroutine until the end of + the coroutine or until `change_coroutine_scheduler` is `co_await`ed + again. It doesn't automatically reset. Thus, local variables + constructed before `change_coroutine_scheduler(s)` was + `co_await`ed were constructed on the original scheduler and are + destroyed on the replaced scheduler. +2. The `task`'s execution may finish on a different than the original + scheduler. To allow symmetric transfer between two `task`s each + `task` needs to complete on the correct scheduler. Thus, the + `task` needs to be prepared to change to the original scheduler + before actually completing. To do so, it is necessary to know + the original scheduler and also to have storage for the state + needed to change to a different scheduler. It can't be statically + detected whether `change_coroutine_scheduler(s)` is `co_await`ed + in the body of a coroutine and, thus, the necessary storage and + checks are needed even for `task`s which don't use + `change_coroutine_scheduler`. + +If there were no way to change the scheduler it would still be possible +to execute using a different scheduler, although not as direct: +instead of using `co_await change_coroutine_scheduler(s)` to change +the scheduler used for affinity to `s` a nested `task` executing on `s` +could be `co_await`ed: + +```c++ +co_await ex::starts_on(s, [](@_parameters_@)->task<@_T_@, @_E_@> { @_logic_@ }(@_arguments_@)); +``` + +Using this approach the use of the scheduler `s` is clearly limited +to the nested coroutine. The scheduler affinity is fully taken care +of by the use of `affine_on` when `co_await`ing work. There is no +need to provide storage or checks needed for the potential of +having a `task` return to the original scheduler if the scheduler +isn't actually changed by a `task`. + +The proposal is remove `change_coroutine_scheduler` and the possibility +of changing the scheduler within a `task`. The alternative to +controlling the scheduler used for affinity from within a `task` +is a bit verbose. This need under the control of the coroutine is +likely relatively rare. Replacing the used scheduler for an existing +`task` by nesting it within `on(s, t)` or `starts_on(s, t)` is +fairly straightforward. + +This functionality was originally included because it is present +for, at least, one of the existing libraries, although in a form +which was recommended against. The existing use changes the scheduler +of a coroutine when `co_await`ing the result of `schedule(s)`; this +exact approach was found to be fragile and surprising and the +recommendation was to provide the functionality more explicit. + +This change is not associated with any national body comment. +However, it is still important to do! It isn't adding any new +functionality but removes a problematic way to achieve something +which can be better achieved differently. If this change is not +made the inherent cost of having the possibility of having +`change_routine_scheduler` can't be removed later without breaking +existing code. + +## `affine_on` Default Implementation + +Using the previous discussion leads to a definition of `affine_on` which +is quite different from effectively just using `continues_on`: + +1. The class `affine_on` + should define a `transform_sender` member function which returns the + child sender if this child sender indicates via an implementation + specific way that it doesn't change the execution agent. It + should be recommended that some of the standard library sender + algorithms (see above) to indicate that they don't change the + execution agent. +2. The `affine_on` algorithm should only allow to get `connect`ed to a + receiver `r` whose scheduler `sched` obtained by + `get_scheduler(get_env(r))` is infallible, i.e., + `get_completion_signatures(schedule(sched), e)` with an environment + `e` where `get_stop_token(e)` yields `never_stop_token` returns + `completion_signatures`. +3. When `affine_on` gets `connect`ed, the scheduling operation state needs + to be created by `connect`ing the scheduler's sender to a suitable receiver to guarantee + that the completion can be scheduled on the execution agent. + The stop token `get_stop_token(get_env(r))` for the receiver + `r` used for this `connect` shall be an `unstoppable_token`. + The child sender also needs to be `connect`ed with a receiver + which will capture the respective result upon completion and + start the scheduling operation. +4. When the result operation state gets `start`ed it `start`s the + operation state from the child operation. +5. Upon completion of the child operation the kind of completion and + the parameters, if any, are stored. If this operation throws, + the storage is set up to be as if `set_error(current_exception)` + were called. Once the parameters are stored, the scheduling + operation is started. +6. Upon completion of the scheduling operation, the appropriate + completion function with the respective arguments is invoked. + +This behavior is similar to `continues_on` but is subtly different +with respect to when the scheduling operation state needs to be +created and that any stop token from the receiver doesn't get +forwarded. In addition `affine_on` is more constrained with respect +to the schedulers it supports and the shape of the algorithm is +different: `affine_on` gets the scheduler to execute on from the +receiver it gets `connect`ed to. + +This change addresses +[US 233-365](https://github.com/cplusplus/nbballot/issues/940) +([LWG4330](https://cplusplus.github.io/LWG/issue4330)) and +[US 236-362](https://github.com/cplusplus/nbballot/issues/937) +([LWG](https://cplusplus.github.io/LWG/issue4344); the proposed +resolution in this issue is incomplete). + +## Name Change + +The name `affine_on` isn't great. It may be worth giving the +algorithm a better name. + +## Use get_start_scheduler Properly + +During the LEWG discussion on 2026-03-24 it was brought up that +`get_start_scheduler` shouldn't fallback to `get_scheduler`. To +make proper use of `get_start_scheduler` without that fallback, +algorithms current using `get_scheduler` also need to use +`get_start_scheduler` and advertise it appropriately: + +- `let*` pass the first operation's `completion_scheduler` + as the `get_start_scheduler` for the second operation +- `starts_on` should advertise `get_start_scheduler` +- `task` should propagate `get_start_scheduler` instead of + `get_scheduler`. Since it names the type, the type is also + renamed `start_scheduler_type`. +- `sync_wait` advertises its scheduler using both `get_scheduler` + and `get_start_scheduler`. + +# Wording Changes + +::: ednote +This wording is relative to [N5032](https://wg21.link/N5032) with +the changes from +[P3826r3](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html) +applied. +::: + +::: ednote + +Add an exposition only concept `@_infallible-scheduler_@` to +[[exec.sched](https://wg21.link/exec.sched)]: + +::: + +... + +[7]{.pnum} A scheduler type's destructor shall not block pending completion of any receivers connected to the sender objects returned from `schedule`. + +::: add +[8]{.pnum} The exposition-only `@_infallible-scheduler_@` concept defines +the requirements of a scheduler type whose `schedule` asynchronous operation +can only complete with `set_value` unless stop can be requested: + +``` +template +concept @_infallible-scheduler_@ = + scheduler && + (same_as, + completion_signatures_of_t())), Env> + > || + (!unstoppable_token> && ( + same_as, + completion_signatures_of_t())), Env> + > || + same_as, + completion_signatures_of_t())), Env> + >) + ) + ); +``` + +::: + + +::: ednote +Add `get_start_scheduler` to the synopsis in [execution.syn] after +`get_scheduler` as follows: +::: + +``` +... +namespace std::execution { + // [exec.queries], queries + struct get_domain_t { @_unspecified_@ }; + struct get_scheduler_t { @_unspecified_@ }; + @[struct]{.add}@ @[get_start_scheduler_t]{.add}@ @@[{ @_unspecified_@ };]{.add}@@ + struct get_delegation_scheduler_t { @_unspecified_@ }; + struct get_forward_progress_guarantee_t { @_unspecified_@ }; + template + struct get_completion_scheduler_t { @_unspecified_@ }; + template + struct get_completion_domain_t { @_unspecified_@ }; + struct get_await_completion_adaptor_t { @_unspecified_@ }; + + inline constexpr get_domain_t get_domain{}; + inline constexpr get_scheduler_t get_scheduler{}; + @[inline]{.add}@ @[constexpr]{.add}@ @[get_start_scheduler_t]{.add}@ @[get_start_scheduler{};]{.add}@ + inline constexpr get_delegation_scheduler_t get_delegation_scheduler{}; + enum class forward_progress_guarantee; + inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee{}; + template + constexpr get_completion_scheduler_t get_completion_scheduler{}; + template + constexpr get_completion_domain_t get_completion_domain{}; + inline constexpr get_await_completion_adaptor_t get_await_completion_adaptor{}; + ... +} +``` + +::: ednote +Add a new section after [exec.get.scheduler] as follows: +::: + +:::add + +### execution::get_start_scheduler [exec.get.start.scheduler] + +[1]{.pnum} `get_start_scheduler` asks a queryable object for the scheduler +an operation will be or was started on. + +[2]{.pnum} The name `get_start_scheduler` denotes a query object. For a +subexpression `env`, `get_start_scheduler(env)` is expression-equivalent to +MANDATE-NOTHROW(AS-CONST(env).query(get_start_scheduler)). + +Mandates: If the expression above is well-formed, its type satisfies `scheduler`. + +[3]{.pnum} `forwarding_query(execution::get_start_scheduler)` is a core +constant expression and has value true. + +[4]{.pnum} Given subexpressions `sndr` and `rcvr` such that +`sender_to` is `true` and the +expression `get_start_scheduler(get_env(rcvr))` is well-formed, an operation +state that is the result of calling `connect(sndr, rcvr)` shall, if +it is started, be started on an execution agent associated with the +scheduler `get_start_scheduler(get_env(rcvr))`. +::: + +::: ednote + +Add two paragraphs at the start of [exec.snd.expos] giving +implementations permission to add members `affine_on` and `as_awaitable` +to standard library sender types: + +::: + +::: add + +[?]{.pnum} Given an expression `sndr`, whose type is any sender +type defined in the standard library, it is unspecified whether the +expression `sndr.affine_on()` is well-formed. If that expression +is well-formed, then the evaluation thereof meets the +semantic requirements of the `affine_on` [exec.affine.on] algorithm. + +[?]{.pnum} Given an expression `sndr`, whose type is any sender type +defined in the standard library, and an expression `p`, whose type is a +promise type, it is unspecified whether the expression +`sndr.as_awaitable(p)` is well-formed. If that expression is +well-formed, then the evaluation thereof meets the semantic +requirements of the `as_awaitable` [exec.as.awaitable] algorithm. + +::: + +::: ednote + +Change [[exec.snd.expos](https://wg21.link/exec.snd.expos)] paragraph +8 to have SCHED-ENV use `get_start_scheduler` +instead of `get_scheduler`: + +::: + +[8]{.pnum} SCHED-ENV(sch) is an expression `o2` whose type +satisfies queryable such that `o2.query(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@)` +is a prvalue with the same type and value as `sch`, and such that +`o2.query(get_domain)` is expression-equivalent to `sch.query(get_domain)`. + +::: ednote +The specification of `on` [[exec.on](https://wg21.link/exec.on)] +shouldn't use `write_env` as it does, i.e., this change removes +these (not removing them was an oversight in +[P3826](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html)). + +In addition, if `get_start_scheduler` is introduced change how `on` +gets its scheduler in [[exec.on](https://wg21.link/exec.on)], i.e., +change the use from `get_scheduler` to use `get_start_scheduler`: + +::: + +

+... +

+ +

+[8]{.pnum} Otherwise, the expression `on.transform_sender(set_value, out_sndr, env)` has effects equivalent to: +

+ +``` +auto&& [_, data, child] = out_sndr; +if constexpr (scheduler) { + auto orig_sch = + @_call-with-default_@(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@, @_not-a-scheduler_@(), env); + + return continues_on( + starts_on(std::forward_like(data), std::forward_like(child)), + std::move(orig_sch)); +} else { + auto& [sch, closure] = data; + auto orig_sch = @_call-with-default_@( + get_completion_scheduler, @_not-a-scheduler_@(), get_env(child), env); + + return continues_on( + std::forward_like(closure)( + continues_on( + std::forward_like(child), + sch)), + orig_sch); +} +``` +

+[9]{.pnum} +Let `out_sndr` be a subexpression denoting a sender returned from +`on(sch, sndr)` or one equal to such, and let `OutSndr` be the type +`decltype((out_sndr))`. Let `out_rcvr` be a subexpression denoting a +receiver that has an environment of type `Env` such that +`sender_in` is `true`. Let `op` be an lvalue referring +to the operation state that results from connecting `out_sndr` with +`out_rcvr`. Calling `start(op)` shall +

    +
  • [9.1]{.pnum} remember the current +scheduler[,]{.rm}[ which is obtained by]{.add} `@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@(get_env(rcvr))`;
  • +
  • [9.2]{.pnum} start `sndr` on an execution agent belonging to `sch`'s associated +execution resource;
  • +
  • [9.3]{.pnum} +upon `sndr`'s completion, transfer execution back to the execution resource associated with the scheduler remembered in step 1; and
  • +
  • [9.4]{.pnum} +forward `sndr`'s async result to `out_rcvr`.
  • +
+If any scheduling operation fails, an error completion on `out_rcvr` shall be executed on an unspecified execution agent. +

+ +::: ednote + +Change [[exec.sync.wait](https://wg21.link/exec.sync.wait#2)] p2 to also provide +a `get_start_scheduler` query: + +::: + +Let `@_sync-wait-env_@` be the following exposition-only class type: + +``` +namespace std::this_thread { + struct sync-wait-env { + execution::run_loop* loop; // @_exposition only_@ + + auto query(execution::get_scheduler_t) const noexcept { + return loop->get_scheduler(); + } + + @[auto]{.add}@ @[query(execution::get_start_scheduler_t)]{.add}@ @[const]{.add}@ @[noexcept]{.add}@ @[{]{.add}@ + @[return]{.add}@ @[loop->get_scheduler();]{.add}@ + @[}]{.add}@ + + auto query(execution::get_delegation_scheduler_t) const noexcept { + return loop->get_scheduler(); + } + }; +} +``` + +::: ednote + +Change [[exec.as.awaitable](https://wg21.link/exec.as.awaitable#7)] +paragraph 7 such that it tries to locate a customization for +`as_awaitable` on the transformed nested sender. + +::: + +[7]{.pnum} `as_awaitable` is a customization point object. For +subexpressions `expr` and `p` where `p` is an lvalue, `Expr` names +the type `decltype((expr))` and `Promise` names the type +`decay_t, as_awaitable(expr, p)` is expression-equivalent +to, except that the evaluations of `expr` and `p` are indeterminately +sequenced: + +
    +
  • +

    [7.1]{.pnum} +`expr.as_awaitable(p)` if that expression is well-formed. +

    +

    +Mandates: `@_is-awaitable_@` is `true`, where `A` is +the type of the expression above. +

    +
  • +
  • + +[7.?]{.pnum} + +[Otherwise, +`@_adapt-for-await-completion_@(transform_sender(expr, +get_env(p))).as_awaitable(p)` if this expression is well-formed, +`sender_in>` is `true`, and +`@_single-sender-value-type_@>` is well-formed, +except that `p` is only evaluated once.]{.add} + +
  • +
  • +[7.2]{.pnum} Otherwise, `(void(p), expr)` if +`decltype(@_GET-AWAITER_@(expr))` satisfies `@_is-awaiter_@`. +
  • +
  • +

    +[7.3]{.pnum} [Otherwise, `@_sender-awaitable_@{@_adapted-expr_@, p}` if]{.rm} +

    +

    [`@_has-queryable-await-completion-adaptor_@`]{.rm}

    +

    [and]{.rm}

    +

    [`@_awaitable-sender_@`]{.rm}

    +

    +[are both satisfied, where `@_adapted-expr_@` is +`get_await_completion_adaptor(get_env(expr))(expr)`, except that +`expr` is evaluated only once.]{.rm} +

    +
  • +
  • +[7.4]{.pnum} Otherwise, [`@_sender-awaitable_@{expr, p}` if +`@_awaitable-sender_@` is `true`.]{.rm} +[`@_sender-awaitable_@{@_adapt-for-await-completion_@(transform_sender(expr, get_env(p))), p}` if `sender_in>` is `true` and `@_single-sender-value-type_@>` is well-formed], except that `p` is only evaluated once.{.add} + +
  • +
  • [7.5]{.pnum} Otherwise, `(void(p), expr)`. +
  • +
+ +::: add + +[8]{.pnum} `@_adapt-for-await-completion_@(s)` is expression-equivalent +to +
    +
  • `get_await_completion_adaptor(get_env(s))(s)` if that is well-formed, except that `s` is evaluated only once,
  • +
  • otherwise, `s`.
  • +
+ +::: + +::: ednote +Change [exec.affine.on] to use only one parameter, require an +infallible scheduler from the receiver, and add a default implementation +which allows customization of `affine_on` for child senders. +The algorithm `affine_on` should use +`get_start_scheduler` to get the start scheduler: +::: + +[1]{.pnum} +`affine_on` adapts a sender into one that completes on a [specified +scheduler]{.rm}[receiver's scheduler]{.add}. If the algorithm +determines that the adapted sender already completes on the correct +scheduler it can avoid any scheduling operation. + +[2]{.pnum} +The name `affine_on` denotes a pipeable sender adaptor object. For +[a ]{.add} subexpression[s sch and]{.rm} `sndr`, if [`decltype((sch))` +does not satisfy scheduler, or]{.rm} `decltype((sndr))` does not +satisfy sender, affine_on(sndr[, sch]{.rm}) is ill-formed. + +[3]{.pnum} +Otherwise, the expression affine_on(sndr[, sch]{.rm}) +is expression-equivalent to +`@_make-sender_@(affine_on, @[sch]{.rm}@@[env<>()]{.add}@, sndr)`. + +:::{.add} +[?]{.pnum} For a subexpression `sch` whose type models `scheduler`, +let `@_UNSTOPPABLE-SCHEDULER_@(sch)` be an expression `e` whose type +models `scheduler` such that: + +
    +
  • [?.1]{.pnum} `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.
  • +
  • [?.2]{.pnum} For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)` +is expression-equivalent to `sch.query(q, args...)`.
  • +
  • [?.3]{.pnum} The expression `e == @_UNSTOPPABLE-SCHEDULER_@(other)` +is expression-equivalent to `sch == other`.
  • +
+ +[?]{.pnum} +Let `sndr` and `ev` be subexpressions such that `Sndr` is +`decltype((sndr))`. If sender-for<Sndr, +affine_on_t> is `false`, then the expression +`affine_on.transform_sender(sndr, ev)` is ill-formed; otherwise, +it is equal to: + +``` +auto&[_, _, child] = sndr; +if constexpr (requires{ std::forward_like(child).affine_on(); }) + return std::forward_like(child).affine_on(); +else + return continues_on(std::forward_like(child), @_UNSTOPPABLE-SCHEDULER_@(get_start_scheduler(ev))); +``` + +[?]{.pnum} +_Recommended Practice_: Implementations should provide `affine_on` +member functions for senders that are known to resume on the +scheduler where they were started. Example senders for which that +is the case are `just`, `just_error`, `just_stopped`, `read_env`, +and `write_env`. + +::: + +[5]{.pnum} +Let _out_sndr_ be a subexpression denoting a sender +returned from affine_on(sndr[, sch]{.rm}) or one equal +to such, and let _OutSndr_ be the type +decltype((_out_sndr_)). Let out_rcvr +be a subexpression denoting a receiver that has an environment of +type `Env` [such that sender_in<_OutSndr_, Env> +is `true`]{.rm}. [If get_start_scheduler(get_env(out_rcvr)) is ill-formed +or does not satisfy `@_infallible-scheduler_@` then evaluation of the +expression `get_completion_signatures<@_OutSndr_@, Env>()` exits with +an exception. +]{.add} Let `op` be an lvalue referring to the operation +state that results from connecting _out_sndr_ to +_out_rcvr_. Calling start(_op_) will +start `sndr` on the current execution agent and execute completion +operations on _out_rcvr_ on an execution agent of the +execution resource associated with [`sch`]{.rm}[_sch_]{.add}. +If the current execution resource is the same as the execution +resource associated with [`sch`]{.rm}[_sch_]{.add}, +the completion operation on _out_rcvr_ may be called +before start(_op_) completes. [If scheduling onto `sch` +fails, an error completion on _out_rcvr_ shall be +executed on an unspecified execution agent.]{.rm} + +::: ednote +Remove `change_coroutine_scheduler` from [execution.syn]: +::: + +``` +namespace std::execution { + ... + // [exec.task.scheduler], task scheduler + class task_scheduler; + + template + struct with_error { + using type = remove_cvref_t; + type error; + }; + template + with_error(E) -> with_error; +``` +::: rm +``` + template + struct change_coroutine_scheduler { + using type = remove_cvref_t; + type scheduler; + }; + template + change_coroutine_scheduler(Sch) -> change_coroutine_scheduler; +``` +::: +``` + // [exec.task], class template task + template + class task; + ... +} +``` + +::: ednot + +In [[task.class](https://wg21.link/task.class)] change `scheduler_type` to +`start_scheduler_type`: + +::: + +``` +namespace std::execution { + template> + class task { + // [task.state] + template + class @_state_@; // @_exposition only_@ + + public: + using sender_concept = sender_t; + using completion_signatures = @_see below_@; + using allocator_type = @_see below_@; + using @[scheduler_type]{.rm}@@[start_scheduler_type]{.add}@ = @_see below_@; + using stop_source_type = @_see below_@; + using stop_token_type = decltype(declval().get_token()); + using error_types = @_see below_@; + + // [task.promise] + class promise_type; + + task(task&&) noexcept; + ~task(); + + template + @_state_@ connect(Rcvr&& rcvr) &&; + + private: + coroutine_handle handle; // @_exposition only_@ + }; +} +``` + +[1]{.pnum} +`task` models sender ([exec.snd]) if `T` is `void`, a reference +type, or a _cv_-unqualified non-array object type and `E` is a class +type. Otherwise a program that instantiates the definition of +`task` is ill-formed. + +[2]{.pnum} +The nested types of task template specializations are determined based on the `Environment` parameter: +
    +
  • [2.1]{.pnum} +`allocator_type` is `Environment::allocator_type` if that _qualified-id_ +is valid and denotes a type, `allocator` otherwise.
  • +
  • [2.2]{.pnum} +`@[scheduler_type]{.rm}@@[start_scheduler_type]{.add}@` is `Environment::@[scheduler_type]{.rm}@@[start_scheduler_type]{.add}@` if that _qualified-id_ + is valid and denotes a type, `task_scheduler` otherwise.
  • +
  • [2.3]{.pnum} +`stop_source_type` is `Environment::stop_source_type` if that +_qualified-id_ is valid and denotes a type, `inplace_stop_source` +otherwise.
  • +
  • [2.4]{.pnum} +`error_types` is `Environment::error_types` if that _qualified-id_ +is valid and denotes a type, +`completion_signatures` otherwise.
  • +
+ +[3]{.pnum} + +A program is ill-formed if `error_types` is not a specialization +of `execution::completion_signatures` or if the template arguments +of that specialization contain an element which is not of the form +`set_error_t(E)` for some type `E`. + +[4]{.pnum} +The type alias `completion_signatures` is a specialization of +`execution::completion_signatures` with the template arguments (in +unspecified order): + +
    +
  • [4.1]{.pnum} +`set_value_t()` if `T` is `void`, and `set_value_t(T)` otherwise;
  • +
  • [4.2]{.pnum} +template arguments of the specialization of +`execution::completion_signatures` denoted by `error_types`; and
  • +
  • [4.3]{.pnum} `set_stopped_t()`.
  • +
+[5]{.pnum} `allocator_type` shall meet the _Cpp17Allocator_ requirements. + +::: ednote +Change the scheduler propagation to `start` to use `get_start_scheduler` in [[task.state](https://wg21.link/task.state#4)] p4: +::: + +``` +void start() & noexcept; +``` + +[4]{.pnum} _Effects_: Let `@_prom_@` be the object `@_handle_@.promise()`. Associates `@_STATE_@(@_prom_@)`, `@_RCVR_@(@_prom_@)`, and `@_SCHED_@(@_prom_@)` with `*this` as follows: + +
    +
  • [4.1]{.pnum} `@_STATE_@(prom)` is `*this`.
  • +
  • [4.2]{.pnum} `@_RCVR_@(@_prom_@)` is `@_rcvr_@`.
  • +
  • [4.3]{.pnum} `@_SCHED_@(@_prom_@)` is the object initialized with +`@[scheduler_type]{.rm}@@[start_scheduler_type]{.add}@(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@(get_env(@_rcvr_@)))` if that expression is +valid and `@[scheduler_type]{.rm}@@[start_scheduler_type]{.add}@()` otherwise. If neither of these expressions +is valid, the program is ill-formed.
  • +
+ +Let `@_st_@` be `get_stop_token(get_env(@_rcvr_@))`. Initializes `@_prom_@.@_token_@` +and `@_prom.source_@` such that + +
    +
  • [4.4]{.pnum} +`@_prom_@.@_token_@.stop_requested()` returns `@_st_@.stop_requested()`;
  • +
  • [4.5]{.pnum} +`@_prom_@.@_token_@.stop_possible()` returns `@_st_@.stop_possible()`; and
  • +
  • [4.6]{.pnum} +for types `Fn` and `Init` such that both `invocable` and +`constructible_from` are modeled, +`stop_token_type::callback_type` models +`@_stoppable-callback-for_@`.
  • +
+ +After that invokes `@_handle_@.resume()`. + +::: ednote +Use `get_start_scheduler` instead of `get_scheduler` when providing +an environment to the `co_await`ed sender in +[[task.state](https://wg21.link/task.state#16)] p16: +::: + +``` +@_unspecified_@ get_env() const noexcept; +``` +[16]{.pnum} + +_Returns_: An object `env` such that queries are forwarded as follows: +
    +
  • [16.1]{.pnum} +`env.query(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@)` returns `@[scheduler_type]{.rm}@@[start_scheduler_type]{.add}@(@_SCHED_@(*this))`.
  • +
  • [16.2]{.pnum} +`env.query(get_allocator)` returns `@_alloc_@`.
  • +
  • [16.3]{.pnum} +`env.query(get_stop_token)` returns `@_token_@`.
  • +
  • [16.4]{.pnum} For any other query `q` and arguments `a...` a call +to `env.query(q, a...)` returns `STATE(*this).environment.query(q, +a...)` if this expression is well-formed and `forwarding_query(q)` is +well-formed and is `true`. Otherwise `env.query(q, a...)` is ill-formed.
  • +
+ +::: ednote +Adjust the use of `affine_on` and remove `change_coroutine_scheduler` from [task.promise]: +::: + +``` +namespace std::execution { + template + class task::promise_type { + public: + ... + + template + auto await_transform(A&& a); +``` +::: rm +``` + template + auto await_transform(change_coroutine_scheduler sch); +``` +::: +``` + + @_unspecified_@ get_env() const noexcept; + + ... + } +}; +``` +... + +``` +template + auto await_transform(Sender&& sndr) noexcept; +``` +[9]{.pnum} +_Returns_: If `same_as` is `true` returns `as_awaitable(​std​::​​forward(sndr), *this);` otherwise returns `as_awaitable(affine_on(​std​::​​forward(sndr)@[, SCHED(*this)]{.rm}@), *this)`. + +::: rm +``` +template + auto await_transform(change_coroutine_scheduler sch) noexcept; +``` +[10]{.pnum} +_Effects_: Equivalent to: +``` +return await_transform(just(exchange(SCHED(*this), scheduler_type(sch.scheduler))), *this); +``` +::: + +``` +void unhandled_exception(); +``` +[11]{.pnum} +_Effects_: If the signature `set_error_t(exception_ptr)` is not an element of `error_types`, calls `terminate()` ([except.terminate]). Otherwise, stores `current_exception()` into _errors_. + +... + +::: ednote +In [exec.task.scheduler] change the constructor of `task_scheduler` to require that the scheduler passed +is infallible +::: + +``` +template> + requires(!same_as>) && scheduler +explicit task_scheduler(Sch&& sch, Allocator alloc = {}); +``` + +::: add +[?]{.pnum} +_Mandates_: `Sch` satisfies `@_infallible-scheduler_@>`. + +::: + +[2]{.pnum} +_Effects_: Initialize sch_ with `allocate_shared>(alloc,​ std​::​forward​(sch))`. + +[3]{.pnum} +_Recommended practice_: Implementations should avoid the use of +dynamically allocated memory for small scheduler objects. + +[4]{.pnum} +_Remarks_: Any allocations performed by construction of +_ts-sender_ or _state_ objects resulting +from calls on `*this` are performed using a copy of `alloc`. + +::: ednote +In [exec.task.scheduler] change the ts-sender completion signatures +to indicate that `task_scheduler` is infallible: +::: + +[8]{.pnum} +``` +namespace std::execution { + class task_scheduler::@_ts-sender_@ { // @_exposition only_@ + public: + using sender_concept = sender_t; + + template + @_state_@ connect(Rcvr&& rcvr) &&; + }; +} +``` + +ts-sender is an exposition-only class that +models `sender` ([exec.snd]) and for which +completion_signatures_of_t<ts-sender[, E]{.add}> +denotes[:]{.rm}[ `completion_signatures` if `unstoppable_token>` is `true`, and +otherwise `completion_signatures`.]{.add} + +::: rm +``` +completion_signatures< + set_value_t(), + set_error_t(error_code), + set_error_t(exception_ptr), + set_stopped_t()> +``` +::: + +::: ednote +In [exec.run.loop.types] change the paragraph defining the completion signatures: +::: + +... + +``` +class run-loop-sender; +``` + +[6]{.pnum} +run-loop-sender is an exposition-only type that satisfies `sender`. +[Let `E` be the type of an environment. If `unstoppable_token>` is `true`, +then ]{.add} completion_signatures_of_t<run-loop-sender[, E]{.add}> is + +::: rm +``` + completion_signatures +``` +::: + +::: add +``` + completion_signatures +``` +Otherwise it is +``` + completion_signatures +``` +::: + +[7]{.pnum} An instance of run-loop-sender remains +valid until the end of the lifetime of its associated `run_loop` +instance. + +... + +::: ednote +Change +[[exec.counting.scopes.general](https://wg21.link/exec.counting.scopes.general#4)] +p4 to use `get_start_scheduler` instead of `get_scheduler`: +::: + +[4]{.pnum} The exposition-only class template `@_impls-for_@` ([exec.snd.expos]) is specialized for `@_scope-join-t_@` as follows: + +``` +namespace std::execution { + template<> + struct @_impls-for_@<@_scope-join-t_@> : @_default-impls_@ { + template + struct state { // @_exposition only_@ + struct @_rcvr-t_@ { // @_exposition only_@ + using receiver_concept = receiver_t; + + Rcvr& @_rcvr_@; // @_exposition only_@ + + void set_value() && noexcept { + execution::set_value(std::move(@_rcvr_@)); + } + + template + void set_error(E&& e) && noexcept { + execution::set_error(std::move(@_rcvr_@), std::forward(e)); + } + + void set_stopped() && noexcept { + execution::set_stopped(std::move(@_rcvr_@)); + } + + decltype(auto) get_env() const noexcept { + return execution::get_env(@_rcvr_@); + } + }; + + using @_sched-sender_@ = // @_exposition only_@ + decltype(schedule(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@(get_env(declval())))); + using @_op-t_@ = // @_exposition only_@ + connect_result_t<@_sched-sender_@, @_rcvr-t_@>; + + Scope* @_scope_@; // @_exposition only_@ + Rcvr& @_receiver_@; // @_exposition only_@ + @_op-t_@ @_op_@; // @_exposition only_@ + + @_state_@(Scope* scope, Rcvr& rcvr) // @_exposition only_@ + noexcept(@_nothrow-callable_@) + : @_scope_@(scope), + @_receiver_@(rcvr), + @_op_@(connect(schedule(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@(get_env(rcvr))), @_rcvr-t_@(rcvr))) {} + + void @_complete_@() noexcept { // @_exposition only_@ + start(@_op_@); + } + + void @_complete-inline_@() noexcept { // @_exposition only_@ + set_value(std::move(@_receiver_@)); + } + }; + + static constexpr auto @_get-state_@ = // @_exposition only_@ + [](auto&& sender, Rcvr& receiver) + noexcept(is_nothrow_constructible_v<@_state_@, @_data-type_@, Rcvr&>) { + auto[_, self] = sender; + return @_state_@(self, receiver); + }; + + static constexpr auto @_start_@ = // @_exposition only_@ + [](auto& s, auto&) noexcept { + if (s.@_scope_@->@_start-join-sender_@(s)) + s.@_complete-inline_@(); + }; + }; +} +``` diff --git a/subprojects/task/docs/P3980-allocation.md b/subprojects/task/docs/P3980-allocation.md new file mode 100644 index 000000000..0a6e9d3ba --- /dev/null +++ b/subprojects/task/docs/P3980-allocation.md @@ -0,0 +1,388 @@ +--- +title: Task's Allocator Use +document: P3980R1 +date: 2026-03-24 +audience: + - Library Evolution Working Group (LEWG) + - Library Working Group (LWG) +author: + - name: Dietmar Kühl (Bloomberg) + email: +source: + - https://github.com/bemanproject/task/doc/P3980-allocation.md +toc: true +--- + +

+There are different uses of allocators for `task`. The obvious use +is that the coroutine frame needs to be allocated and using an +allocator control where this coroutine frame gets allocated. In +addition, the environment used when `connect`ing a sender can provide +access to an allocator via the `get_allocator` query. The current +specification uses the same allocator for coroutine frame and the +child environments. At the Kona meeting the room preferred if these +were separated and the allocator for the environment were taken +from the environment of the receiver it gets `connect`ed to. In +doing so, the allocator for the coroutine frame becomes more flexible +and it should be brought more in line with `generator`'s allocator +use. This paper addresses +[US 254-385](https://github.com/cplusplus/nbballot/issues/960), +[US 253-386](https://github.com/cplusplus/nbballot/issues/961), +[US 255-384](https://github.com/cplusplus/nbballot/issues/959), +and [US 261-391](https://github.com/cplusplus/nbballot/issues/966). +

+ +# Change History + +## R0 Initial Revision + +# Overview of Changes + +

+There are a few NB comments about `task`'s use of allocators: +

+
    +
  • [US 254-385](https://github.com/cplusplus/nbballot/issues/960): Constraint `allocator_arg` argument to be the first argument
  • +
  • [US 253-386](https://github.com/cplusplus/nbballot/issues/961): Allow use of arbitrary allocators for coroutine frame
  • +
  • [US 255-384](https://github.com/cplusplus/nbballot/issues/959): Use allocator from receiver's environment
  • +
  • [US 261-391](https://github.com/cplusplus/nbballot/issues/966): Bad specification of parameter type
  • +
+

+The first issue +([US 254-385](https://github.com/cplusplus/nbballot/issues/960)) is +about where an allocator argument for the coroutine frame may go +on the coroutine function. The options are a fixed location (which +would fit first for consistency with existing use) and anywhere. The +status quo is anywhere, and the request is to require that it goes +first. However, to support optionally passing an allocator, having +it go anywhere is easier to do. +

+

+The allocator constraints for allocating the coroutine frame are +due to the use of the same allocator for the environment of child +senders. If the allocator for the environment of child senders uses +the allocator from the receiver's environment, these constraints +can be relaxed. Instead, there may be requirements on the result +of the `get_allocator` query from the receiver's environment. The +discussion in Kona favored this direction. This change can address +the second ([US 253-386](https://github.com/cplusplus/nbballot/issues/961)) +and the third +([US 255-384](https://github.com/cplusplus/nbballot/issues/959)) issues. +

+

+The fourth issue ([US +261-391](https://github.com/cplusplus/nbballot/issues/966)) is +primarily a wording issue. However, some of the problematic paragraphs +will need some modifications to address the other issues, i.e., fixing +these wording issues in isolation isn't reasonable. +

+ +# Allocator Argument Position + +

+The combination of using `allocator_arg` followed by an allocator +object when invoking a function or a constructor is used in various +places throughout the standard C++ library. The `allocator_arg` +argument normally needs to be the first argument when present. The +definition of `task` makes the position of the `allocator_arg` more +flexible to allow easier support for optionally passing an allocator. +

+

+For coroutines, the arguments to the coroutine [factory] function +show up in three separate places: +

+
    +
  1. The parameters to the coroutine [factory] functions.
  2. +
  3. The constructor of the `promise_type` if there is a suitable matching overload.
  4. +
  5. the `operator new()` of the `promise_type` if there is a suitable matching overload.
  6. +
+

+This added flexibility doesn't introduce any constraints on how the coroutine +function is defined. It rather allows passing an `allocator_arg`/allocator +pair without requiring a specific location. The main benefit is that support +of an optional allocator can be supported by having a trailing `, auto&&...` +on the parameter list. Note that the allocator used for the coroutine frame +is normally not used in the body of the allocator. If it is needed, it can +in all cases be put into the first location. +

+::: cmptable + +### First +``` +task<> none(int x) +{ ... } +``` + +### Flexible +``` +task<> none(int x) +{ ... } +``` + +--- + +``` +task<> comes_first(allocator_arg_t, auto a, int x) +{ ... } +``` + +``` +task<> comes_first(allocator_arg_t, auto a, int x) +{ ... } +``` + +--- + +``` +task<> optional(allocator_arg_t, auto, int x) +{ ... } +task<> optional(int x) +{ return optional(allocator_arg, allocator(), x); } +``` + +``` +task<> optional(int x, auto&&...) +{ ... } +``` + +::: + +

+The comparison table above shows three separate cases the author of +a coroutine function may want to support: +

+
    +
  1. No allocator support (`none`): the use identical and just doesn't mention any allocator.
  2. +
  3. Mandate that the allocator is the first argument (`comes_first`): the use is identical.
  4. +
  5. + Support optionally passing an `allocator_arg`/allocator pair + (`optional`): the use can be identical but it can also be simplified + taking advantage of the flexible location. +
  6. +
+ +Below are three variations of the wording changes, only one can be picked: + +
    +
  1. Only support `allocator_arg` as the first argument and use the receiver's allocator for the environment.
  2. +
  3. Flexible position of the allocator arg and use allocator for the environment.
  4. +
  5. Flexible position of the allocator arg and use the receiver's allocator for the environment.
  6. +
+ +At the LEWG meeting on 2026-02-03 the first approach (putting the `allocator_arg` first, Wording Change A) +was preferred ([notes](https://wiki.isocpp.org/2026-02-03_LEWG_Telecon)). It was identified that the original wording change did not support member functions returning a `task` (the wording was fixed accordingly). + +## Wording Change A: `allocator_arg` must be first argument + +::: ednote +Change the synopsis of `promise` type in [task.promise], modifying +the overloads of `operator new`: +::: + +``` +namespace std::execution { + template + class task::promise_type { + public: + ... + unspecified get_env() const noexcept; + + @[void*]{.add}@ @[operator]{.add}@ @[new(size_t]{.add}@ @[size);]{.add}@ + template<@[class Alloc,]{.add}@ class... Args> + void* operator new(size_t size, @[allocator_arg_t,]{.add}@ @[Alloc]{.add}@ @[alloc, ]{.add}@Args&&... @[args]{.rm}@); + @[template]{.add}@ + @[void*]{.add}@ @[operator]{.add}@ @[new(size_t]{.add}@ @[size,]{.add}@ @[const]{.add}@ @[This&,]{.add}@ @[allocator_arg_t,]{.add}@ @[Alloc]{.add}@ @[alloc,]{.add}@ @[Args&&...);]{.add}@ + + void operator delete(void* pointer, size_t size) noexcept; + + private: + ... + }; +} +``` + +::: ednote +Change [task.promise] paragraphs 17 and 18: +::: + +::: add + +``` +void* operator new(size_t size); +``` + +[??]{.pnum} _Effects_: Equivalent to `return operator new(size, allocator_arg, allocator_type());` + +::: + +``` +template<@[class Alloc,]{.add}@ class... Args> + void* operator new(size_t size, @[allocator_arg_t,]{.add}@ @[Alloc]{.add}@ @[alloc, ]{.add}@Args&&... @[args]{.rm}@); +@[template]{.add}@ + @[void*]{.add}@ @[operator]{.add}@ @[new(size_t]{.add}@ @[size,]{.add}@ @[const]{.add}@ @[This&,]{.add}@ @[allocator_arg_t,]{.add}@ @[Alloc]{.add}@ @[alloc,]{.add}@ @[Args&&...);]{.add}@ +``` + +[17]{.pnum} [If there is no parameter with type `allocator_arg_t` +then let `alloc` be `allocator_type()`. Otherwise, let `arg_next` +be the parameter following the first `allocator_arg_t` parameter, +and let `alloc` be `allocator_type(arg_next)`]{.rm}. Let `PAlloc` +be `allocator_traits<@[allocator_type]{.rm}@@[Alloc]{.add}@>::template +rebind_alloc`, where `U` is an unspecified type whose size and +alignment are both `__STDCPP_DEFAULT_NEW_ALIGNMENT__`. + +::: rm + +[18]{.pnum} +_Mandates_: + +
    +
  • [18.1]{.pnum} The first parameter of type `allocator_arg_t` (if any) is not the last parameter.
  • +
  • [18.2]{.pnum} `allocator_type(arg_next)` is a valid expression if there is a parameter of type `allocator_arg_t`.
  • +
  • [18.3]{.pnum} `allocator_traits​::​pointer` is a pointer type.
  • +
+ +::: + +::: add + +[18]{.pnum} +_Mandates_: `allocator_traits​::​pointer` is a pointer type. + +::: + +[19]{.pnum} _Effects_: Initializes an allocator `palloc` of type +`PAlloc` with `alloc`. Uses `palloc` to allocate storage for the +smallest array of `U` sufficient to provide storage for a coroutine +state of size `size`, and unspecified additional state necessary to +ensure that `operator delete` can later deallocate this memory block +with an allocator equal to `palloc`. + +[20]{.pnum} _Returns_: A pointer to the allocated storage. + +# Use Allocator From Environment + +

+During the discussion at Kona the conclusion was that the allocator +forwarded by `task`'s environment to child senders should be the +allocator from `get_allocator` on the receiver `task` gets `connect`ed +to. Let `rcvr` be the receiver a `task` got `connect`ed to and let `ev` +be the result of `get_env(rcvr)`. The implication is that the `task`'s +`allocator_type` is compatible with the allocator of `ev`: +

+
    +
  • + If `get_allocator(ev)` is not defined, `allocator_type` has to + be default constructible. +
  • +
  • + Otherwise `allocator_type(get_allocator(ev))` has to be well-formed. +
  • +
  • + There is no need to store an alloc in the + `promise_type`: it can be obtained when requested from `ev` which, + in turn, can be obtained from `rcvr`. Thus, the ctor for `promise_type` + isn't needed. +
  • +
  • + The definition of `get_env` needs to be changed to get the allocator + when needed. +
  • +
+ +## Wording Changes + +::: ednote + +In [[task.members](https://wg21.link/task.members#3)] add a _Mandates_ to `connect`: + +::: + +``` +template + state connect(Rcvr&& recv) &&; +``` + +::: add + +[?]{.pnum} _Mandates_: At least one of the expressions `allocator_type(get_allocator(get_env(rcvr)))` and `allocator_type()` is well-formed. + +::: + +[3]{.pnum} _Preconditions_: bool(handle) is `true`. + +[4]{.pnum} _Effects_: Equivalent to: +``` +return state(exchange(handle, {}), std::forward(recv)); +``` + +::: ednote + +In [[task.promise]](https://wg21.link/task.promise) in the synopsis remove the `promise_type` constructor and +the alloc exposition-only member. + +::: + +``` +namespace std::execution { + template + class task::promise_type { + public: + @[template]{.rm}@ + @[promise_type(const]{.rm}@ @[Args&...]{.rm}@ @[args);]{.rm}@ + + task get_return_object() noexcept; + + ... + private: + using error-variant = see below; // @_exposition only_@ + + @[allocator_type]{.rm}@ @[_alloc_;]{.rm}@ @[//]{.rm}@ @[_exposition_]{.rm}@ @[_only_]{.rm}@ + stop_source_type @_source_@; // @_exposition only_@ + stop_token_type @_token_@; // @_exposition only_@ + optional @_result_@; // @_exposition only_@; present only if is_void_v is false + error-variant @_errors_@; // @_exposition only_@ + }; +} +``` + +::: ednote + +Remove the ctor for `promise_type`, i.e., [[task.promise]](https://wg21.link/task.promise) paragraph [3](https://wg21.link/task.promise#3) and [4](https://wg21.link/task.promise#4): + +::: + +::: rm + +``` +template + promise_type(const Args&... args); +``` + +[3]{.pnum} _Mandates_: The first parameter of type `allocator_arg_t` +(if any) is not the last parameter. + +[4]{.pnum} _Effects_: If `Args` contains an element of type +`allocator_arg_t` then alloc is initialized +with the corresponding next element of `args`. Otherwise, +alloc is initialized with `allocator_type()`. + +::: + +::: ednote + +Change `get_env` to get the allocator from the receiver when needed +in [[task.promise] p16](https://wg21.link/task.promise#16): + +::: + +``` +@_unspecified_@ get_env() const noexcept; +``` + +[16]{.pnum} _Returns_: An object `env` such that queries are forwarded as follows: +
    +
  • [16.1]{.pnum} `env.query(get_scheduler)` returns scheduler_type(SCHED(*this)).
  • +
  • [16.2]{.pnum} `env.query(get_allocator)` returns [alloc]{.rm}[allocator_type(get_allocator(get_env(RCVR(*this)))) if this expression is well-formed and `allocator_type()` otherwise]{.add}.
  • +
  • [16.3]{.pnum} `env.query(get_stop_token)` returns token.
  • +
  • [16.4]{.pnum} For any other query `q` and arguments `a...` a call to `env.query(q, a...)` returns STATE(*this).environment.query(q, a...) if this expression is well-formed and `forwarding_query(q)` is well-formed and is `true`. Otherwise `env.query(q, a...)` is ill-formed.
  • +
diff --git a/subprojects/task/docs/c++now.md b/subprojects/task/docs/c++now.md new file mode 100644 index 000000000..17f5e0f93 --- /dev/null +++ b/subprojects/task/docs/c++now.md @@ -0,0 +1,46 @@ +# Getting The Lazy Task Done + +## Description + +The C++26 specificatiom includes the sender/receiver framework for +dealing with asynchronous operations. It defines a vocualary and +various algorithms and utilities using this vocabulary. The current +specification doesn't include matching coroutine task class template +although it is expected that most users would use coroutines to +actually use the facilities. This presentation discusses various +aspects for creating a coroutine task suitable for many needs: + +- The basic operation of a sender/receiver aware task. +- Scheduler affinity and why it is important. +- Allocator support for the coroutine frame. +- Providing an environment to child operations. +- Propagating errors without exceptions. + +## Outline + +The presentation starts off with motivating why coroutines are a +reasonable approach to implementing asynchronous algorithms and +show some basic usage. That will include some motivation of why an +environment is important as a replacement for thread local variables +and why executing from common scheduler is normally desirable. + +The most contentious aspect is probably scheduler affinity and some +time is spent on showing how it is implement, motivating why it is +important, and why it should be the default. There are some use +cases where scheduler affinity may be undesirable and it is also +shown how it can be avoided. + +The coroutine frame typically can't live on the stack and needs to +be allocated while the HALO operations are not always applicable. It +is shown how allocation of the coroutine frame can be customized if +it is desired using an allocator. + +Any use allocation would likely be part of an environment exposed to +child operations. It should also be possible to customize the available +environment with user-specified properties. It is showing how to get +access to environment properties and how they can be customized. + +Error handling and cancellation are important aspects which are directly +supported by the sender/receiver framework. There are a few ways how a +coroutine can interact with error handling and cancellation and the +different options are discussed. diff --git a/subprojects/task/docs/compiler-explorer.ico b/subprojects/task/docs/compiler-explorer.ico new file mode 100644 index 0000000000000000000000000000000000000000..0b0380f39316005f52c036da8b347c5c27b049c1 GIT binary patch literal 15406 zcmeHOYiu0V6`q!qM?+}>1yUZtrEYd-XXB)}X=?Ao>ktemLM>2>@`wl!&y1b8Kt(}n zm4+gqK&T1=(Mmi8LakCrNvNZQSA;V&b|8ZA$d8n!w0TgP5Yo^_Nl6{^eX}!@>+$UF z%z78w(vEaIvorUebMCq4-g{n_q%TNENi%0kxYtSgG)eldBuRC3`QP!$lJpqfO`7C< zpCC!rY)Oiv4Z27R0PJU9_4!-vZ%qO<2~``Yl<|8r8vf~ zY3tOl$(Hu4Vrn}QrnZg1XW7(ErB_RgarUL8skS97?Z4oA&<~2CFCOdsE2et8pYJ2T z)o1EgcONraV`$u{ekoz;-;NobF=_DDFM51HuNCXM6BCx&o-ozFAK`b{9PGvSALrMC z&>x7BuXdfOm>sh~1dgB|< z0j!6&(e_U8J2Pf1J`FO_(B@6R9=>l_JAYy&SHzc=$h_xjwf$&%^ZH zZCl`e81E^IEr9a?S2ScJ%Cc+adc{kTriKjl7{x3k_bXJ zPz-fyUayD9e-Gq$`ABUVrB6>mE`RmOW^df;XbH()$!7LCG_?VN#sKEymy~-hmz&U< zO2t~!=^0TVo6Sx~-{VP~a)5kz?tKE-$2)9|8%r`4qCw2K_A9{mGQgW-(Azl8(obi1 z@Gzw%o$hVPWIn=uR|NP6#*npf;C!KNhk0&IuGez}o@*i4E7?T0)E_`c|1I!8fOj{z z9p^MPoerKi0|q0){+3kgJQq(T???7I{eJMxGuD$mp0|s3Fyuc8vcCLKZ4IgKECZSU@l%b!@BXfO3JH}xNLjM)ZPu^-F|0jzZLhKwzjs3 z*>w5}%t3c9m0Da3*rqj;`99_hQIhIhCHXhir6T22Ds>uk;axyj{eQ8sv9UTfG8ik9 z|4rb30PtxE7{FYu8CCv$`qR|ZbR71P3v-#wk4gc?b~)B}uD!jy^IDwuLeFKcU;1L9 zfV?Y;ax}r|Q;3za-*f-8eL+io0Bd%;Z!K@5&*dVA{7-~T?}Kdr1XvLPevkdL!NpUp z`*=LRL|Hic^olqKqWy60?AfPcO}+%V?*MFz09!C`Y0{#9S}ps5_`!QOAkcnmmuwqu z3QULql8~-@BpU1oZ0^KR|KLd6a9qZ(xc^i!e)u8wPR3BG7eC|xc)9anX#WJgt?;*(y7621G`A#7{WZMb z4-oY!=$Ub=cNpJ^ukUP(y}IxlvZ;3B-6p`tj2l)g_L=A0`^&hR+^3!s6hF7~E{Fv= zafBeg`3^c*IDRf0o!_e#@7^FvJWzh%GmwA8+*gcd{-Oly_UB^xBjm-d4})Vg+fa`v zXRL`K`8)i!5=MtIn!JUj&0G?5rsFYx8g`7eb~OpqBv6yUc$Wa{%<2F+fLCXRTD00# zAqlW%i1|T+SaVS!^u^Gey;1$DW6vRHv;&-3zyTb?bNr#1Y zgFgO{pV#k*A_v)AbQX9=y@W?wq`SwlV}F8N_9cwNqFp!i@oCgl&<7OTI(+K8Sf3PD zFX)Q^&*LI4XG}fJtq1ukw#?we*?bK3d&$F^nd=rr+Wjz(mkM$mfPOy`->~SUQe$X% z>!yjUH3Ch~_ZIW2rTV+yht!){`s(`LYfiRf!r82hcxpkI+nAnOzxv7(5{CLa@bEEU zANg_dhPg9`jehR{D7(Gre=hrFGnwBY4!sw!y8!Ups*8{ZV|<%+Caia$K82yn`b^0A z0*w7h(9P#bF?M(TARc}_a)oO_V|4*|0X!~qs8IVUn*p~Fb!k2#oB?%b0N>K}aZ3n72(-Cmx)$K5-jdpBeJCZIPEG8qeV z^dNJW!muJg7FmDGK=(ty&H})1&ys#Q>rZ5qXNIST&i<30o}P@<($r$m*3f)PY$a-A zMzH^ok8<~4Wpmf@-0#4p2Hpzy5 zTacec9|*Pqb9&tKwk0-n{w(&oo{DeKniBomO~A9&{)W2hpgnmp_2mEga$&dYq&0k-%`Sa@! zXzd1g`UBq9;&(y)ArGSdI`hf1>;URL=5u_k`6#5n>@Qy6$6G&MoJediMxH+dsaq(^ literal 0 HcmV?d00001 diff --git a/subprojects/task/docs/contributing.md b/subprojects/task/docs/contributing.md new file mode 100644 index 000000000..0c617363c --- /dev/null +++ b/subprojects/task/docs/contributing.md @@ -0,0 +1,36 @@ + + +# Contributing to `beman::task` + +We welcome all constructive contributions! You will have something you +can contribute independent of your level of expertise! + +There is a huge range of contributions needed and welcome! Here is an +incomplete list of starting points for contributions: + +* Some documentation is unclear. +* There is some bug somewhere. That can be a typo in some documentation, + a logic error in the implementation, a deviation from the standard + specification, etc. +* Something reference moved or the reference is out of date. +* When compiling the code on your system warnings or even errors are + created. +* The [issues page](https://github.com/bemanproject/task/issues) lists + known issues. +* The implementation of a component may be missing. +* The layout of some pages related to the project can be improved. +* Some behavior of a component isn't tested or documented. +* You found something which should be linked from the + [resources](https://github.com/bemanproject/task/blob/main/docs/resources.md) page. +* There are [pull requests](https://github.com/bemanproject/task/pulls) + which could be reviewed. +* This very page or list is lacking something. + +Whatever it is, you can improve the project! Ideally, create a pull +request with your proposed change. If you think you can't do that +for whatever reason, create an issue (if possible checking if the +issue isn't reported, yet) explaining what should be done. You can +possibly use a pull request or an issue to ask for help with something +you want to contribute. You can also send a message to one of the +contributors with your suggestion, e.g. to +[Dietmar Kühl](mailto:dietmar.kuehl@me.com). diff --git a/subprojects/task/docs/examples.md b/subprojects/task/docs/examples.md new file mode 100644 index 000000000..c3c1f9080 --- /dev/null +++ b/subprojects/task/docs/examples.md @@ -0,0 +1,246 @@ +# Examples + +## Code used to prepare Dietmar's C++Now 2025 presentation + +
+ +c++now-affinity.cpp +: +demo scheduler affinity + + +The example program +[`c++now-affinity.cpp`](https://github.com/bemanproject/task/blob/main/examples/c%2B%2Bnow-affinity.cpp) +uses [`demo::thread_loop`](../examples/demo-thread_loop.hpp) to demonstrate +the behavior of _scheduler affinity_: the idea is that scheduler +affinity causes the coroutine to resume on the same scheduler as +the one the coroutine was started on. The program implements three +coroutines which have most of their behavior in common: + +1. Each coroutine is executed from `main` using sync_wait(_fun_(loop.get_scheduler())). +2. Each coroutine prints the id of the thread it is executing on prior to any `co_await` and after all `co_await` expression. +3. Each coroutine `co_await`s the result of `scheduler(sched) | then([]{ ... })` where the function passed to `then` just prints the thread id. +4. `work2` additionally changes the coroutines scheduler to be an `inline_scheduler` and later restores the original scehduler using `change_coroutine_scheduler`. +5. While `work1` and `work2` use the default scheduler (`task_scheduler` a +type-erased scheduler which gets initialized from the receiver's environment's `get_scheduler`), `work3` sets the coroutine's scheduler up to be `inline_scheduler`, effectively causing the coroutine to resume wherever +the `co_await`'s expression resumed. + +The output of the program is something like the below: + +``` +before id=0x1fd635f00 +then id =0x16b64b000 +after id =0x1fd635f00 + +before id=0x1fd635f00 +then id =0x16b64b000 +after1 id=0x16b64b000 +then id =0x16b64b000 +after2 id=0x1fd635f00 + +before id=0x1fd635f00 +then id =0x16b64b000 +after id =0x16b64b000 +``` + +It shows that: + +1. The thread on which the `then`'s function is executed is always the +same and different from the thread each of the coroutines started on. +2. For `work1` the `co_await` resumes on the same thread as the one the +coroutine was started on. +3. For `work2` the first `co_await` after `schedule(sched)` resumes on +the thread used by `sched`. After restoring the original scheduler the +`co_await` resumes on the original thread. +4. For `work3` the `co_await` resumes on the thread used by `sched` as +the `inline_scheduler` doesn't do any actual scheduling. + +
+ +
+ +c++now-allocator.cpp +: +demo allocator use + + +This demo shows how to configure `task`'s environment argument to +use a different allocator than the default `std::allocator`. +To do so it defines an environment type `with_allocator` which +defines a nested type alias `allocator_type` to be +`std::pmr::polymorphic_allocator`. + +The coroutine `coro` shows how to use `read_env` to extract the +used allocator object to potentially use it for any allocation +purposes within the coroutine. There are two uses of `coro`, the +first one using the default which just uses +`std::pmr::polymorphic_allocator()` to allocate memory. +The second use explicitly specifies the memory resource +`std::pmr::new_delete_resource()` to initialized the use +`std::pmr::polymorphic_allocator`. + +
+ +
+ +c++now-basic.cpp +: +demo basic task use + + +The example +c++now-basic.cpp +shows some basic use of a `task`: + +1. The coroutine `basic` just `co_await`s the awaiter `std::suspend_never{}` which immediately completes. + This use demonstrates that any awaiter can be `co_await`ed by a `task<...>`. +2. The coroutine `await_sender` demonstrates the results of `co_await`ing various senders. It uses variations of + `just*` to show the different results: + - `co_await`ing a sender completing with `set_value_t()`, e.g., `just()`, produces an expression with type `void`. + - `co_await`ing a sender completing with `set_value_t(T)`, e.g., `just(1)`, produces an expression with type `T`. + - `co_await`ing a sender completing with set_value_t(T0, ..., Tn), e.g., `just(1, true)`, produces an expression with type tuple<T0, ..., Tn>. + - `co_await`ing a sender completing with `set_error_t(E)`, e.g., `just_error(1)`, results in an exception of type `E` being thrown. + - `co_await`ing a sender completing with `set_stopped_t()`, e.g., `just_stopped()`, results in the corouting never getting resumed although all local objects are properly destroyed. +
+ +
+ +c++now-cancel.cpp +: +demo how a task can actively cancel the work + + +The example +c++now-cancel.cpp +shows a coroutine `co_await`ing `just_stopped()` which results in the coroutine getting cancelled. The coroutine will +complete with `set_stopped()`. +
+ +
+ +c++now-errors.cpp +: +demo how to handle errors within task + + +The example +c++now-errors.cpp +shows examples of how to handle errors within a coroutine: + +- The coroutine `error_result` simply `co_await`s a sender producing an error (`just_error(17)`). When + a `co_await`ed sender completes with `set_error_t(T)` an exception of type `T` is thrown and the error + needs to be handled with a `try`/`catch` block. Otherwise the coroutine itself completes with `set_error_t(exception_ptr)` + where the `exception_ptr` hold the thrown exception object. +- The coroutine `expected` uses a sender algorithm `as_expected` which is implemented at the top of the example + to turn the result of the `co_await`ed sender into an object of type `expected`, avoiding an exception + from being thrown. + +
+ +
+ +c++now-query.cpp +: +demo passing a custom environment into a task + + +The example +c++now-query.cpp +shows how to define and use a custom environment element. + +1. The coroutine `with_env` uses a simple environment named `context` which just defines a custom query for `get_value` + to obtain a value. The value itself gets initialized from the environment of the receiver used with the `task`. +2. The coroutine `with_fancy_env` uses an environment which embed a an object depending the type of the environment + of the receiver used with the `task`. While the type accessed from within the `task` needs to be type-erased, + the actually stored value can depend on the environment of the upstream receiver. +
+ +
+ +c++now-result-types.cpp +: +demo the result type of co_await expressions. + + +The example +c++now-result-types.cpp +shows the result types of successful senders using variatons of `just`: + +- `co_await just()` doesn't produce a value, i.e., the type of the expression is `void`. +- `co_await just(1)` produces an `int`. +- `co_await just(1, true)` produces a `tuple`. + +
+ +
+ +c++now-return.cpp +: +shows the different normal values returned + + +The example +c++now-return.cpp +shows various ways of returning normally (without an error) for a `task`. Some of the coroutines are set up to produce +specific error results although none of them are actually use: + +- `default_return` shows that the default return type for `task<>` is `void`. +- `void_return` explicitly specifies a `void` return type. +- `int_return` specifies the return type as `int` and returns an `int` value. +- `error_return` specifies the return type as `int` and also specifies custom error results. +- `no_error_return` specifies the return type as `int` and also specifies that the coroutine can't produce any error. +
+ +
+ +c++now-stop_token.cpp +: +demo how to get and use a stop token in a `task` + + +The example +c++now-stop_token.cpp +shows how to get a stop token in side a `task` and how to use it to cancel active work. It doesn't actually complete +with a `set_stopped()` but completes with `set_value()`. + +- In the coroutine `co_await read_env(get_stop_token)` is used to get a stop token. +- In the loop the value of `token.stop_requested()` is checked to determine if the loop should continue. +- In `main` an `inplace_stop_source` is used to have something which can be stopped. +- When running the coroutine `stopping` on a separate thread, the environment is changed using `write_env` to use stop token from `main`'s stop source in the environment. +- After sleeping for a bit, `source.request_stop()` is called to trigger cancellation of the coroutine. +
+ +
+ +c++now-with_error.cpp +: +demo exiting a task with an error + + +The example +c++now-with_error.cpp +shows how a coroutine can be exited reporting an error without throwing an exception. To do so, the coroutine +uses `co_yield with_error(e)`. By default the `task` only declares `set_error_t(exception_ptr)`. To return +other errors, an environment declaring a suitable `set_error_t(E)` completion using the `error_types` alias is used. +
+ +## Tools Used By The Examples + +
+ +demo::thread_loop is a run_loop whose run() is called from a std::thread. + + +Technically [`demo::thread_loop`](../examples/demo-thread_loop.hpp) +is a class `public`ly derived from `execution::run_loop` which is +also owning a `std::thread`. The `std::thread` is constructed with +a function object calling `run()` on the +[`demo::thread_loop`](../examples/demo-thread_loop.hpp) object. +Destroying the object calls `finish()` and then `join()`s the +`std::thread`: the destructor will block until the `execution::run_loop`'s +`run()` returns. + +The important bit is that work executed on the +[`demo::thread_loop`](../examples/demo-thread_loop.hpp)'s `scheduler` +will be executed on a corresponding `std::thread`. +
diff --git a/subprojects/task/docs/issues-overview.md b/subprojects/task/docs/issues-overview.md new file mode 100644 index 000000000..f154adfd5 --- /dev/null +++ b/subprojects/task/docs/issues-overview.md @@ -0,0 +1,274 @@ +# Task Issues Overview + +A brief overview of the issues based on two lists (there is some +overlap between the two lists; the initials are used to reference +the respective issue): + +- Lewis's list [LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org) +- Dietmar's list [DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md) (build nearly complete from Mattermost feedback) + +## Task is eager + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-is-not-actually-lazily-started) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#task-is-not-actually-lazily-started) + +- the current spec implies eager start +- the intent is to establish task's invariant (scheduler affinity) +- fix: `initial_suspend() -> suspend_always` and constraint start +- comment: rephrase to clarify it is resumed later + +=> say it always suspend (steal wording from suspend_always) but don't nail down suspend_always + change op_state.start(): calls handle.resume() on an execution agent associated with SCHED(prom) + +- we may want to require that the awaiter operations are not potentially throwing/noexcept + +## Initial resume shouldn't reschedule + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-should-not-unconditionally-reschedule-when-control-enters-the-coroutine) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#task-should-not-unconditionally-reschedule-when-control-enters-the-coroutine) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#starting-a-task-reschedules) + +- `initial_suspend()`'s spec implies reschedule +- fix: constrain `start(rcvr)` to be called on `get_scheduler(get_env(rcvr))` instead +- comment: the description from `initial_suspend()` merely lays out what needs to hold => that's OK + +=> Change op_state.start() to handle.resume() on the correct execution agent. + +- this change isn't really task but could be a different paper +- we are not saying anything about schedule - we just say where we are executing +- maybe add a recommended practice about task constructed from task +- co_await on a task should never call schedule + +## Shouldn't reschedule after `co_await`ing task + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-awaiting-another-task-should-not-reschedule-on-resumption) + +- `await_transform` uses `affine_on` without enough context information => reschedule +- fix: specify a custom `affine_on` for `task` and arrange for various things +- comment: how can a user tell what exactly happened? the `co_await`ing `task` and the `co_await`ed task are from the implementation + +=> define affine_on such that it start on the receiver's get_scheduler +- LB: I'm imagining +- it turns out that task doesn't have any default parameters! +- inline_scheduler should have a customization of affine_on and we drop the special case +- affine_on on the task could be specialized via the task's domain: it just returns the task + +- get_scheduler is where the operation is started +- affine_on should just take one argument and use get_scheduler but that requires that start is called on get_scheduler's scheduler + +[unrelated: +- get_scheduler: where we got started +- get_delegation_scheduler: where to start new work +- get_completion_scheduler: where things complete +] + +## No symmetric transfer + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-coroutine-awaiting-another-task-does-not-use-symmetric-transfer) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#no-support-for-symmetric-transfer) + +- `co_await tsk;` doesn't use symmetric transfer +- fix: require symmetric transfer +- comment: recommended practice? the `task`s can communicate appropriately + +## no support for non-senders + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#minor-task-does-not-accept-awaiting-types-that-provide-as_awaitable-but-that-do-not-satisfy-sender-concept) + +- `as_awaitable` isn't used +- fix: use `as_awaitable` +- comment: how to guarantee affinity? seems to require an additional coroutine +- don't do? + +## `affine_on` has no default spec + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-is-missing-a-specification-for-a-default-implementation) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#affine_on-underspecified) + +- there is no default specification of what `affine_on` does. +- fix: say something specific +- comment: we don't have the tools to say what implementations can do for known senders + +=> get a continues_on p5-like paragraph which calls continues_on + +## `affine_on` semantics unclear + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-semantics-are-not-clear) + +- the wording is unclear +- fix: two versions +- comment: I think I had the 2nd in mind + +=> allow affine_on to complete inline regardless that's the difference to continues_on + in addition it can be customized separately + +## `affine_on` shape + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-might-not-have-the-right-shape) + +- `affine_on` should just depend on the receiver's `get_scheduler()`. +- comment: agreed; that is design, though + +=> starts on get_scheduler, complete on the passed scheduler (which may get dropped) + +## `affine_on` schedule vs. stop token + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-should-probably-not-forward-stop-requests-to-reschedule-operation) + +- non-success completions don't guarantee affinity invariant +- fix: suppress stop token +- comment: in case of `set_stopped()` the coroutine doesn't resume; for `set_error(e)` other reasons may exist +- comment: however, I don't object to this direction + +=> this requirement needs to align with what continues_on does +- the actual conclusion is: affine_on may need to do what continues_on does +- unless continues_on's schedule operation should also not be cancelable +- alternatively we could call continues_on with an adapted scheduler + +## `affine_on` vs. other algorithms + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#we-should-probably-define-customsiations-for-affine_on-for-some-other-senders) + +- `affine_on(sch, sndr)` should behave special for some `sndr` +- fix: specify some of the interactions +- comment: I think an implementation can do these; once we have better tools we may want to require some + +## promise_type vs with_awaitable_sender + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#minor-taskpromise_type-doesnt-use-with_awaitable_senders---should-it) + +- should it use `with_awaitable_sender`? +- potential fix: have affinity aware `with_awaitable_sender` variant? +- comment: there are multiple tools which may be useful + +## unhandled_stopped should be noexcept + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_typeunhandled_stopped-should-be-marked-noexcept) + +- fix: do it! +- comment: for consistency it should be `noexcept` + +## allocator type not specified + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#behaviour-when-the-tasks-environment-type-does-not-specify-an-allocator_type) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#unusual-allocator-customisation) + +- generator uses the allocator anyway +- fix: use type-erased allocator in that case like generator +- comment: that's an oversight; I didn't realize generator does that + +=> write up the pros and cons +- we could construct environment with the allocator so the user can choose which allocator to use +- use the coroutine_traits to determine the promise_type based on the allocator? + +## allocator_arg more permissive + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#handling-of-allocator_arg-is-more-permissive-than-for-stdgenerator) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#issue-flexible-allocator-position) + +- generator requires allocator in first position +- fix: should generator be generalized? +- comment: yes but not for C++26 + +=> coroutines can forward - write design discussion + +## hiding parent's allocator + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#minor-task-environments-allocator_type-overrides-the-parent-environments-get_allocator) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#shadowing-the-environment-allocator-is-questionable) + +- task's allocator (from ctor) hides receiver's allocator +- fix: allow receiver's allocator through +- comment: that misaligns with my understanding of the allocator model + +## no stop_source in promise + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_type-should-not-contain-a-stop-source) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#a-stop-source-always-needs-to-be-created) + +- the stop_source is only need for misaligned stop tokens +- fix: have the stop source in promise and only when needed +- comment: I think that is already allowed; maybe should be clarified, recommended practice? + +## `stop_token` default constructible + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_type-wording-assumes-that-stop-token-is-default-constructible) + +- the exposition-only member for `stop_token` require default constructible +- fix: don't have it: get it from env or stop source +- comment: I think that is already allowed + +## coro frame destroyed too late + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-coroutine-state-is-not-destroyed-early-enough-after-completing) + +- frame should be destroyed before completing +- fix: result in state, destroy before completing +- comment: that is what I had hoped to say; may need wording fix + +## inefficient env + +[LB](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_typeget_env-seems-to-require-an-inefficient-implementation) +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#the-environment-design-is-odd) + +- `own_env` must copy lots of state +- comment: `own_env` just allows a potential type-erase entity to be workable, primarily it is env + +## no completion scheduler + +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#no-completion-scheduler) + +- `task` doesn't expose a completion scheduler +- comment: it can't have one +- fix: redesign get_completion_scheduler(sender, env) or get_completion_scheduler(os) + +## future coro may not need `co_yield error` + +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#a-future-coroutine-feature-could-avoid-co_yield-for-errors) + +- sure + +## no hook to capture/restore TLS + +[DK](https://github.com/bemanproject/task/blob/issues/docs/issues.md#there-is-no-hook-to-capturerestore-tls) + +- legacy APIs may need TLS to be set appropriately +- objective: capture TLS before async, restore after +- fix: use custom `affine_on` with wrapped scheduler, delegating + +# Outline + +## Major + +- [x] `affine_on` - require more detailed specification - Dietmar will have a paper - get_scheduler semantics (Telecon during August, vote during Kona) - NB comment + - [x] [`affine_on` is missing a specification for a default implementation](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-is-missing-a-specification-for-a-default-implementation) + - [x] [`affine_on` semantics are not clear](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-semantics-are-not-clear) + - [x] [`affine_on` might not have the right shape](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-might-not-have-the-right-shape) + - [x] [`affine_on` should probably not forward stop-requests to reschedule operation](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#affine_on-should-probably-not-forward-stop-requests-to-reschedule-operation) + - [x] [`affine_on` customization for other senders](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#we-should-probably-define-customsiations-for-affine_on-for-some-other-senders) +- [x] [`task` is not actually lazily started](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-is-not-actually-lazily-started) - wording fix (should suspend always) (Dietmar’s paper) +- [x] [`task` coroutine reschedules too often](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-coroutine-reschedules-too-often): - apply “known design” to optimize / fix (Dietmar’s paper) + - [x] [`task` should not unconditionally reschedule when control enters the coroutine](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-should-not-unconditionally-reschedule-when-control-enters-the-coroutine) + - [x] [`task` awaiting another task should not reschedule on resumption](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-awaiting-another-task-should-not-reschedule-on-resumption) + - [x] [`task` coroutine awaiting another task does not use symmetric-transfer](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-coroutine-awaiting-another-task-does-not-use-symmetric-transfer) - (Dietmar’s paper) +- [x] [`task` allocation strategy](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-allocator-customisation-behaviour-is-inconsistent-with-generator) + - [x] [`task` allocator customization is more permissive](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#handling-of-allocator_arg-is-more-permissive-than-for-stdgenerator) + - [x] [`task` environment’s `allocator_type` hides the parent environment’s `get_allocator`](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#minor-task-environments-allocator_type-overrides-the-parent-environments-get_allocator) + - [x] [for `generator` an `allocator_arg, allocator` can always be used](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#behaviour-when-the-tasks-environment-type-does-not-specify-an-allocator_type) + +## Medium + +- [x] [`task::promise_type` should not contain a stop-source](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_type-should-not-contain-a-stop-source) - (Dietmar’s paper) +- [x] [`task::promise_type` wording assumes that stop-token is default constructible](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_type-wording-assumes-that-stop-token-is-default-constructible) - (Dietmar’s paper) +- [x] [`task` coroutine-state is not destroyed early enough after completing](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#task-coroutine-state-is-not-destroyed-early-enough-after-completing) (optimization?) - (Dietmar’s paper) +- [x] [`task::promise_type::get_env` seems to require an inefficient implementation](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_typeget_env-seems-to-require-an-inefficient-implementation) (optimization?) - (Dietmar’s paper) + +## Minor issues + +- [x] [`task` does not accept awaiting types that provide `as_awaitable`](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#minor-task-does-not-accept-awaiting-types-that-provide-as_awaitable-but-that-do-not-satisfy-sender-concept) but that do not satisfy sender concept +- [x] [`task::promise_type` doesn’t use `with_awaitable_senders` - should it?](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#minor-taskpromise_type-doesnt-use-with_awaitable_senders---should-it) +- [x] `task` Has No Default Arguments +- [x] [`task::promise_type::unhandled_stopped` should be marked `noexcept`](https://github.com/lewissbaker/papers/blob/master/isocpp/task-issues.org#taskpromise_typeunhandled_stopped-should-be-marked-noexcept) +- [x] No Completion Scheduler +- [x] Awaitable non-`sender`s Are Not Supported +- [x] Future Coroutine Features Could avoid co_yield with_error. +- [x] No hook to capture/restore TLS diff --git a/subprojects/task/docs/resources.md b/subprojects/task/docs/resources.md new file mode 100644 index 000000000..181ac50f2 --- /dev/null +++ b/subprojects/task/docs/resources.md @@ -0,0 +1,12 @@ + + +# Resources Related to `beman::task` + +This page has links to some resources related to this repository: + +* Lewis Baker's [Asymmetric Transfer blog](https://lewissbaker.github.io/) +* Dietmar Kühl's + [co_await All The Things](https://youtu.be/Npiw4cYElng?si=J0EYX6IgBf_u-e6z) + from [ACCU 2023](https://accu.org/conf-previous/2023/sessions/) implementing + a simple task live +* The C++ standard proposal [Add a Coroutine Lazy Type](https://wg21.link/P3552). diff --git a/subprojects/task/docs/task-issues.md b/subprojects/task/docs/task-issues.md new file mode 100644 index 000000000..9da8013e7 --- /dev/null +++ b/subprojects/task/docs/task-issues.md @@ -0,0 +1,28 @@ +# C++ task related issues + +NB Comment | LWG Issue | State | Section | Title +-----------|-----------|-------|---------|------ +[US 232-366](https://github.com/cplusplus/nbballot/issues/941) | [LWG4329](https://cplusplus.github.io/LWG/issue4329) [P3941](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3941r1.html) | open/SG1 | [33.13.3](https://wg21.link/exec.affine.on) | Specify customizations for `affine_on` when sender does not change scheduler +[US 233-365](https://github.com/cplusplus/nbballot/issues/940) | [LWG4330](https://cplusplus.github.io/LWG/issue4330) [P3941](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3941r1.html) | open/SG1 | [33.13.3](https://wg21.link/exec.affine.on) | Clarify `affine_on` vs. `continues_on` +[US 234-364](https://github.com/cplusplus/nbballot/issues/939) | [LWG4331](https://cplusplus.github.io/LWG/issue4331) [P3941](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3941r1.html) | open/LEWG | [33.13.3](https://wg21.link/exec.affine.on) | Remove scheduler parameter from `affine_on` +[US 235-363](https://github.com/cplusplus/nbballot/issues/938) | [LWG4332](https://cplusplus.github.io/LWG/issue4332) [P3941](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3941r1.html) | open/SG1 | [33.13.3](https://wg21.link/exec.affine.on) | `affine_on` should not forward the stop token to the scheduling operation +[US 236-362](https://github.com/cplusplus/nbballot/issues/937) | [LWG4344](https://cplusplus.github.io/LWG/issue4344) [P3941](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3941r1.html) | open/SG1 | [33.13.3](https://wg21.link/exec.affine.on) | Specify default implementation of `affine_on` +[US 237-369](https://github.com/cplusplus/nbballot/issues/944) | [LWG4342](https://cplusplus.github.io/LWG/issue4342) | closed | [33.13.5](https://wg21.link/exec.task.scheduler) | Add rvalue reference qualification to task_scheduler::ts-sender::connect +[US 238-368](https://github.com/cplusplus/nbballot/issues/943) | [LWG4336](https://cplusplus.github.io/LWG/issue4336) [P3927](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3927r0.html) | open/LEWG | [33.13.5](https://wg21.link/exec.task.scheduler) | Allow customizations when using `task_scheduler` +[US 239-367](https://github.com/cplusplus/nbballot/issues/942) | | closed | [33.13.5](https://wg21.link/exec.task.scheduler) | sch_ must not be in moved-from state +[US 242-372](https://github.com/cplusplus/nbballot/issues/947) | [LWG4339](https://cplusplus.github.io/LWG/issue4339) | open/LWG | [33.13.6](https://wg21.link/exec.task) | Timing of destruction of coroutine frame +[US 243-376](https://github.com/cplusplus/nbballot/issues/951) | | closed | [33.13.6.2](https://wg21.link/task.class) | Add default template arguments for `task` +[US 244-375](https://github.com/cplusplus/nbballot/issues/950) | [LWG4341](https://cplusplus.github.io/LWG/issue4341) | closed | [33.13.6.2](https://wg21.link/task.class) | Add rvalue reference qualifier for `task::connect()` +[US 245-374](https://github.com/cplusplus/nbballot/issues/949) | [LWG4338](https://cplusplus.github.io/LWG/issue4338) | rejected | [33.13.6.2](https://wg21.link/task.class) | Add `operator co_await` +[US 246-373](https://github.com/cplusplus/nbballot/issues/948) | [LWG4348](https://cplusplus.github.io/LWG/issue4348) | ⚠️open/SG1 | [33.13.6.2](https://wg21.link/task.class) | Support symmetric transfer +[US 249-379](https://github.com/cplusplus/nbballot/issues/954) | | ⚠️open/LWG | [33.13.6.4p4](https://wg21.link/task.state#4) | Move specification for `stop_token_type` +[US 250-389](https://github.com/cplusplus/nbballot/issues/964) | [LWG4346](https://cplusplus.github.io/LWG/issue4346) | closed | [33.13.6.5](https://wg21.link/task.promise) | Add specification for `return_void` and `return_value` +[US 251-388](https://github.com/cplusplus/nbballot/issues/963) | [LWG4345](https://cplusplus.github.io/LWG/issue4345) | closed | [33.13.6.5](https://wg21.link/task.promise) | Add default template argument for `return_value` +[US 252-387](https://github.com/cplusplus/nbballot/issues/962) | [LWG4340](https://cplusplus.github.io/LWG/issue4340) | closed | [33.13.6.5](https://wg21.link/task.promise) | `task::promise_type::unhandled_stopped()` should be `noexcept` +[US 253-386](https://github.com/cplusplus/nbballot/issues/961) | [LWG4333](https://cplusplus.github.io/LWG/issue4333) [D3980](https://isocpp.org/files/papers/D3980R0.html) | ✓open/LEWG | [33.13.6.5](https://wg21.link/task.promise) | Allow use of arbitrary allocators for coroutine frame +[US 254-385](https://github.com/cplusplus/nbballot/issues/960) | [LWG4334](https://cplusplus.github.io/LWG/issue4334) [D3980](https://isocpp.org/files/papers/D3980R0.html) | ✓open/LEWG | [33.13.6.5](https://wg21.link/task.promise) | Constraint `allocator_arg` argument to be the first argument +[US 255-384](https://github.com/cplusplus/nbballot/issues/959) | [LWG4335](https://cplusplus.github.io/LWG/issue4335) [D3980](https://isocpp.org/files/papers/D3980R0.html) | ✓open/LEWG | [33.13.6.5](https://wg21.link/task.promise) | Use allocator from receiver's environment +[US 256-383](https://github.com/cplusplus/nbballot/issues/958) | [LWG4337](https://cplusplus.github.io/LWG/issue4337) | rejected | [33.13.6.5](https://wg21.link/task.promise) | Scheduler need not be assignable +[US 257-382](https://github.com/cplusplus/nbballot/issues/957) | [LWG4347](https://cplusplus.github.io/LWG/issue4347) | ⚠️open/LWG | [33.13.6.5](https://wg21.link/task.promise) | Respecify source and token members +[US 258-381](https://github.com/cplusplus/nbballot/issues/956) | [LWG4349](https://cplusplus.github.io/LWG/issue4349) | closed | [33.13.6.5](https://wg21.link/task.promise) | Clarify `task` is not eagerly started +[US 261-391](https://github.com/cplusplus/nbballot/issues/966) | [D3980](https://isocpp.org/files/papers/D3980R0.html) | ✓open/LEWG | [33.13.6.5](https://wg21.link/task.promise) [p3](https://wg21.link/task.promise#3), [p16](https://wg21.link/task.promise#16), [p17](https://wg21.link/task.promise#17) | Bad specification of parameter type diff --git a/subprojects/task/examples/CMakeLists.txt b/subprojects/task/examples/CMakeLists.txt new file mode 100644 index 000000000..8dda33eef --- /dev/null +++ b/subprojects/task/examples/CMakeLists.txt @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.30...4.3) + +# include(../cmake/prelude.cmake) + +project(beman_task.example LANGUAGES CXX) + +# TODO(CK): cmake -S . -G Ninja -B build -D CMAKE_BUILD_TYPE=RelWithDebInfo --fresh +if(PROJECT_IS_TOP_LEVEL) + # include(../cmake/cxx-modules-rules.cmake) + set(BEMAN_USE_MODULES OFF) + set(BEMAN_USE_STD_MODULE OFF) + set(CMAKE_CXX_STANDARD 23) + + find_package(beman.task 0.2.0 EXACT REQUIRED) + + enable_testing() +endif() + +set(TODO tls-scheduler into_optional issue-start-reschedules loop) + +set(ALL_EXAMPLES + task_scheduler + rvalue-task + customize + issue-symmetric-transfer + container +) + +if(NOT MSVC) + list( + APPEND ALL_EXAMPLES + task-sender + alloc-1 + alloc-2 + c++now-allocator + c++now-cancel + c++now-errors + alloc + affinity + odd-return + dangling-references + aggregate-return + issue-affine_on + c++now-affinity + c++now-basic + # FIXME: c++now-query + c++now-result-types + c++now-return + # FIXME: c++now-stop_token + c++now-with_error + co_await-result + co_await-task + error + escaped-exception + friendly + hello + issue-frame-allocator + # FIXME: query + result_example + # FIXME: stop + ) +endif() + +set(xALL_EXAMPLES issue-symmetric-transfer) +set(xALL_EXAMPLES customize) + +message(STATUS "Examples to be built: ${ALL_EXAMPLES}") + +foreach(example ${ALL_EXAMPLES}) + add_executable(beman.task.examples.${example}) + target_sources(beman.task.examples.${example} PRIVATE ${example}.cpp) + target_link_libraries(beman.task.examples.${example} beman::task_headers) + add_test( + NAME beman.task.examples.${example} + COMMAND $ + ) +endforeach() diff --git a/subprojects/task/examples/affinity.cpp b/subprojects/task/examples/affinity.cpp new file mode 100644 index 000000000..9f5605865 --- /dev/null +++ b/subprojects/task/examples/affinity.cpp @@ -0,0 +1,63 @@ +// examples/affinity.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include "demo-thread_loop.hpp" +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +struct test_receiver { + using receiver_concept = ex::receiver_tag; + + auto set_value(auto&&...) && noexcept {} + auto set_error(auto&&) && noexcept {} + auto set_stopped() && noexcept {} +}; +static_assert(ex::receiver); + +std::ostream& fmt_id(std::ostream& out) { return out << std::this_thread::get_id(); } + +struct non_affine { + using scheduler_type = ex::inline_scheduler; +}; +} // namespace + +int main() { + std::cout << std::unitbuf; + demo::thread_loop loop; + ex::sync_wait(ex::just() | ex::then([]() noexcept { std::cout << "main:" << fmt_id << "\n"; })); + ex::sync_wait(ex::schedule(loop.get_scheduler()) | + ex::then([]() noexcept { std::cout << "loop:" << fmt_id << "\n"; })); + ex::sync_wait(ex::schedule(ex::task_scheduler(loop.get_scheduler())) | + ex::then([]() noexcept { std::cout << "any: " << fmt_id << "\n"; })); + ex::sync_wait([]() -> ex::task { + std::cout << "coro:" << fmt_id << "\n"; + co_return; + }()); + std::cout << "scheduler affine:\n"; + ex::sync_wait([](auto& pl) -> ex::task { + std::cout << "cor1:" << fmt_id << "\n"; + co_await (ex::schedule(pl.get_scheduler()) | ex::then([] { std::cout << "then:" << fmt_id << "\n"; })); + std::cout << "cor2:" << fmt_id << "\n"; + }(loop)); + + std::cout << "not scheduler affine:\n"; + ex::sync_wait([](auto& pl) -> ex::task { + std::cout << "cor1:" << fmt_id << "\n"; + co_await (ex::schedule(pl.get_scheduler()) | ex::then([] { std::cout << "then:" << fmt_id << "\n"; })); + std::cout << "cor2:" << fmt_id << "\n"; + }(loop)); + + std::cout << "use inline_scheduler:\n"; + ex::sync_wait(ex::starts_on(ex::inline_scheduler{}, [](auto& pl) -> ex::task { + std::cout << "cor1:" << fmt_id << "\n"; + co_await (ex::schedule(pl.get_scheduler()) | ex::then([] { std::cout << "then:" << fmt_id << "\n"; })); + std::cout << "cor2:" << fmt_id << "\n"; + }(loop))); +} diff --git a/subprojects/task/examples/aggregate-return.cpp b/subprojects/task/examples/aggregate-return.cpp new file mode 100644 index 000000000..bebcb9fd1 --- /dev/null +++ b/subprojects/task/examples/aggregate-return.cpp @@ -0,0 +1,20 @@ +// examples/aggregate-return.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +int main() { + struct aggregate { + int value = 0; + }; + + ex::sync_wait([]() -> ex::task { co_return aggregate{42}; }()); + ex::sync_wait([]() -> ex::task { co_return aggregate{}; }()); + ex::sync_wait([]() -> ex::task { co_return {42}; }()); + ex::sync_wait([]() -> ex::task { co_return {}; }()); +} diff --git a/subprojects/task/examples/alloc-1.cpp b/subprojects/task/examples/alloc-1.cpp new file mode 100644 index 000000000..129c2a30f --- /dev/null +++ b/subprojects/task/examples/alloc-1.cpp @@ -0,0 +1,124 @@ +// examples/alloc-1.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- +// --- defer_frame turns yields a sender calling a coroutine upon start() --- + +template +struct defer_frame { + Mem mem; + Self self; + template + auto operator()(Arg&&... arg) const { + return ex::let_value(ex::read_env(ex::get_allocator), + [mem = this->mem, self = this->self, ... a = std::forward(arg)](auto alloc) { + return std::invoke(mem, self, std::allocator_arg, alloc, std::move(a)...); + }); + } + template + auto operator()(::std::allocator_arg_t, Alloc alloc, Arg&&... arg) const { + return ex::let_value(ex::just(alloc), + [&mem = this->mem, self = this->self, ... a = std::forward(arg)](auto alloc) { + return std::invoke(mem, self, std::allocator_arg, alloc, std::move(a)...); + }); + } + auto operator()(::std::allocator_arg_t) const = delete; +}; + +template +struct defer_frame { + Task task; + template + auto operator()(Arg&&... arg) const { + return ex::let_value(ex::read_env(ex::get_allocator), + [task = this->task, ... a = std::forward(arg)](auto alloc) { + return std::invoke(task, std::allocator_arg, alloc, std::move(a)...); + }); + } + template + auto operator()(::std::allocator_arg_t, Alloc alloc, Arg&&... arg) const { + return ex::let_value(ex::just(alloc), [&task = this->task, ... a = std::forward(arg)](auto alloc) { + return std::invoke(task, std::allocator_arg, alloc, std::move(a)...); + }); + } + auto operator()(::std::allocator_arg_t) const = delete; +}; + +// ---------------------------------------------------------------------------- + +void* operator new(std::size_t n) { + auto p = std::malloc(n); + std::cout << " global new(" << n << ")->" << p << "\n"; + return p; +} +void operator delete(void* ptr) noexcept { + std::cout << " global operator delete(" << ptr << ")\n"; + std::free(ptr); +} +void operator delete(void* ptr, std::size_t size) noexcept { + std::cout << " global operator delete(" << ptr << ", " << size << ")\n"; + std::free(ptr); +} + +struct resource : std::pmr::memory_resource { + void* do_allocate(std::size_t n, std::size_t) override { + auto p{std::malloc(n)}; + std::cout << " resource::allocate(" << n << ")->" << p << "\n"; + return p; + } + void do_deallocate(void* p, std::size_t n, std::size_t) override { + std::cout << " resource::deallocate(" << p << ", " << n << ")\n"; + std::free(p); + } + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { return this == &other; } +}; + +// ---------------------------------------------------------------------------- + +using allocator_type = std::pmr::polymorphic_allocator; +struct alloc_env { + using allocator_type = ::allocator_type; +}; +template +using a_task = ex::task; + +a_task hidden_async_fun(std::allocator_arg_t, ::allocator_type, int value) { co_return value; } +auto async_fun(int value) { return defer_frame(&hidden_async_fun)(value); } + +int main() { + std::cout << std::unitbuf; + resource res{}; + allocator_type alloc(&res); + + std::cout << "not setting up an allocator:\n"; + ex::sync_wait([]() -> a_task<> { + auto result{co_await async_fun(17)}; + std::cout << " result=" << result << "\n"; + }()); + + std::cout << "setting up an allocator:\n"; + ex::sync_wait(ex::write_env( + []() -> a_task<> { + auto result{co_await async_fun(17)}; + std::cout << " result=" << result << "\n"; + }(), + ex::env{ex::prop{ex::get_allocator, alloc}})); + + std::cout << "setting up an allocator and using defer_frame:\n"; + ex::sync_wait(ex::write_env(defer_frame([](auto, auto) -> a_task<> { + auto result{co_await async_fun(17)}; + std::cout << " result=" << result << "\n"; + })(), + ex::env{ex::prop{ex::get_allocator, alloc}})); +} diff --git a/subprojects/task/examples/alloc-2.cpp b/subprojects/task/examples/alloc-2.cpp new file mode 100644 index 000000000..18776618c --- /dev/null +++ b/subprojects/task/examples/alloc-2.cpp @@ -0,0 +1,82 @@ +// examples/alloc-1.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +struct tls_allocator : std::pmr::polymorphic_allocator { + thread_local static std::pmr::memory_resource* alloc; + + tls_allocator() : std::pmr::polymorphic_allocator(alloc) {} + + static void set(std::pmr::memory_resource* a) { alloc = a; } +}; +thread_local std::pmr::memory_resource* tls_allocator::alloc{std::pmr::new_delete_resource()}; + +// ---------------------------------------------------------------------------- + +void* operator new(std::size_t n) { + auto p = std::malloc(n); + std::cout << " global new(" << n << ")->" << p << "\n"; + return p; +} +void operator delete(void* ptr) noexcept { + std::cout << " global operator delete(" << ptr << ")\n"; + std::free(ptr); +} +void operator delete(void* ptr, std::size_t size) noexcept { + std::cout << " global operator delete(" << ptr << ", " << size << ")\n"; + std::free(ptr); +} + +struct resource : std::pmr::memory_resource { + void* do_allocate(std::size_t n, std::size_t) override { + auto p{std::malloc(n)}; + std::cout << " resource::allocate(" << n << ")->" << p << "\n"; + return p; + } + void do_deallocate(void* p, std::size_t n, std::size_t) override { + std::cout << " resource::deallocate(" << p << ", " << n << ")\n"; + std::free(p); + } + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { return this == &other; } +}; + +// ---------------------------------------------------------------------------- + +using allocator_type = tls_allocator; +struct alloc_env { + using allocator_type = ::allocator_type; +}; +template +using a_task = ex::task; + +a_task async_fun(int value) { co_return value; } + +int main(int ac, char*[]) { + resource res{}; + + std::cout << "not setting up an allocator:\n"; + ex::sync_wait([ac]() -> a_task<> { + auto result{co_await async_fun(ac)}; + std::cout << " result=" << result << "\n"; + }()); + + std::cout << "setting up an allocator:\n"; + tls_allocator::set(&res); + ex::sync_wait([ac]() -> a_task<> { + auto result{co_await async_fun(ac)}; + std::cout << " result=" << result << "\n"; + }()); +} diff --git a/subprojects/task/examples/alloc.cpp b/subprojects/task/examples/alloc.cpp new file mode 100644 index 000000000..5d01db575 --- /dev/null +++ b/subprojects/task/examples/alloc.cpp @@ -0,0 +1,118 @@ +// examples/alloc.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +#if defined(__has_feature) +#if __has_feature(thread_sanitizer) +#define TSAN_ENABLED +#endif +#endif +#if !defined(TSAN_ENABLED) +void* operator new(std::size_t size) { // NOLINT(misc-no-recursion) + void* pointer(std::malloc(size)); // NOLINT(hicpp-no-malloc) + std::cout << "global new(" << size << ")->" << pointer << "\n"; + return pointer; +} +void operator delete(void* pointer) noexcept { + std::cout << "global delete(" << pointer << ")\n"; + return std::free(pointer); // NOLINT(hicpp-no-malloc) +} +void operator delete(void* pointer, std::size_t size) noexcept { + std::cout << "global delete(" << pointer << ", " << size << ")\n"; + return std::free(pointer); // NOLINT(hicpp-no-malloc) +} +#endif + +template +class fixed_resource : public std::pmr::memory_resource { + std::array buffer{}; + std::byte* free{this->buffer.data()}; + + void* do_allocate(std::size_t size, std::size_t) override { + if (size <= std::size_t(buffer.data() + Size - free)) { + auto ptr{this->free}; + this->free += size; + std::cout << "resource alloc(" << size << ")->" << ptr << "\n"; + return ptr; + } else { + return nullptr; + } + } + void do_deallocate(void* ptr, std::size_t size, std::size_t) override { + std::cout << "resource dealloc(" << size << "+" << sizeof(std::pmr::polymorphic_allocator) << ")->" + << ptr << "\n"; + } + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { return &other == this; } +}; + +struct alloc_aware { + using allocator_type = std::pmr::polymorphic_allocator; +}; + +struct test_base { + template + void* operator new(std::size_t size, Args&&...) { + void* ptr{::operator new(size)}; + std::cout << "custom alloc(" << size << ") -> " << ptr << "\n"; + return ptr; + } + void operator delete(void* ptr, std::size_t size) { + std::cout << "custom dealloc(" << ptr << ", " << size << ")\n"; + ::operator delete(ptr, size); + } +}; +struct test : test_base {}; + +int main() { + std::cout << "running just\n"; + ex::sync_wait(ex::just(0)); + std::cout << "just done\n\n"; + + std::cout << "running ex::task\n"; + ex::sync_wait([]() -> ex::task { + co_await ex::just(0); + co_await ex::just(0); + }()); + std::cout << "ex::task done\n\n"; + + fixed_resource<2048> resource; + if constexpr (true) { + std::cout << "running ex::task with alloc\n"; + ex::sync_wait([](auto&&...) -> ex::task { + co_await ex::just(0); + co_await ex::just(0); + }(std::allocator_arg, &resource)); + std::cout << "ex::task with alloc done\n\n"; + } + + if constexpr (false) { + std::cout << "running ex::task with alloc\n"; + ex::sync_wait([](auto&&...) -> ex::task { + co_await ex::just(0); + co_await ex::just(0); + }(std::allocator_arg, &resource)); + std::cout << "ex::task with alloc done\n\n"; + } + + if constexpr (false) { + std::cout << "running ex::task extracting alloc\n"; + ex::sync_wait([](auto&&, [[maybe_unused]] auto* res) -> ex::task { + auto alloc = co_await ex::read_env(ex::get_allocator); + static_assert(std::same_as, decltype(alloc)>); + assert(alloc == std::pmr::polymorphic_allocator(res)); + }(std::allocator_arg, &resource)); + std::cout << "ex::task extracting alloc done\n\n"; + } + + delete new test{}; +} diff --git a/subprojects/task/examples/async-lock.cpp b/subprojects/task/examples/async-lock.cpp new file mode 100644 index 000000000..47bc7a117 --- /dev/null +++ b/subprojects/task/examples/async-lock.cpp @@ -0,0 +1,116 @@ +// examples/async-lock -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { +struct queue { + struct notify { + notify() = default; + notify(const notify&) = delete; + notify(notify&&) = delete; + virtual ~notify() = default; + notify& operator=(const notify&) = delete; + notify& operator=(notify&&) = delete; + virtual void complete(int) = 0; + }; + + std::mutex mutex; + std::condition_variable condition; + std::queue> work; + + void push(notify* n, int w) { + { + std::lock_guard cerberos{this->mutex}; + work.emplace(n, w); + } + condition.notify_one(); + } + std::tuple pop() { + std::unique_lock cerberos{this->mutex}; + condition.wait(cerberos, [this] { return not work.empty(); }); + auto result{work.front()}; + work.pop(); + return result; + } +}; + +struct request { + using sender_concept = ex::sender_tag; + using completion_signatures = ex::completion_signatures; + + template + struct state : queue::notify { + using operation_state_concept = ex::operation_state_tag; + Receiver receiver; + int value; + queue& que; + + template + state(R&& r, int val, queue& q) : receiver(std::forward(r)), value(val), que(q) {} + + void start() & noexcept { + try { + this->que.push(this, value); + } catch (...) { + std::terminate(); + } + } + void complete(int result) override { ex::set_value(std::move(this->receiver), result); } + }; + + int value; + queue& que; + + template + auto connect(Receiver&& receiver) { + return state>(std::forward(receiver), this->value, this->que); + } +}; + +void stop(queue& q) { ex::sync_wait(request{0, q}); } +} // namespace + +int main(int ac, char* av[]) { + try { + queue q{}; + + std::thread process([&q] { + while (true) { + auto [completion, request] = q.pop(); + completion->complete(2 * request); + if (request == 0) { + break; + } + } + }); + + auto work{[](queue& que) -> ex::task { + std::cout << std::this_thread::get_id() << " start\n" << std::flush; + auto result = co_await request{17, que}; + std::cout << std::this_thread::get_id() << " result=" << result << "\n" << std::flush; + stop(que); + }}; + + if (1 < ac && av[1] == std::string_view("run-it")) { + ex::sync_wait( + ex::detail::write_env(work(q), ex::detail::make_env(ex::get_scheduler, ex::inline_scheduler{}))); + } else { + ex::sync_wait(work(q)); + } + process.join(); + std::cout << "done\n"; + } catch (...) { + std::cout << "ERROR: unexpected exception\n"; + } +} diff --git a/subprojects/task/examples/bulk.cpp b/subprojects/task/examples/bulk.cpp new file mode 100644 index 000000000..aabb167ba --- /dev/null +++ b/subprojects/task/examples/bulk.cpp @@ -0,0 +1,27 @@ +// examples/bulk.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; +namespace beman::execution { +using parallel_scheduler = inline_scheduler; +} // namespace beman::execution + +// ---------------------------------------------------------------------------- + +int main() { + struct env { + auto query(ex::get_scheduler_t) const noexcept { return ex::parallel_scheduler(); } + }; + struct work { + auto operator()(std::size_t) { /*...*/ }; + }; + + ex::sync_wait(ex::write_env(ex::bulk(ex::just(), 16u, work{}), env{})); + + ex::sync_wait( + ex::write_env([]() -> ex::task> { co_await ex::bulk(ex::just(), 16u, work{}); }(), env{})); +} diff --git a/subprojects/task/examples/c++now-affinity.cpp b/subprojects/task/examples/c++now-affinity.cpp new file mode 100644 index 000000000..642fde8bf --- /dev/null +++ b/subprojects/task/examples/c++now-affinity.cpp @@ -0,0 +1,56 @@ +// examples/c++now-affinity.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include "demo-thread_loop.hpp" +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { + +ex::task<> work1(auto sched) { + std::cout << "before id=" << std::this_thread::get_id() << "\n"; + co_await (ex::schedule(sched) | ex::then([] { std::cout << "then id =" << std::this_thread::get_id() << "\n"; })); + std::cout << "after id =" << std::this_thread::get_id() << "\n\n"; +} + +ex::task<> work2(auto sched) { + std::cout << "before id=" << std::this_thread::get_id() << "\n"; + auto s = co_await ex::change_coroutine_scheduler(ex::inline_scheduler()); + co_await (ex::schedule(sched) | ex::then([] { std::cout << "then id =" << std::this_thread::get_id() << "\n"; })); + std::cout << "after1 id=" << std::this_thread::get_id() << "\n"; + + co_await ex::change_coroutine_scheduler(s); + co_await (ex::schedule(sched) | ex::then([] { std::cout << "then id =" << std::this_thread::get_id() << "\n"; })); + std::cout << "after2 id=" << std::this_thread::get_id() << "\n\n"; +} + +struct inline_context { + using scheduler_type = ex::inline_scheduler; +}; + +ex::task work3(auto sched) { + std::cout << "before id=" << std::this_thread::get_id() << "\n"; + co_await (ex::schedule(sched) | ex::then([] { std::cout << "then id =" << std::this_thread::get_id() << "\n"; })); + std::cout << "after id =" << std::this_thread::get_id() << "\n\n"; +} +} // namespace + +int main() { + try { + demo::thread_loop loop; + ex::sync_wait(work1(loop.get_scheduler())); + ex::sync_wait(work2(loop.get_scheduler())); + ex::sync_wait(work3(loop.get_scheduler())); + } catch (...) { + std::cout << "ERROR: unexpected exception\n"; + } +} diff --git a/subprojects/task/examples/c++now-allocator.cpp b/subprojects/task/examples/c++now-allocator.cpp new file mode 100644 index 000000000..ce7d40ef8 --- /dev/null +++ b/subprojects/task/examples/c++now-allocator.cpp @@ -0,0 +1,28 @@ +// examples/c++now-alloctor.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { + +struct with_allocator { + using allocator_type = std::pmr::polymorphic_allocator; +}; + +ex::task coro(int value, auto&&...) { + co_await ex::just(value); + [[maybe_unused]] auto alloc = co_await ex::read_env(ex::get_allocator); +} +} // namespace + +int main() { + ex::sync_wait(coro(0)); + ex::sync_wait(coro(1, std::allocator_arg, std::pmr::new_delete_resource())); +} diff --git a/subprojects/task/examples/c++now-basic.cpp b/subprojects/task/examples/c++now-basic.cpp new file mode 100644 index 000000000..b53586fa3 --- /dev/null +++ b/subprojects/task/examples/c++now-basic.cpp @@ -0,0 +1,34 @@ +// examples/c++now-basic.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +ex::task<> basic() { co_await std::suspend_never{}; } + +ex::task<> await_sender() { + co_await ex::just(); + [[maybe_unused]] int n = co_await ex::just(1); + [[maybe_unused]] auto [m, b] = co_await ex::just(1, true); + try { + co_await ex::just_error(1); + } catch (int e) { + std::cout << "caught " << e << '\n'; + } + co_await ex::just_stopped(); +} + +} // namespace + +int main() { + ex::sync_wait(std::suspend_never{}); + ex::sync_wait(basic()); + ex::sync_wait(await_sender()); +} diff --git a/subprojects/task/examples/c++now-cancel.cpp b/subprojects/task/examples/c++now-cancel.cpp new file mode 100644 index 000000000..60cf6fd8b --- /dev/null +++ b/subprojects/task/examples/c++now-cancel.cpp @@ -0,0 +1,15 @@ +// examples/c++now-cancel.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// ---------------------------------------------------------------------------- + +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- +namespace { +ex::task<> cancel() { co_await ex::just_stopped(); } +} // namespace + +int main() { ex::sync_wait(cancel()); } diff --git a/subprojects/task/examples/c++now-errors.cpp b/subprojects/task/examples/c++now-errors.cpp new file mode 100644 index 000000000..f4fb468fd --- /dev/null +++ b/subprojects/task/examples/c++now-errors.cpp @@ -0,0 +1,84 @@ +// examples/c++now-errors.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +struct none_t {}; +std::ostream& operator<<(std::ostream& out, none_t) { return out << ""; } + +template +struct identity_or_none; +template <> +struct identity_or_none<> { + using type = none_t; +}; +template <> +struct identity_or_none { + using type = none_t; +}; +template +struct identity_or_none { + using type = T; +}; +template +using identity_or_none_t = typename identity_or_none::type; + +#if 202202 <= __cpp_lib_expected +template +auto as_expected(Sender&& sndr) { + using value_type = ex::value_types_of_t, std::tuple, identity_or_none_t>; + using error_type = ex::error_types_of_t, identity_or_none_t>; + using result_type = std::expected; + + return std::forward(sndr) | + ex::then([](T&& x) noexcept { return result_type(std::forward(x)); }) | + ex::upon_error([](T&& x) noexcept { return result_type(std::unexpected(std::forward(x))); }); +} +#endif + +void print_expected(const char* msg, const auto& e) { + if (e) { + if constexpr (std::same_as>) + std::cout << msg << " (value)\n"; + else + std::cout << msg << " (value)" << std::get<0>(e.value()) << "\n"; + } else + std::cout << msg << " (error)" << e.error() << "\n"; +} + +// ---------------------------------------------------------------------------- + +ex::task<> error_result() { + try { + co_await ex::just_error(17); + } catch (int n) { + std::cout << "Error: " << n << "\n"; + } +} + +#if 202202 <= __cpp_lib_expected +ex::task<> expected() { + auto e = co_await as_expected(ex::just(17)); + print_expected("expected with value=", e); + auto u = co_await as_expected(ex::just_error(17)); + print_expected("expected without value=", u); +} +#endif +} // namespace + +int main() { + ex::sync_wait(error_result()); +#if 202202 <= __cpp_lib_expected + ex::sync_wait(expected()); +#endif +} diff --git a/subprojects/task/examples/c++now-query.cpp b/subprojects/task/examples/c++now-query.cpp new file mode 100644 index 000000000..369786ba6 --- /dev/null +++ b/subprojects/task/examples/c++now-query.cpp @@ -0,0 +1,66 @@ +// examples/c++now-query.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; +namespace exd = beman::execution::detail; + +// ---------------------------------------------------------------------------- + +namespace { + +constexpr struct get_value_t { + template + requires requires(const get_value_t& self, const Env& e) { e.query(self); } + decltype(auto) operator()(const Env& e) const { + return e.query(*this); + } + constexpr auto query(const ex::forwarding_query_t&) const noexcept -> bool { return true; } +} get_value{}; + +struct context { + int value{}; + explicit context(const auto& e) : value(get_value(e)) {} + int query(const get_value_t&) const { return this->value; } +}; + +ex::task with_env() { + decltype(auto) v = co_await ex::read_env(get_value); + std::cout << "with_env: v=" << v << "\n"; +} + +struct fancy { + struct env_base { + virtual int get() const = 0; + + env_base() = default; + env_base(const env_base&) = delete; + env_base(env_base&&) = delete; + virtual ~env_base() = default; + env_base& operator=(const env_base&) = delete; + env_base& operator=(env_base&&) = delete; + }; + template + struct env_type final : env_base { + explicit env_type(const Env& e) : env(e) {} + Env env; + int get() const override { return get_value(env); } + }; + int query(const get_value_t&) const { return this->env.get(); } + explicit fancy(const env_base& own) : env(own) {} + const env_base& env; +}; + +ex::task with_fancy_env() { + decltype(auto) v = co_await ex::read_env(get_value); + std::cout << "v=" << v << "\n"; +} +} // namespace + +int main() { + ex::sync_wait(exd::write_env(with_env(), exd::make_env(get_value, 17))); + ex::sync_wait(exd::write_env(with_fancy_env(), exd::make_env(get_value, 17))); +} diff --git a/subprojects/task/examples/c++now-result-types.cpp b/subprojects/task/examples/c++now-result-types.cpp new file mode 100644 index 000000000..bb1634290 --- /dev/null +++ b/subprojects/task/examples/c++now-result-types.cpp @@ -0,0 +1,20 @@ +// examples/c++now-result-types.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +ex::task<> result_type() { + co_await ex::just(); + [[maybe_unused]] int n = co_await ex::just(1); + [[maybe_unused]] std::tuple p = co_await ex::just(1, true); +} +} // namespace + +int main() { ex::sync_wait(result_type()); } diff --git a/subprojects/task/examples/c++now-return.cpp b/subprojects/task/examples/c++now-return.cpp new file mode 100644 index 000000000..def18e211 --- /dev/null +++ b/subprojects/task/examples/c++now-return.cpp @@ -0,0 +1,45 @@ +// examples/c++now-return.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { + +struct E {}; +struct F {}; + +struct context { + using error_types = ex::completion_signatures; +}; + +struct empty_errors_context { + using error_types = ex::completion_signatures<>; +}; + +// ---------------------------------------------------------------------------- + +ex::task<> default_return() { co_return; } + +ex::task void_return() { co_return; } + +ex::task int_return() { co_return 17; } + +ex::task error_return() { co_return 17; } + +ex::task no_error_return() { co_return 17; } +} // namespace + +int main() { + ex::sync_wait(default_return()); + ex::sync_wait(void_return()); + [[maybe_unused]] auto [n] = ex::sync_wait(int_return()).value_or(std::tuple(0)); + std::cout << "n=" << n << "\n"; + [[maybe_unused]] auto [e] = ex::sync_wait(error_return()).value_or(std::tuple(0)); + [[maybe_unused]] auto [x] = ex::sync_wait(no_error_return()).value_or(std::tuple(0)); +} diff --git a/subprojects/task/examples/c++now-stop_token.cpp b/subprojects/task/examples/c++now-stop_token.cpp new file mode 100644 index 000000000..1957917de --- /dev/null +++ b/subprojects/task/examples/c++now-stop_token.cpp @@ -0,0 +1,39 @@ +// examples/c++now-stop_token.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +ex::task<> stopping() { + auto token = co_await ex::read_env(ex::get_stop_token); + std::size_t count{}; + while (!token.stop_requested()) { + ++count; + } + std::cout << "count=" << count << "\n"; +} + +} // namespace + +int main() { + using namespace std::chrono_literals; + + ex::inplace_stop_source source; + std::thread thread([&] { + ex::sync_wait(ex::detail::write_env(stopping(), ex::detail::make_env(ex::get_stop_token, source.get_token()))); + }); + + std::this_thread::sleep_for(100ms); + source.request_stop(); + + thread.join(); +} diff --git a/subprojects/task/examples/c++now-with_error.cpp b/subprojects/task/examples/c++now-with_error.cpp new file mode 100644 index 000000000..8e672f4db --- /dev/null +++ b/subprojects/task/examples/c++now-with_error.cpp @@ -0,0 +1,63 @@ +// examples/c++now-with_error.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +void unreachable([[maybe_unused]] const char* msg) { assert(nullptr == msg); } + +struct E { + int value; +}; + +struct context { + using error_types = ex::completion_signatures; +}; + +// ---------------------------------------------------------------------------- + +ex::task error_return(int arg) noexcept { + try { + if (arg == 1) + co_yield ex::with_error{E{arg}}; + co_return arg * 17; + } catch (...) { + unreachable("no error should escape co_yield with_error{E{0}}"); + std::terminate(); + } +} + +struct ctxt { + using error_types = ex::completion_signatures; +}; + +ex::task call(int v) { + if (v == 1) + co_yield ex::with_error{-1}; + co_return 2 * v; +} +} // namespace + +int main(int ac, char*[]) { + for (int i{}; i != 3; ++i) { + auto [r] = ex::sync_wait(error_return(i) | ex::upon_error([](auto x) { + std::cout << "error: " << x.value << "\n"; + return -1; + })) + .value_or(std::tuple(0)); + std::cout << i << ": r=" << r << "\n"; + } + + [[maybe_unused]] auto [n] = ex::sync_wait(call(ac) | ex::upon_error([](int e) { + std::cout << "error(" << e << "\n"; + return -1; + })) + .value_or(std::tuple(0)); +} diff --git a/subprojects/task/examples/co_await-result.cpp b/subprojects/task/examples/co_await-result.cpp new file mode 100644 index 000000000..6fa7b0a0c --- /dev/null +++ b/subprojects/task/examples/co_await-result.cpp @@ -0,0 +1,40 @@ +// examples/co_await-result.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { +void unreachable([[maybe_unused]] const char* msg) { assert(nullptr == msg); } +} // namespace +// ---------------------------------------------------------------------------- + +int main() { + [[maybe_unused]] auto o = ex::sync_wait([]() -> ex::task<> { + co_await ex::just(); // void + std::cout << "after co_await ex::just()\n"; + [[maybe_unused]] auto v = co_await ex::just(42); // int + assert(v == 42); + [[maybe_unused]] auto [i, b, c] = co_await ex::just(17, true, 'c'); // tuple + assert(i == 17 && b == true && c == 'c'); + try { + co_await ex::just_error(-1); // exception + unreachable("co_await just_error(...) result in an exception"); + } catch ([[maybe_unused]] int e) { + assert(e == -1); + } + std::cout << "about to cancel\n"; + try { + co_await ex::just_stopped(); + } catch (...) { + unreachable("co_await just_stopped(...) doesn't result in an exception"); + } // cancel: never resumed + unreachable("co_await just_stopped(...) result in the coroutine not to be resumed"); + }()); + assert(not o); + std::cout << "done\n"; +} diff --git a/subprojects/task/examples/co_await-task.cpp b/subprojects/task/examples/co_await-task.cpp new file mode 100644 index 000000000..6a451d0eb --- /dev/null +++ b/subprojects/task/examples/co_await-task.cpp @@ -0,0 +1,24 @@ +// examples/co_await-task.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +auto inner() -> ex::task<> { co_return; } + +auto outer() -> ex::task<> { + for (int i{}; i < 10; ++i) { + co_await inner(); + } + co_return; +} + +auto main() -> int { + std::cout << std::unitbuf; + ex::sync_wait(outer()); +} diff --git a/subprojects/task/examples/container.cpp b/subprojects/task/examples/container.cpp new file mode 100644 index 000000000..7a538f228 --- /dev/null +++ b/subprojects/task/examples/container.cpp @@ -0,0 +1,26 @@ +// examples/container.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +void unreachable([[maybe_unused]] const char* msg) { assert(nullptr != msg); } +} // namespace + +int main() { + try { + std::vector> cont; + cont.emplace_back([]() -> ex::task<> { co_return; }()); + cont.push_back([]() -> ex::task<> { co_return; }()); + } catch (...) { + unreachable("no exception should escape to main"); + } +} diff --git a/subprojects/task/examples/customize.cpp b/subprojects/task/examples/customize.cpp new file mode 100644 index 000000000..0a5e1631c --- /dev/null +++ b/subprojects/task/examples/customize.cpp @@ -0,0 +1,31 @@ +// examples/customize.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { +struct empty {}; +} // namespace + +int main() { +#if 0 + auto task = []()->ex::task { co_return; }(); + static_assert(ex::sender); + auto env = ex::get_env(task); + static_assert(std::same_as); + auto dom = ex::get_domain(env); + static_assert(std::same_as); + auto edom = ex::detail::get_domain_early(task); + static_assert(std::same_as); + std::cout << "---\n"; + transform_sender(edom, ex::detail::make_sender(ex::affine_on, ex::inline_scheduler{}, std::move(task))); + std::cout << "---\n"; + auto affine = ex::affine_on(std::move(task), ex::inline_scheduler{}); +#endif +} diff --git a/subprojects/task/examples/dangling-references.cpp b/subprojects/task/examples/dangling-references.cpp new file mode 100644 index 000000000..501ba6745 --- /dev/null +++ b/subprojects/task/examples/dangling-references.cpp @@ -0,0 +1,34 @@ +// examples/dangling-references.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +ex::task> do_work(std::string) { /* work */ co_return 0; }; +ex::task> execute_all() { + co_await ex::when_all(do_work("arguments 1"), do_work("arguments 2")); + co_return; +} + +struct error_env { + using error_types = ex::completion_signatures; +}; +} // namespace + +int main() { + ex::sync_wait([]() -> ex::task, error_env> { co_return ex::with_error{42}; }()); + + ex::sync_wait(execute_all()); + ex::sync_wait([]() -> ex::task> { + auto t = [](const int /* this would be added: &*/ v) -> ex::task> { co_return v; }(42); + [[maybe_unused]] auto v = co_await std::move(t); + }()); +} diff --git a/subprojects/task/examples/demo-scope.hpp b/subprojects/task/examples/demo-scope.hpp new file mode 100644 index 000000000..d1c2efb6c --- /dev/null +++ b/subprojects/task/examples/demo-scope.hpp @@ -0,0 +1,91 @@ +// examples/demo_scope.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_EXAMPLES_DEMO_SCOPE +#define INCLUDED_EXAMPLES_DEMO_SCOPE + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace demo { +namespace ex = ::beman::net::detail::ex; + +class scope { + private: + static constexpr bool log_completions{false}; + struct env { + scope* self; + + auto query(ex::get_stop_token_t) const noexcept { return this->self->source.get_token(); } + }; + + struct job_base { + virtual ~job_base() = default; + }; + + struct receiver { + using receiver_concept = ex::receiver_tag; + scope* self; + job_base* state{}; + + auto set_error(auto&&) noexcept -> void { + ::std::cerr << "ERROR: demo::scope::job in scope completed with error!\n"; + this->complete(); + } + auto set_value() && noexcept -> void { + if (log_completions) + std::cout << "demo::scope::set_value()\n"; + this->complete(); + } + auto set_stopped() && noexcept -> void { + if (log_completions) + std::cout << "demo::scope::set_stopped()\n"; + this->complete(); + } + auto complete() -> void { + scope* slf{this->self}; + delete this->state; + if (0u == --slf->count) { + slf->complete(); + } + } + auto get_env() const noexcept -> env { return {this->self}; } + }; + + template + struct job : job_base { + using state_t = decltype(ex::connect(std::declval(), std::declval())); + state_t state; + template + job(scope* self, S&& sender) : state(ex::connect(::std::forward(sender), receiver{self, this})) { + ex::start(this->state); + } + }; + + std::atomic count{}; + ex::inplace_stop_source source; + + auto complete() -> void {} + + public: + ~scope() { + if (0u < this->count) + std::cerr << "ERROR: scope destroyed with live jobs: " << this->count << "\n"; + } + template + auto spawn(Sender&& sender) { + ++this->count; + new job(this, std::forward(sender)); + } + auto stop() -> void { this->source.request_stop(); } + auto empty() const -> bool { return 0u == this->count; } +}; +} // namespace demo + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/examples/demo-thread_loop.hpp b/subprojects/task/examples/demo-thread_loop.hpp new file mode 100644 index 000000000..bedbe9d9d --- /dev/null +++ b/subprojects/task/examples/demo-thread_loop.hpp @@ -0,0 +1,30 @@ +// examples/demo-thread_loop.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_EXAMPLES_DEMO_THREAD_LOOP +#define INCLUDED_EXAMPLES_DEMO_THREAD_LOOP + +// ---------------------------------------------------------------------------- + +#include +#include +#include + +namespace demo { + +class thread_loop : public ::beman::execution::run_loop { + private: + std::thread thread{std::bind(&thread_loop::run, this)}; + + public: + ~thread_loop() { + this->finish(); + this->thread.join(); + } +}; + +} // namespace demo + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/examples/demo-tls_scheduler.hpp b/subprojects/task/examples/demo-tls_scheduler.hpp new file mode 100644 index 000000000..772b54faf --- /dev/null +++ b/subprojects/task/examples/demo-tls_scheduler.hpp @@ -0,0 +1,171 @@ +// examples/demo-tls_scheduler.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_EXAMPLES_DEMO_TLS_SCHEDULER +#define INCLUDED_EXAMPLES_DEMO_TLS_SCHEDULER + +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace demo { +struct tls_domain { + template <::beman::execution::receiver Rcvr, typename Data> + struct affine_state_base { + std::remove_cvref_t rcvr; + Data data{}; + + auto complete() { + std::cout << "affine_state::complete\n"; + this->data.restore(); + ::beman::execution::set_value(::std::move(this->rcvr)); + } + }; + + template <::beman::execution::receiver Rcvr, typename Data> + struct affine_receiver { + using receiver_concept = ::beman::execution::receiver_tag; + struct env_t { + affine_state_base* st; + template + requires requires(affine_state_base* self, Q&& q, A&&... a) { + std::forward(q)(::beman::execution::get_env(self->st->rcvr), ::std::forward(a)...); + } + auto query(Q&& q, A&&... a) const noexcept { + std::forward(q)(::beman::execution::get_env(this->st->rcvr, ::std::forward(a)...)); + } + }; + affine_state_base* st; + + template + auto set_error(E&& e) && noexcept -> void { + ::beman::execution::set_error(::std::move(this->st->rcvr), ::std::forward(e)); + } + auto set_stopped() && noexcept -> void { ::beman::execution::set_stopped(::std::move(this->st->rcvr)); } + auto set_value() && noexcept -> void { this->st->complete(); } + + auto get_env() const noexcept -> env_t { return {this->st}; } + }; + + template <::beman::execution::sender Sndr, ::beman::execution::scheduler Sch, ::beman::execution::receiver Rcvr> + struct affine_state : affine_state_base { + using operation_state_concept = ::beman::execution::operation_state_tag; + using env_t = decltype(::beman::execution::get_env(std::declval())); + using base_t = affine_state_base; + using data_t = typename Sch::type; + using state_t = decltype(::beman::execution::connect( + ::beman::execution::affine_on(::std::declval(), ::std::declval()), + ::std::declval>())); + + state_t state; + + template <::beman::execution::sender S, ::beman::execution::scheduler SC, ::beman::execution::receiver R> + affine_state(S&& s, SC&& sc, R&& r) + : base_t{::std::forward(r)}, + state(::beman::execution::connect( + ::beman::execution::affine_on(::std::forward(s), ::std::forward(sc)), + affine_receiver{this})) {} + auto start() & noexcept -> void { + std::cout << "affine_state::start\n"; + this->data.save(); + ::beman::execution::start(this->state); + } + }; + template <::beman::execution::sender Sndr, ::beman::execution::scheduler Sch> + struct affine_sender { + using sender_concept = ::beman::execution::sender_tag; + + std::remove_cvref_t sndr; + std::remove_cvref_t sch; + + template + auto get_completion_signatures(const Env& env) const noexcept { + return ::beman::execution::get_completion_signatures(this->sndr, env); + } + template <::beman::execution::receiver Rcvr> + auto connect(Rcvr&& rcvr) const& { + return affine_state(sndr, sch, std::forward(rcvr)); + } + template <::beman::execution::receiver Rcvr> + auto connect(Rcvr&& rcvr) && { + return affine_state(std::move(sndr), std::move(sch), std::forward(rcvr)); + } + }; + template <::beman::execution::sender Sndr, typename... Env> + requires std::same_as<::beman::execution::tag_of_t<::std::remove_cvref_t>, + ::beman::execution::affine_on_t> + auto transform_sender(Sndr&& s, const Env&...) const noexcept { + auto [tag, sch, sndr] = s; + return affine_sender{::beman::execution::detail::forward_like(sndr), + ::beman::execution::detail::forward_like(sch)}; + } +}; + +template +struct tls_scheduler { + using scheduler_concept = ::beman::execution::scheduler_tag; + using type = Data; + struct env { + Scheduler sched; + template + auto query(const ::beman::execution::get_completion_scheduler_t&) const noexcept -> tls_scheduler { + return {this->sched}; + } + }; + template <::beman::execution::receiver Receiver> + struct state { + using operation_state_concept = ::beman::execution::operation_state_tag; + using upstream_state_t = decltype(::beman::execution::connect( + ::beman::execution::schedule(std::declval()), std::declval>())); + + upstream_state_t upstream; + + template <::beman::execution::sender Sndr, ::beman::execution::receiver Rcvr> + state(Sndr&& sndr, Rcvr&& rcvr) + : upstream(::beman::execution::connect(std::forward(sndr), std::forward(rcvr))) {} + auto start() & noexcept -> void { ::beman::execution::start(this->upstream); } + }; + struct tls_sender { + using sender_concept = ::beman::execution::sender_tag; + using sender_type = decltype(::beman::execution::schedule(std::declval())); + + Scheduler sched; + + template + auto get_completion_signatures(const Env&... env) const noexcept { + return ::beman::execution::get_completion_signatures(::beman::execution::schedule(Scheduler(this->sched)), + env...); + } + auto get_env() const noexcept -> env { return {this->sched}; } + + template <::beman::execution::receiver Receiver> + auto connect(Receiver&& rcvr) -> state { + return state(::beman::execution::schedule(this->sched), std::forward(rcvr)); + } + }; + + Scheduler sched; + template + requires(!std::same_as>) + tls_scheduler(Sch&& sch) : sched(sch) {} + tls_scheduler(const tls_scheduler&) = default; + tls_scheduler(tls_scheduler&&) = default; + tls_scheduler& operator=(const tls_scheduler&) = default; + tls_scheduler& operator=(tls_scheduler&&) = default; + + auto schedule() -> tls_sender { return {this->sched}; } + + auto query(const ::beman::execution::get_domain_t&) const noexcept -> tls_domain { return {}; } + + auto operator==(const tls_scheduler&) const -> bool = default; +}; +static_assert(::beman::execution::scheduler< + tls_scheduler().get_scheduler())>>); + +} // namespace demo + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/examples/environment.cpp b/subprojects/task/examples/environment.cpp new file mode 100644 index 000000000..808046aa5 --- /dev/null +++ b/subprojects/task/examples/environment.cpp @@ -0,0 +1,159 @@ +// examples/environment.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef _MSC_VER +#include +#include +#include +#include +#include +#include +#include +#include +#include "demo-scope.hpp" +#include "demo-thread_loop.hpp" + +namespace ex = beman::execution; +namespace net = beman::net; +using namespace std::chrono_literals; + +// ---------------------------------------------------------------------------- + +template +void spawn(Sched&& sched, demo::scope& scope, Sender&& sender) { + scope.spawn(ex::detail::write_env(std::forward(sender), + ex::detail::make_env(ex::get_scheduler, std::forward(sched)))); +} + +// ---------------------------------------------------------------------------- + +class environment { + static thread_local std::string name; + + public: + static auto get() -> std::string { return name; } + static auto set(const std::string& n) -> void { name = n; } +}; +thread_local std::string environment::name{""}; + +struct env_scheduler { + using scheduler_concept = ex::scheduler_tag; + + std::string name; + ex::task_scheduler scheduler; + + template + env_scheduler(std::string n, Sched&& sched) : name(std::move(n)), scheduler(std::forward(sched)) {} + + template + struct receiver { + using receiver_concept = ex::receiver_tag; + + std::remove_cvref_t rcvr; + std::string name; + + receiver(Rcvr&& r, std::string n) : rcvr(std::forward(r)), name(std::move(n)) {} + auto get_env() const noexcept { return ex::get_env(this->rcvr); } + auto set_value() && noexcept { + environment::set(std::move(this->name)); + ex::set_value(std::move(this->rcvr)); + } + template + auto set_error(E&& e) && noexcept { + environment::set(std::move(this->name)); + ex::set_error(std::move(this->rcvr), std::forward(e)); + } + auto set_stopped() && noexcept { + environment::set(std::move(this->name)); + ex::set_stopped(std::move(this->rcvr)); + } + }; + + struct env { + std::string name; + ex::task_scheduler scheduler; + template + auto query(const ex::get_completion_scheduler_t&) const noexcept { + return env_scheduler(this->name, this->scheduler); + } + }; + + struct sender { + using sender_concept = ex::sender_tag; + using task_sender = decltype(ex::schedule(std::declval())); + template + auto get_completion_signatures(const E& e) const noexcept { + return ex::get_completion_signatures(this->sender, e); + } + + std::string name; + task_sender sender; + + auto get_env() const noexcept -> env { + return env{this->name, ex::get_completion_scheduler(ex::get_env(this->sender))}; + } + + template + auto connect(Rcvr&& rcvr) && { + return ex::connect(std::move(this->sender), + receiver(std::forward(rcvr), std::move(this->name))); + } + }; + + auto schedule() -> sender { return sender{this->name, ex::schedule(this->scheduler)}; } + bool operator==(const env_scheduler&) const = default; +}; + +struct with_env { + using scheduler_type = env_scheduler; +}; + +// ---------------------------------------------------------------------------- + +std::ostream& print_env(std::ostream& out) { + return out << "tid=" << std::this_thread::get_id() << " " + << "env=" << environment::get(); +} + +ex::task run(auto scheduler, auto duration) { + std::cout << print_env << " duration=" << duration << " start\n" << std::flush; + for (int i = 0; i != 4; ++i) { + co_await net::resume_after(scheduler, duration); + std::cout << print_env << " duration=" << duration << "\n" << std::flush; + } + std::cout << print_env << " duration=" << duration << " done\n" << std::flush; +} + +const std::string text("####"); +[[maybe_unused]] const std::string black("\x1b[30m" + text + "\x1b[0m:"); +[[maybe_unused]] const std::string red("\x1b[31m" + text + "\x1b[0m:"); +[[maybe_unused]] const std::string green("\x1b[32m" + text + "\x1b[0m:"); +[[maybe_unused]] const std::string yellow("\x1b[33m" + text + "\x1b[0m:"); +[[maybe_unused]] const std::string blue("\x1b[34m" + text + "\x1b[0m:"); +[[maybe_unused]] const std::string magenta("\x1b[35m" + text + "\x1b[0m:"); +[[maybe_unused]] const std::string cyan("\x1b[36m" + text + "\x1b[0m:"); +[[maybe_unused]] const std::string white("\x1b[37m" + text + "\x1b[0m:"); + +int main() { + demo::thread_loop loop1; + demo::thread_loop loop2; + net::io_context context; + demo::scope scope; + + environment::set("main"); + + ex::sync_wait(ex::schedule(loop1.get_scheduler()) | ex::then([] { environment::set("thread1"); }) | + ex::then([] { std::cout << print_env << "\n"; })); + std::cout << print_env << "\n"; + + spawn(env_scheduler(magenta, loop1.get_scheduler()), scope, run(context.get_scheduler(), 100ms)); + spawn(env_scheduler(green, loop1.get_scheduler()), scope, run(context.get_scheduler(), 150ms)); + spawn(env_scheduler(blue, loop1.get_scheduler()), scope, run(context.get_scheduler(), 250ms)); + + while (!scope.empty()) { + context.run(); + } +} +#else +int main() {} +#endif diff --git a/subprojects/task/examples/error.cpp b/subprojects/task/examples/error.cpp new file mode 100644 index 000000000..ca4c14b28 --- /dev/null +++ b/subprojects/task/examples/error.cpp @@ -0,0 +1,64 @@ +// examples/error.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { +struct error_context { + using error_types = + ex::completion_signatures; +}; + +ex::task fun(int i) { + using on_exit = std::unique_ptr; + on_exit msg("> "); + std::cout << "<"; + + switch (i) { + default: + break; + case 0: + // successful execution: a later `co_return 0;` completes + co_await ex::just(0); + break; + case 1: + // the co_awaited work fails and an exception is thrown + co_await ex::just_error(0); + break; + case 2: + // the co_awaited work is stopped and the coroutine is stopped itself + co_await ex::just_stopped(); + break; + case 3: + // successful return of a value + co_return 17; + case 4: + co_yield ex::with_error{std::make_error_code(std::errc::io_error)}; + break; + } + + co_return 0; +} + +template +void print(const char* what, const E& e) { + if constexpr (std::same_as) + std::cout << what << "(exception_ptr)\n"; + else + std::cout << what << "(" << e << ")\n"; +} +} // namespace + +int main() { + for (int i{}; i != 5; ++i) { + std::cout << "i=" << i << " "; + ex::sync_wait(fun(i) | ex::then([](auto e) { print("then", e); }) | + ex::upon_error([](const auto& e) { print("upon_error", e); }) | + ex::upon_stopped([] { std::cout << "stopped\n"; })); + } +} diff --git a/subprojects/task/examples/escaped-exception.cpp b/subprojects/task/examples/escaped-exception.cpp new file mode 100644 index 000000000..5d815e691 --- /dev/null +++ b/subprojects/task/examples/escaped-exception.cpp @@ -0,0 +1,30 @@ +// examples/escaped-exception.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include + +namespace ex = beman::execution; + +struct my_exception : std::exception { + const char* what() const noexcept override { return "my_exception"; } +}; + +int main() { + try { + ex::sync_wait([]() -> ex::task { +#ifndef __clang__ + //-dk:TODO determine what's up with clang + throw my_exception{}; +#endif + co_return 0; + }()); + std::cout << "not reached!\n"; + } catch (const std::exception& ex) { + std::cout << "ERROR: " << ex.what() << "\n"; + } catch (...) { + std::cout << "ERROR: unknown exception\n"; + } +} diff --git a/subprojects/task/examples/friendly.cpp b/subprojects/task/examples/friendly.cpp new file mode 100644 index 000000000..adb921393 --- /dev/null +++ b/subprojects/task/examples/friendly.cpp @@ -0,0 +1,17 @@ +// examples/friendly.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +int main() { + ex::sync_wait([]() -> ex::task { co_await ex::just(); }()); + ex::sync_wait([]() -> ex::task { co_await std::suspend_never(); }()); +} diff --git a/subprojects/task/examples/hello.cpp b/subprojects/task/examples/hello.cpp new file mode 100644 index 000000000..bf98974f9 --- /dev/null +++ b/subprojects/task/examples/hello.cpp @@ -0,0 +1,16 @@ +// examples/hello.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +int main() { + return std::get<0>(ex::sync_wait([]() -> ex::task { + std::cout << "Hello, world!\n"; + co_return co_await ex::just(0); + }()) + .value_or(std::tuple(-1))); +} diff --git a/subprojects/task/examples/http-server.cpp b/subprojects/task/examples/http-server.cpp new file mode 100644 index 000000000..c91f57bb9 --- /dev/null +++ b/subprojects/task/examples/http-server.cpp @@ -0,0 +1,37 @@ +// examples/http-server.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; +namespace net = beman::net; + +// ---------------------------------------------------------------------------- + +namespace { +auto sched_spawn(auto&& scheduler, auto&& sender, auto&& token) { + return ex::spawn( + ex::write_env(std::forward(sender) | + ex::upon_error([](auto&&) noexcept { std::cout << "ERROR!\n"; }), + ex::detail::make_env(ex::get_scheduler, std::forward(scheduler))), + std::forward(token)); +} +} // namespace + +int main() { + net::io_context context{}; + ex::counting_scope scope{}; + + sched_spawn( + context.get_scheduler(), ex::just() | ex::then([]() noexcept { std::cout << "hello\n"; }), scope.get_token()); + sched_spawn( + context.get_scheduler(), + []() -> ex::task<> { + std::cout << "hello\n"; + co_return; + }(), + scope.get_token()); + ex::sync_wait(scope.join()); +} diff --git a/subprojects/task/examples/into_optional.cpp b/subprojects/task/examples/into_optional.cpp new file mode 100644 index 000000000..badfd2c7c --- /dev/null +++ b/subprojects/task/examples/into_optional.cpp @@ -0,0 +1,76 @@ +// examples/into_optional.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { +template +struct multi_sender { + using sender_concept = ex::sender_tag; + using completion_signatures = ex::completion_signatures; + + template + auto connect(Receiver&& receiver) const { + return ex::connect(ex::just(), std::forward(receiver)); + } +}; + +template +struct queue { + multi_sender async_pop() { return {}; } +}; + +template +auto to_optional(V& v) { + using result_type = decltype([] { + if constexpr (sizeof...(T) == 1u) + return std::optional{}; + else + return std::optional>{}; + }()); + return std::visit( + [](std::tuple& a) { + if constexpr (sizeof...(X) == 0u) + return result_type{}; + else if constexpr (sizeof...(X) == 1u) + return result_type(std::move(std::get<0>(a))); + else + return result_type(std::move(a)); + }, + v); +} + +template +auto my_into_optional(S&& s) { + return ex::into_variant(s) | ex::then([](auto&&...) { return std::optional(); }) +#if 0 + //-dk:TODO see if this code is needed or can be removed + | ex::then([]( + std::variant, std::tuple> r) { + if constexpr (sizeof...(A) != 0u) { + return to_optional(r); + } + else { + return to_optional(r); + } + }) +#endif + ; +} +} // namespace + +int main() { + queue que; + ex::sync_wait([](auto& q) -> ex::task<> { + // auto x = co_await ex::just(true) | into_optional; + [[maybe_unused]] std::optional x = co_await (q.async_pop() | ex::into_optional); + [[maybe_unused]] std::optional y = co_await ex::into_optional(q.async_pop()); + }(que)); +} diff --git a/subprojects/task/examples/issue-affine_on.cpp b/subprojects/task/examples/issue-affine_on.cpp new file mode 100644 index 000000000..b5894dd1d --- /dev/null +++ b/subprojects/task/examples/issue-affine_on.cpp @@ -0,0 +1,21 @@ +// examples/issue-affine_on.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +template +ex::task<> test(Sender&& sender) { + co_await std::move(sender); +} + +int main() { + ex::sync_wait(test(ex::just())); + ex::sync_wait(test(ex::read_env(ex::get_scheduler))); + ex::sync_wait(test(ex::read_env(ex::get_scheduler) | ex::let_value([](auto) noexcept { return ex::just(); }))); + ex::sync_wait(test(ex::read_env(ex::get_scheduler) | + ex::let_value([](auto sched) { return ex::starts_on(sched, ex::just()); }))); +} diff --git a/subprojects/task/examples/issue-frame-allocator.cpp b/subprojects/task/examples/issue-frame-allocator.cpp new file mode 100644 index 000000000..0c92b2cd6 --- /dev/null +++ b/subprojects/task/examples/issue-frame-allocator.cpp @@ -0,0 +1,26 @@ +// examples/issue-frame-allocator.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +template +ex::task test(int i, auto&&...) { + co_return co_await ex::just(i); +} +struct default_env {}; +struct allocator_env { + using allocator_type = std::pmr::polymorphic_allocator<>; +}; + +int main() { + ex::sync_wait(test(17)); // OK: no allocator + ex::sync_wait(test(17, std::allocator_arg, std::allocator())); // OK: allocator is convertible + // ex::sync_wait(test(17, std::allocator_arg, std::pmr::polymorphic_allocator<>())); // error + ex::sync_wait(test(17, std::allocator_arg, std::pmr::polymorphic_allocator<>())); // OK but unusual +} diff --git a/subprojects/task/examples/issue-start-reschedules.cpp b/subprojects/task/examples/issue-start-reschedules.cpp new file mode 100644 index 000000000..4d2a1e9ae --- /dev/null +++ b/subprojects/task/examples/issue-start-reschedules.cpp @@ -0,0 +1,36 @@ +// examples/issue-start-reschedules.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include "demo-thread_loop.hpp" +#include +#include + +namespace ex = beman::execution; + +ex::task<> test(auto sched) { + std::cout << "init =" << std::this_thread::get_id() << "\n"; + co_await ex::starts_on(sched, ex::just()); + // static_assert(std::same_as{}))>); + co_await ex::just(); + std::cout << "final=" << std::this_thread::get_id() << "\n"; +} + +int main() { + demo::thread_loop loop1; + demo::thread_loop loop2; + std::cout << "main =" << std::this_thread::get_id() << "\n"; + ex::sync_wait(ex::schedule(loop1.get_scheduler()) | + ex::then([] { std::cout << "loop1=" << std::this_thread::get_id() << "\n"; })); + ex::sync_wait(ex::schedule(loop2.get_scheduler()) | + ex::then([] { std::cout << "loop2=" << std::this_thread::get_id() << "\n"; })); + try { + std::cout << "--- use 1 ---\n"; + ex::sync_wait(test(loop2.get_scheduler())); + std::cout << "--- use 2 ---\n"; + // ex::sync_wait(ex::starts_on(loop1.get_scheduler(), test(loop2.get_scheduler()))); + } catch (...) { + } +} diff --git a/subprojects/task/examples/issue-symmetric-transfer.cpp b/subprojects/task/examples/issue-symmetric-transfer.cpp new file mode 100644 index 000000000..f6455cfeb --- /dev/null +++ b/subprojects/task/examples/issue-symmetric-transfer.cpp @@ -0,0 +1,29 @@ +// examples/issue-symmetric-transfer.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +template +ex::task test() { + for (std::size_t i{}; i < 1000000; ++i) { + co_await std::invoke([]() -> ex::task<> { co_await ex::when_all(ex::just()); }); + } +} + +struct affine_env {}; +struct inline_env { + using scheduler_type = ex::inline_scheduler; +}; + +int main() { + [[maybe_unused]] affine_env ae{}; + [[maybe_unused]] inline_env ie{}; +#ifndef _MSC_VER + ex::sync_wait(test()); // OK +#endif + // ex::sync_wait(test()); // error: stack overflow without symmetric transfer +} diff --git a/subprojects/task/examples/loop.cpp b/subprojects/task/examples/loop.cpp new file mode 100644 index 000000000..766e7af42 --- /dev/null +++ b/subprojects/task/examples/loop.cpp @@ -0,0 +1,97 @@ +// examples/loop.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { +struct run_loop_env { + using scheduler_type = decltype(std::declval().get_scheduler()); +}; + +template +struct log_scheduler { + using scheduler_concept = ex::scheduler_tag; + + std::remove_cvref_t scheduler; + + log_scheduler(auto&& sched) : scheduler(std::forward(sched)) {} + + struct sender { + using sender_concept = ex::sender_tag; + using completion_signatures = ex::completion_signatures; + using up_sender = decltype(ex::schedule(std::declval())); + + up_sender _sender; + + template + struct state { + using operation_state_concept = ex::operation_state_tag; + using up_state_t = decltype(ex::connect(std::declval(), std::declval())); + + up_state_t _state; + + template + state(S&& sender, R&& receiver) + : _state(ex::connect(std::forward(sender), std::forward(receiver))) {} + + auto start() & noexcept -> void { + std::cout << "start scheduler: " << &this->_state << "\n"; + ex::start(this->_state); + } + }; + + struct env { + log_scheduler _sched; + auto query(const ex::get_completion_scheduler_t&) const noexcept -> log_scheduler { + return this->_sched; + } + }; + auto get_env() const noexcept -> env { + return {ex::get_completion_scheduler(ex::get_env(this->_sender))}; + } + template + auto connect(Receiver&& receiver) noexcept -> state> { + return {this->_sender, std::forward(receiver)}; + } + }; + + auto schedule() noexcept { return sender{ex::schedule(this->scheduler)}; } + auto operator==(const log_scheduler&) const noexcept -> bool = default; +}; +template +log_scheduler(Scheduler&&) -> log_scheduler>; + +static_assert(ex::scheduler>); +static_assert(ex::scheduler>); +} // namespace + +namespace { +[[maybe_unused]] ex::task loop(auto count) { + for (decltype(count) i{}; i < count; ++i) + // co_await ex::just(i); + co_await [](int x) -> ex::task<> { + std::cout << "before co_await: " << &x << ": " << x << "\n"; + co_await (ex::just(x) | ex::then([](int v) { std::cout << &v << ": " << v << "\n"; })); + std::cout << "after co_await: " << &x << ": " << x << "\n"; + }(i); +} +} // namespace + +int main(int ac, char* av[]) { + auto count = 1 < ac && av[1] == std::string_view("run-it") ? 1000000 : 1000; + ex::sync_wait(loop(count)); + ex::sync_wait([](std::size_t count) -> ex::task { + co_await ex::write_env( + loop(count), + ex::detail::make_env(ex::get_scheduler, log_scheduler(co_await ex::read_env(ex::get_scheduler)))); + }(count)); +#if 0 + ex::sync_wait(ex::detail::write_env(loop(count), ex::detail::make_env(ex::get_scheduler, ex::inline_scheduler{}))); +#endif +} diff --git a/subprojects/task/examples/odd-completions.cpp b/subprojects/task/examples/odd-completions.cpp new file mode 100644 index 000000000..efa196a94 --- /dev/null +++ b/subprojects/task/examples/odd-completions.cpp @@ -0,0 +1,28 @@ +// examples/hello.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +int main() { + return std::get<0>(ex::sync_wait([]() -> ex::task { + std::cout << "Hello, world!\n"; + co_return co_await ex::just(0); + }()) + .value_or(std::tuple(-1))); + ex::sync_wait([](int value) -> ex::task { + switch (value) { + default: + co_return value; + case -1: + co_yield ex::with_error(std::make_exception_ptr(value)); + case 2: + throw value; + case 0: + co_await ex::just_stopped(); + } + }(0)); +} diff --git a/subprojects/task/examples/odd-return.cpp b/subprojects/task/examples/odd-return.cpp new file mode 100644 index 000000000..d3c2c2741 --- /dev/null +++ b/subprojects/task/examples/odd-return.cpp @@ -0,0 +1,20 @@ +// examples/odd-return.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +int main() { + auto mono = ex::sync_wait([]() -> ex::task { co_return std::monostate{}; }()); + assert(mono); + + auto exp = ex::sync_wait([]() -> ex::task { co_return std::make_exception_ptr(17); }()); + assert(exp); +} diff --git a/subprojects/task/examples/query.cpp b/subprojects/task/examples/query.cpp new file mode 100644 index 000000000..d0db8e188 --- /dev/null +++ b/subprojects/task/examples/query.cpp @@ -0,0 +1,64 @@ +// examples/query.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +constexpr struct get_value_t { + template + requires requires(const get_value_t& self, const Env& e) { e.query(self); } + decltype(auto) operator()(const Env& e) const { + return e.query(*this); + } + constexpr auto query(const ::beman::execution::forwarding_query_t&) const noexcept -> bool { return true; } +} get_value{}; + +struct simple_context { + int value{}; + int query(const get_value_t&) const noexcept { return this->value; } + explicit simple_context(auto&& env) + requires(not std::same_as>) + : value(get_value(env)) {} +}; + +struct context { + struct env_base { + env_base() = default; + env_base(const env_base&) = delete; + env_base(env_base&&) = delete; + virtual ~env_base() = default; + env_base& operator=(const env_base&) = delete; + env_base& operator=(env_base&&) = delete; + virtual int do_get_value() const = 0; + }; + template + struct env_type : env_base { + Env env; + explicit env_type(const Env& e) : env(e) {} + int do_get_value() const override { return get_value(env) + 3; } + }; + const env_base& env; + int query(const get_value_t&) const noexcept { return this->env.do_get_value(); } + explicit context(const env_base& own) : env(own) {} +}; + +int main() { + ex::sync_wait(ex::detail::write_env( + []() -> ex::task { + auto value(co_await ex::read_env(get_value)); + std::cout << "value=" << value << "\n"; + }(), + ex::detail::make_env(get_value, 42))); + ex::sync_wait(ex::detail::write_env( + []() -> ex::task { + auto value(co_await ex::read_env(get_value)); + std::cout << "value=" << value << "\n"; + }(), + ex::detail::make_env(get_value, 42))); +} diff --git a/subprojects/task/examples/result_example.cpp b/subprojects/task/examples/result_example.cpp new file mode 100644 index 000000000..fb52c7370 --- /dev/null +++ b/subprojects/task/examples/result_example.cpp @@ -0,0 +1,17 @@ +// examples/result_example.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +int main() { + ex::sync_wait([]() -> ex::task<> { + [[maybe_unused]] int result = co_await []() -> ex::task { co_return 42; }(); + assert(result == 42); + }()); +} diff --git a/subprojects/task/examples/rvalue-task.cpp b/subprojects/task/examples/rvalue-task.cpp new file mode 100644 index 000000000..47bd0678a --- /dev/null +++ b/subprojects/task/examples/rvalue-task.cpp @@ -0,0 +1,38 @@ +// examples/rvalue-task.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +namespace { +struct receiver { + using receiver_concept = ex::receiver_tag; + void set_value() && noexcept { std::cout << "set_value called\n"; } + void set_error(std::exception_ptr) && noexcept { std::cout << "set_error called\n"; } + void set_stopped() && noexcept { std::cout << "set_stopped called\n"; } + + struct env { + auto query(const ex::get_scheduler_t&) const noexcept -> ex::inline_scheduler { + return ex::inline_scheduler{}; + } + }; + env get_env() const noexcept { return {}; } +}; + +template +void test(T&& task) { + if (requires { ex::connect(std::forward(task), receiver{}); }) { + //[[maybe_unused]] auto op_state = ex::connect(std::forward(task), std::move(receiver{})); + } +} +} // namespace + +int main() { + auto task = []() -> ex::task> { co_return; }(); + test(std::move(task)); + // test(task); +} diff --git a/subprojects/task/examples/stop.cpp b/subprojects/task/examples/stop.cpp new file mode 100644 index 000000000..bc5d18378 --- /dev/null +++ b/subprojects/task/examples/stop.cpp @@ -0,0 +1,53 @@ +// examples/stop.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +namespace { +void unreachable([[maybe_unused]] const char* msg) { assert(nullptr == msg); } +} // namespace + +int main() { + try { + ex::stop_source source; + std::thread t([&source] { + using namespace std::chrono_literals; + std::this_thread::sleep_for(100ms); + source.request_stop(); + }); + + struct context { + struct xstop_source_type { // remove the x to disable the stop token + static constexpr bool stop_possible() { return false; } + static constexpr void request_stop() {} + static constexpr beman::execution::never_stop_token get_token() { return {}; } + }; + }; + + auto [result] = ex::sync_wait(ex::detail::write_env( + []() -> ex::task { + auto token(co_await ex::read_env(ex::get_stop_token)); + std::uint64_t count{}; + while (!token.stop_requested() && count != 200'000'000u) { + ++count; + } + co_return count; + }(), + ex::detail::make_env(ex::get_stop_token, source.get_token()))) + .value_or(std::uint64_t()); + std::cout << "result=" << result << "\n"; + + t.join(); + } catch (...) { + unreachable("an unexpected exception escaped to main"); + } +} diff --git a/subprojects/task/examples/task-sender.cpp b/subprojects/task/examples/task-sender.cpp new file mode 100644 index 000000000..f763bed6b --- /dev/null +++ b/subprojects/task/examples/task-sender.cpp @@ -0,0 +1,160 @@ +// examples/task_sender.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ex = beman::execution; + +void* operator new(std::size_t n) { + auto p = std::malloc(n); + std::cout << "global new(" << n << ")->" << p << "\n"; + return p; +} +void operator delete(void* ptr) noexcept { + std::cout << " global operator delete(" << ptr << ")\n"; + std::free(ptr); +} +void operator delete(void* ptr, std::size_t size) noexcept { + std::cout << " global operator delete(" << ptr << ", " << size << ")\n"; + std::free(ptr); +} + +template +struct defer_frame { + Mem mem; + Self self; + template + auto operator()(Arg&&... arg) const { + return ex::let_value(ex::read_env(ex::get_allocator), + [mem = this->mem, self = this->self, ... a = std::forward(arg)](auto alloc) { + return std::invoke(mem, self, std::allocator_arg, alloc, std::move(a)...); + }); + } + template + auto operator()(::std::allocator_arg_t, Alloc alloc, Arg&&... arg) const { + return ex::let_value(ex::just(alloc), + [&mem = this->mem, self = this->self, ... a = std::forward(arg)](auto alloc) { + return std::invoke(mem, self, std::allocator_arg, alloc, std::move(a)...); + }); + } + auto operator()(::std::allocator_arg_t) const = delete; +}; + +template +struct defer_frame { + Task task; + template + auto operator()(Arg&&... arg) const { + return ex::let_value(ex::read_env(ex::get_allocator), + [&task = this->task, ... a = std::forward(arg)](auto alloc) { + return std::invoke(task, std::allocator_arg, alloc, std::move(a)...); + }); + } + template + auto operator()(::std::allocator_arg_t, Alloc alloc, Arg&&... arg) const { + return ex::let_value(ex::just(alloc), [&task = this->task, ... a = std::forward(arg)](auto alloc) { + return std::invoke(task, std::allocator_arg, alloc, std::move(a)...); + }); + } + auto operator()(::std::allocator_arg_t) const = delete; +}; + +struct env { + using allocator_type = std::pmr::polymorphic_allocator; +}; + +auto lambda{[](int i, auto&&...) -> ex::task { + auto alloc = co_await ex::read_env(ex::get_allocator); + alloc.deallocate(alloc.allocate(1), 1); + std::cout << "lambda(" << i << ")\n"; + co_return; +}}; + +class example { + ex::task member_(std::allocator_arg_t, std::pmr::polymorphic_allocator, int); + ex::task const_member_(std::allocator_arg_t, std::pmr::polymorphic_allocator, int) const; + + public: + auto member(int i) { return defer_frame(&example::member_, this)(i); } + auto const_member(int i) { return defer_frame(&example::const_member_, this)(i); } +}; + +inline ex::task example::member_(std::allocator_arg_t, std::pmr::polymorphic_allocator, int i) { + std::cout << "example::member(" << i << ")\n"; + co_return; +} +inline ex::task +example::const_member_(std::allocator_arg_t, std::pmr::polymorphic_allocator, int i) const { + std::cout << "example::const member(" << i << ")\n"; + co_return; +} + +struct resource : std::pmr::memory_resource { + void* do_allocate(std::size_t n, std::size_t) override { + auto p{std::malloc(n)}; + std::cout << " resource::allocate(" << n << ")->" << p << "\n"; + return p; + } + void do_deallocate(void* p, std::size_t n, std::size_t) override { + std::cout << " resource::deallocate(" << p << ", " << n << ")\n"; + std::free(p); + } + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { return this == &other; } +}; + +int main() { + resource res{}; + std::pmr::polymorphic_allocator alloc(&res); + std::cout << "direct allocator use:\n"; + alloc.deallocate(alloc.allocate(1), 1); + std::cout << "write_env/just/then use:\n"; + ex::sync_wait(ex::write_env(ex::just(alloc) | ex::then([](auto a) { a.deallocate(a.allocate(1), 1); }), + ex::env{ex::prop{ex::get_allocator, alloc}})); + std::cout << "write_env/read_env/then use:\n"; + ex::sync_wait( + ex::write_env(ex::read_env(ex::get_allocator) | ex::then([](auto a) { a.deallocate(a.allocate(1), 1); }), + ex::env{ex::prop{ex::get_allocator, alloc}})); + std::cout << "write_env/let_value/then use:\n"; + ex::sync_wait(ex::write_env(ex::just() | ex::let_value([] { + return ex::read_env(ex::get_allocator) | + ex::then([](auto a) { a.deallocate(a.allocate(1), 1); }); + }), + ex::env{ex::prop{ex::get_allocator, alloc}})); + std::cout << "write_env/task<>:\n"; + ex::sync_wait(ex::write_env( + []() -> ex::task<> { + auto a = co_await ex::read_env(ex::get_allocator); + a.deallocate(a.allocate(1), 1); + }(), + ex::env{ex::prop{ex::get_allocator, alloc}})); + std::cout << "write_env/task:\n"; + ex::sync_wait(ex::write_env( + [](auto&&...) -> ex::task { + auto a = co_await ex::read_env(ex::get_allocator); + a.deallocate(a.allocate(1), 1); + }(std::allocator_arg, alloc), + ex::env{ex::prop{ex::get_allocator, alloc}})); + std::cout << "write_env/defer_frame>:\n"; + static constexpr defer_frame t0([](auto, auto, int i) -> ex::task { + std::cout << " i=" << i << "\n"; + auto a = co_await ex::read_env(ex::get_allocator); + a.deallocate(a.allocate(1), 1); + }); + ex::sync_wait(ex::write_env(t0(17), ex::env{ex::prop{ex::get_allocator, alloc}})); + std::cout << "write_env/temporary defer_frame>:\n"; + ex::sync_wait(ex::write_env(defer_frame([](auto, auto, auto i) -> ex::task { + std::cout << " i=" << i << "\n"; + co_await std::suspend_never{}; + auto a = co_await ex::read_env(ex::get_allocator); + a.deallocate(a.allocate(1), 1); + })(42), + ex::env{ex::prop{ex::get_allocator, alloc}})); + std::cout << "done\n"; +} diff --git a/subprojects/task/examples/task_scheduler.cpp b/subprojects/task/examples/task_scheduler.cpp new file mode 100644 index 000000000..c02f5ced1 --- /dev/null +++ b/subprojects/task/examples/task_scheduler.cpp @@ -0,0 +1,21 @@ +// examples/task_scheduler.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include +#include +#include +#include + +namespace ex = beman::execution; + +int main() { + ex::inline_scheduler isched; + const ex::inline_scheduler cisched; + ex::task_scheduler asched(isched); + const ex::task_scheduler casched(cisched); + + return asched == isched && isched == asched && asched == cisched && cisched == casched && !(cisched != casched) && + !(asched != isched) + ? EXIT_SUCCESS + : EXIT_FAILURE; +} diff --git a/subprojects/task/examples/tls-scheduler.cpp b/subprojects/task/examples/tls-scheduler.cpp new file mode 100644 index 000000000..eb8f5a2aa --- /dev/null +++ b/subprojects/task/examples/tls-scheduler.cpp @@ -0,0 +1,60 @@ +// examples/hello.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include "demo-tls_scheduler.hpp" +#include +#include +#include +#include + +namespace ex = beman::execution; + +// ---------------------------------------------------------------------------- + +class tls_data { + static thread_local std::string value; + + public: + static std::string get() { return tls_data::value; } + static void set(std::string_view v) { tls_data::value = v; } +}; + +thread_local std::string tls_data::value{}; + +struct tls_save { + std::optional value; + void save() { this->value = tls_data::get(); } + void restore() { tls_data::set(*this->value); } +}; + +struct tls_env { + using scheduler_type = demo::tls_scheduler; + // using scheduler_type = ex::inline_scheduler; +}; + +ex::task run_timer(std::string name) { + tls_data::set(name); + + for (int i = 0; i != 3; ++i) { + co_await ex::just(); + std::cout << "data=" << tls_data::get() << "\n"; + } +} + +int main() { + ex::counting_scope scope; + ex::sync_wait([](auto& scope) -> ex::task { + auto scheduler = co_await ex::read_env(ex::get_scheduler); + + ex::spawn(ex::write_env(run_timer("timer 1") | ex::upon_error([](auto&&) noexcept {}) | + ex::then([]() noexcept { std::cout << "loop done\n"; }), + ex::detail::make_env(ex::get_scheduler, scheduler)), + scope.get_token()); + ex::spawn(ex::write_env(run_timer("timer 2") | ex::upon_error([](auto&&) noexcept {}) | + ex::then([]() noexcept { std::cout << "loop done\n"; }), + ex::detail::make_env(ex::get_scheduler, scheduler)), + scope.get_token()); + co_return; + }(scope)); + ex::sync_wait(scope.join()); +} diff --git a/subprojects/task/include/beman/execution/task.hpp b/subprojects/task/include/beman/execution/task.hpp new file mode 100644 index 000000000..8a8f49027 --- /dev/null +++ b/subprojects/task/include/beman/execution/task.hpp @@ -0,0 +1,13 @@ +// include/beman/execution/task.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_EXECUTION_TASK +#define INCLUDED_INCLUDE_BEMAN_EXECUTION_TASK + +// ---------------------------------------------------------------------------- + +#include + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/lazy/lazy.hpp b/subprojects/task/include/beman/lazy/lazy.hpp new file mode 100644 index 000000000..9a89462ad --- /dev/null +++ b/subprojects/task/include/beman/lazy/lazy.hpp @@ -0,0 +1,24 @@ +// include/beman/lazy/lazy.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_LAZY_LAZY +#define INCLUDED_INCLUDE_BEMAN_LAZY_LAZY + +#include + +// ---------------------------------------------------------------------------- + +namespace beman::lazy { +template +using lazy = ::beman::task::detail::task; +} + +namespace beman::execution { +template +using lazy [[deprecated("beman::execution::lazy has been renamed to beman::execution::task")]] = + ::beman::execution::task; +} + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/allocator_of.hpp b/subprojects/task/include/beman/task/detail/allocator_of.hpp new file mode 100644 index 000000000..e319c18ea --- /dev/null +++ b/subprojects/task/include/beman/task/detail/allocator_of.hpp @@ -0,0 +1,38 @@ +// include/beman/task/detail/allocator_of.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_ALLOCATOR_OF +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_ALLOCATOR_OF + +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +/*! + * \brief Utility to get an allocator type from a context + * \headerfile beman/task/task.hpp + * \internal + */ +template +struct allocator_of { + using type = std::allocator; +}; +template + requires requires { typename Context::allocator_type; } +struct allocator_of { + using type = typename Context::allocator_type; + static_assert( + requires(type& a, std::size_t s, std::byte* ptr) { + { a.allocate(s) } -> std::same_as; + a.deallocate(ptr, s); + }, "The allocator_type needs to be an allocator of std::byte"); +}; +template +using allocator_of_t = typename allocator_of::type; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/allocator_support.hpp b/subprojects/task/include/beman/task/detail/allocator_support.hpp new file mode 100644 index 000000000..a3d397992 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/allocator_support.hpp @@ -0,0 +1,87 @@ +// include/beman/task/detail/allocator_support.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_BEMAN_TASK_DETAIL_ALLOCATOR_SUPPORT +#define INCLUDED_BEMAN_TASK_DETAIL_ALLOCATOR_SUPPORT + +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +/*! + * \brief Utility adding allocator support to type by embedding the allocator + * \headerfile beman/task/task.hpp + * + * To add allocator support using this class just publicly inherit from + * allocator_support. This utility is probably + * only useful for coroutine promise types. + * + * This struct is a massive hack, primarily support allocators for coroutines. + * The memory for coroutines is implicitly managed and there isn't a way to + * provide the memory directly. Instead, the promise_type can overload an + * operator new and somehow determine an allocator based on the arguments + * passed to the coroutine. Even worse, the operator delete only gets passed + * a pointer to delete and a size. To determine the correct allocator the + * operator delete needs to located it based on this information. Putting + * the allocator after actually used memory causes the address sanitizer to + * object! So, the current strategy is to embed space for the allocator + * into the object and pull it out from there. + */ +template +struct allocator_support { + using allocator_traits = std::allocator_traits; + + static std::size_t offset(std::size_t size) { + return (size + alignof(Allocator) - 1u) & ~(alignof(Allocator) - 1u); + } + static Allocator* get_allocator(void* ptr, std::size_t size) { + ptr = static_cast(ptr) + offset(size); + return ::std::launder(reinterpret_cast(ptr)); + } + + template + static void* operator new(std::size_t size, [[maybe_unused]] A&&... a) { + if constexpr (::std::same_as>) { + Allocator alloc{}; + return allocator_traits::allocate(alloc, size); + } else { + Allocator alloc{::beman::task::detail::find_allocator(a...)}; + void* ptr{allocator_traits::allocate(alloc, allocator_support::offset(size) + sizeof(Allocator))}; + try { + new (allocator_support::get_allocator(ptr, size)) Allocator(alloc); + } catch (...) { + allocator_traits::deallocate( + alloc, static_cast(ptr), allocator_support::offset(size) + sizeof(Allocator)); + throw; + } + return ptr; + } + } + template + static void operator delete(void* ptr, std::size_t size, const A&...) { + allocator_support::operator delete(ptr, size); + } + static void operator delete(void* ptr, std::size_t size) { + if constexpr (::std::same_as>) { + Allocator alloc{}; + allocator_traits::deallocate(alloc, static_cast(ptr), size); + } else { + Allocator* aptr{allocator_support::get_allocator(ptr, size)}; + Allocator alloc{*aptr}; + aptr->~Allocator(); + allocator_traits::deallocate( + alloc, static_cast(ptr), allocator_support::offset(size) + sizeof(Allocator)); + } + } +}; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/awaiter.hpp b/subprojects/task/include/beman/task/detail/awaiter.hpp new file mode 100644 index 000000000..d69efae9b --- /dev/null +++ b/subprojects/task/include/beman/task/detail/awaiter.hpp @@ -0,0 +1,116 @@ +// include/beman/task/detail/awaiter.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_AWAITER +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_AWAITER + +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +template +struct awaiter_scheduler_receiver { + using receiver_concept = ::beman::execution::receiver_tag; + Awaiter* aw; + auto set_value(auto&&...) noexcept { this->aw->actual_complete().resume(); } + auto set_error(auto&&) noexcept { this->aw->actual_complete().resume(); } + auto set_stopped() noexcept { this->aw->actual_complete().resume(); } +}; + +template +struct awaiter_op_t { + using state_type = + decltype(::beman::execution::connect(::beman::execution::schedule(::beman::execution::get_scheduler( + ::beman::execution::get_env(::std::declval()))), + ::std::declval>())); + + awaiter_op_t(const ParentPromise& p, Awaiter* aw) + : state(::beman::execution::connect( + ::beman::execution::schedule(beman::execution::get_scheduler(::beman::execution::get_env(p))), + awaiter_scheduler_receiver{aw})) {} + state_type state; + auto start() noexcept -> void { ::beman::execution::start(this->state); } +}; +template +struct awaiter_op_t { + awaiter_op_t(const ParentPromise&, Awaiter*) noexcept {} + auto start() noexcept -> void {} +}; + +template +class awaiter : public ::beman::task::detail::state_base { + public: + using allocator_type = typename ::beman::task::detail::state_base::allocator_type; + using stop_token_type = typename ::beman::task::detail::state_base::stop_token_type; + using scheduler_type = typename ::beman::task::detail::state_base::scheduler_type; + + explicit awaiter(::beman::task::detail::handle h) : handle(std::move(h)) {} + constexpr auto await_ready() const noexcept -> bool { return false; } + struct env_receiver { + ParentPromise* parent; + auto get_env() const noexcept { return parent->get_env(); } + }; + auto await_suspend(::std::coroutine_handle parent) noexcept { + this->state_rep.emplace(env_receiver{&parent.promise()}); + this->scheduler.emplace( + this->template from_env(::beman::execution::get_env(parent.promise()))); + this->parent = ::std::move(parent); + return this->handle.start(this); + } + auto await_resume() { return this->result_resume(); } + + private: + friend struct awaiter_scheduler_receiver; + auto do_complete() -> std::coroutine_handle<> override { + assert(this->parent); + assert(this->scheduler); + if constexpr (requires { + *this->scheduler != + ::beman::execution::get_scheduler(::beman::execution::get_env(this->parent.promise())); + }) { + if (*this->scheduler != + ::beman::execution::get_scheduler(::beman::execution::get_env(this->parent.promise()))) { + this->reschedule.emplace(this->parent.promise(), this); + this->reschedule->start(); + return ::std::noop_coroutine(); + } + } + return this->actual_complete(); + } + auto actual_complete() -> std::coroutine_handle<> { + return this->no_completion_set() ? this->parent.promise().unhandled_stopped() : ::std::move(this->parent); + } + auto do_get_allocator() -> allocator_type override { + if constexpr (requires { + ::beman::execution::get_allocator(::beman::execution::get_env(this->parent.promise())); + }) + return ::beman::execution::get_allocator(::beman::execution::get_env(this->parent.promise())); + else + return allocator_type{}; + } + auto do_get_scheduler() -> scheduler_type override { return *this->scheduler; } + auto do_set_scheduler(scheduler_type other) -> scheduler_type override { + return ::std::exchange(*this->scheduler, other); + } + auto do_get_stop_token() -> stop_token_type override { return {}; } + auto do_get_environment() -> Env& override { return this->state_rep->context; } + + ::beman::task::detail::handle handle; + ::std::optional<::beman::task::detail::state_rep> state_rep; + ::std::optional scheduler; + ::std::coroutine_handle parent{}; + ::std::optional> reschedule{}; +}; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/change_coroutine_scheduler.hpp b/subprojects/task/include/beman/task/detail/change_coroutine_scheduler.hpp new file mode 100644 index 000000000..ad35b6fa3 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/change_coroutine_scheduler.hpp @@ -0,0 +1,37 @@ +// include/beman/task/detail/change_coroutine_scheduler.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_CHANGE_COROUTINE_SCHEDULER +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_CHANGE_COROUTINE_SCHEDULER + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +template <::beman::execution::scheduler Scheduler> +struct change_coroutine_scheduler { + using type = Scheduler; + type scheduler; + + template <::beman::execution::scheduler S> + change_coroutine_scheduler(S&& s) : scheduler(::std::forward(s)) {} + + constexpr bool await_ready() const { return false; } + template + bool await_suspend(::std::coroutine_handle

h) { + this->scheduler = h.promise().change_scheduler(std::move(this->scheduler)); + return false; + } + type await_resume() { return this->scheduler; } +}; +template <::beman::execution::scheduler S> +change_coroutine_scheduler(S&&) -> change_coroutine_scheduler<::beman::task::detail::task_scheduler>; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/completion.hpp b/subprojects/task/include/beman/task/detail/completion.hpp new file mode 100644 index 000000000..28340e83e --- /dev/null +++ b/subprojects/task/include/beman/task/detail/completion.hpp @@ -0,0 +1,28 @@ +// include/beman/task/detail/completion.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_COMPLETION +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_COMPLETION + +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +template +struct completion { + using type = ::beman::execution::set_value_t(R); +}; +template <> +struct completion { + using type = ::beman::execution::set_value_t(); +}; + +template +using completion_t = typename beman::task::detail::completion::type; + +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/error_types_of.hpp b/subprojects/task/include/beman/task/detail/error_types_of.hpp new file mode 100644 index 000000000..b2fb585b6 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/error_types_of.hpp @@ -0,0 +1,28 @@ +// include/beman/task/detail/error_types_of.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_ERROR_TYPES_OF +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_ERROR_TYPES_OF + +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +template +struct error_types_of { + using type = ::beman::execution::completion_signatures<::beman::execution::set_error_t(::std::exception_ptr)>; +}; +template + requires requires { typename Context::error_types; } +struct error_types_of { + using type = typename Context::error_types; +}; +template +using error_types_of_t = typename error_types_of::type; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/final_awaiter.hpp b/subprojects/task/include/beman/task/detail/final_awaiter.hpp new file mode 100644 index 000000000..861ed933f --- /dev/null +++ b/subprojects/task/include/beman/task/detail/final_awaiter.hpp @@ -0,0 +1,27 @@ +// include/beman/task/detail/final_awaiter.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_FINAL_AWAITER +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_FINAL_AWAITER + +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { + +struct final_awaiter { + static constexpr auto await_ready() noexcept -> bool { return false; } + template + static auto await_suspend(std::coroutine_handle handle) noexcept { + return handle.promise().notify_complete(); + } + static constexpr void await_resume() noexcept {} +}; + +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/find_allocator.hpp b/subprojects/task/include/beman/task/detail/find_allocator.hpp new file mode 100644 index 000000000..0b2120857 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/find_allocator.hpp @@ -0,0 +1,46 @@ +// include/beman/task/detail/find_allocator.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_FIND_ALLOCATOR +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_FIND_ALLOCATOR + +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +/*! + * \brief Utility locating an allocator_arg/allocator pair + * \headerfile beman/task/task.hpp + * \internal + */ +template +Allocator find_allocator() { + return Allocator(); +} +template +Allocator find_allocator(const std::allocator_arg_t&) { + static_assert( + requires { + { Allocator() } -> std::same_as; + }, "There needs to be an allocator argument following std::allocator_arg"); + return Allocator(); +} + +template +Allocator find_allocator(const std::allocator_arg_t&, const Alloc& alloc, const A&...) { + static_assert( + requires(const Alloc& a) { Allocator(a); }, + "The allocator needs to be constructible from the argument following std::allocator"); + return Allocator(alloc); +} +template +Allocator find_allocator(A0 const&, const A&... a) { + return ::beman::task::detail::find_allocator(a...); +} + +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/handle.hpp b/subprojects/task/include/beman/task/detail/handle.hpp new file mode 100644 index 000000000..b1d9471c7 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/handle.hpp @@ -0,0 +1,48 @@ +// include/beman/task/detail/handle.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_HANDLE +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_HANDLE + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +template +class handle { + private: + struct deleter { + auto operator()(P* p) noexcept -> void { + if (p) { + std::coroutine_handle

::from_promise(*p).destroy(); + } + } + }; + std::unique_ptr h; + + public: + explicit handle(P* p) : h(p) {} + auto reset() -> void { this->h.reset(); } + template + auto start(A&&... a) noexcept -> auto { + return this->h->start(::std::forward(a)...); + } + auto release() -> ::std::coroutine_handle

{ + return ::std::coroutine_handle

::from_promise(*this->h.release()); + } + P* get() const noexcept { return this->h.get(); } + auto get_env() const noexcept { + assert(this->h.get()); + return ::beman::execution::get_env(*this->h); + } +}; + +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/infallible_scheduler.hpp b/subprojects/task/include/beman/task/detail/infallible_scheduler.hpp new file mode 100644 index 000000000..40c171642 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/infallible_scheduler.hpp @@ -0,0 +1,33 @@ +// include/beman/task/detail/infallible_scheduler.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_INFALLIBLE_SCHEDULER +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_INFALLIBLE_SCHEDULER + +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +template +concept completes_with = + ::std::same_as<::beman::execution::completion_signatures, + ::beman::execution:: + completion_signatures_of_t())), Env>>; + +template +concept infallible_scheduler = + (::beman::execution::scheduler) && + (::beman::task::detail::completes_with || + (!::beman::execution::unstoppable_token<::beman::execution::stop_token_of_t> && + (::beman::task::detail:: + completes_with || + ::beman::task::detail:: + completes_with))); +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/into_optional.hpp b/subprojects/task/include/beman/task/detail/into_optional.hpp new file mode 100644 index 000000000..7bef831ca --- /dev/null +++ b/subprojects/task/include/beman/task/detail/into_optional.hpp @@ -0,0 +1,100 @@ +// include/beman/task/detail/into_optional.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_BEMAN_TASK_DETAIL_INTO_OPTIONAL +#define INCLUDED_BEMAN_TASK_DETAIL_INTO_OPTIONAL + +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { + +inline constexpr struct into_optional_t : beman::execution::sender_adaptor_closure { + template <::beman::execution::sender Upstream> + struct sender { + using upstream_t = std::remove_cvref_t; + using sender_concept = ::beman::execution::sender_tag; + upstream_t upstream; + + template + struct type_list {}; + + template + static auto find_type(type_list>) { + return std::optional{}; + } + template + static auto find_type(type_list, type_list<>>) { + return std::optional{}; + } + template + static auto find_type(type_list, type_list>) { + return std::optional{}; + } + template + static auto find_type(type_list>) { + return std::optional>{}; + } + template + static auto find_type(type_list, type_list<>>) { + return std::optional>{}; + } + template + static auto find_type(type_list, type_list>) { + return std::optional>{}; + } + + template + static auto get_type(Env&&) { + return find_type( + ::beman::execution::value_types_of_t, type_list, type_list>()); + } + + template + constexpr auto make_signatures(auto&& env, type_list, type_list) const { + return ::beman::execution::completion_signatures<::beman::execution::set_value_t( + decltype(this->get_type(env))), + ::beman::execution::set_error_t(E)..., + S...>(); + } + template + auto get_completion_signatures(Env&& env) const { + return make_signatures( + env, + ::beman::execution::error_types_of_t, type_list>{}, + std::conditional_t<::beman::execution::sends_stopped>, + type_list<::beman::execution::set_stopped_t()>, + type_list<>>{}); + } + template + struct make_object { + template + auto operator()(A&&... a) const + -> decltype(get_type(::beman::execution::get_env(std::declval()))) { + if constexpr (sizeof...(A) == 0u) + return {}; + else + return {std::forward(a)...}; + } + }; + template + auto connect(Receiver&& receiver) && { + return ::beman::execution::connect( + ::beman::execution::then(std::move(this->upstream), make_object{}), + std::forward(receiver)); + } + }; + + template + sender operator()(Upstream&& upstream) const { + return {std::forward(upstream)}; + } +} into_optional{}; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/logger.hpp b/subprojects/task/include/beman/task/detail/logger.hpp new file mode 100644 index 000000000..465494088 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/logger.hpp @@ -0,0 +1,40 @@ +// include/beman/task/detail/logger.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_LOGGER +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_LOGGER + +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +struct logger { + static auto level(int i) -> int { + static int rc{}; + return rc += i; + } + ::std::ostream& log(const char* pre, const char* msg) { + ::std::fill_n(::std::ostreambuf_iterator(std::cout.rdbuf()), level(0), ' '); + std::cout << pre; + return std::cout << msg << "\n"; + } + ::std::ostream& log(const char* msg) { return log("| ", msg); } + const char* msg; + explicit logger(const char* m) : msg(m) { + level(1); + log("\\ ", this->msg); + } + logger(logger&&) = delete; + ~logger() { + log("/ ", this->msg); + level(-1); + } +}; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/poly.hpp b/subprojects/task/include/beman/task/detail/poly.hpp new file mode 100644 index 000000000..a48f92f75 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/poly.hpp @@ -0,0 +1,78 @@ +// include/beman/task/detail/poly.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_BEMAN_TASK_DETAIL_POLY +#define INCLUDED_BEMAN_TASK_DETAIL_POLY + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +/*! + * \brief Utility providing small object optimization and type erasure. + * \headerfile beman/task/task.hpp + * \internal + */ +template +class alignas(sizeof(double)) poly { + private: + std::array buf{}; + + Base* pointer() { return static_cast(static_cast(buf.data())); } + const Base* pointer() const { return static_cast(static_cast(buf.data())); } + + public: + template + requires(sizeof(T) <= Size) + poly(T*, Args&&... args) { + new (this->buf.data()) T(::std::forward(args)...); + static_assert(sizeof(T) <= Size); + } + poly(poly&& other) + requires requires(Base* b, void* t) { b->move(t); } + { + other.pointer()->move(this->buf.data()); + } + poly& operator=(poly&& other) + requires requires(Base* b, void* t) { b->move(t); } + { + if (this != &other) { + this->pointer()->~Base(); + other.pointer()->move(this->buf.data()); + } + return *this; + } + poly& operator=(const poly& other) + requires requires(Base* b, void* t) { b->clone(t); } + { + if (this != &other) { + this->pointer()->~Base(); + other.pointer()->clone(this->buf.data()); + } + return *this; + } + poly(const poly& other) + requires requires(Base* b, void* t) { b->clone(t); } + { + other.pointer()->clone(this->buf.data()); + } + ~poly() { this->pointer()->~Base(); } + bool operator==(const poly& other) const + requires requires(const Base& b) { + { b.equals(&b) } -> std::same_as; + } + { + return other.pointer()->equals(this->pointer()); + } + Base* operator->() { return this->pointer(); } + const Base* operator->() const { return this->pointer(); } +}; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/promise_base.hpp b/subprojects/task/include/beman/task/detail/promise_base.hpp new file mode 100644 index 000000000..787577bec --- /dev/null +++ b/subprojects/task/include/beman/task/detail/promise_base.hpp @@ -0,0 +1,61 @@ +// include/beman/task/detail/promise_base.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_PROMISE_BASE +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_PROMISE_BASE + +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +/* + * \brief Helper base class dealing with void vs. value results. + * \headerfile beman/task/task.hpp + * \internal + */ +template <::beman::task::detail::stoppable Stop, typename Value, typename Environment> +class promise_base { + public: + /* + * \brief Set the value result. + * \internal + */ + template + void return_value(T&& value) { + this->get_state()->set_value(::std::forward(value)); + } + + public: + auto set_state(::beman::task::detail::state_base* s) noexcept -> void { this->state_ = s; } + auto get_state() const noexcept -> ::beman::task::detail::state_base* { return this->state_; } + + private: + ::beman::task::detail::state_base* state_{}; +}; + +template +class promise_base { + public: + /* + * \brief Set the value result although without any value. + */ + void return_void() { this->get_state()->set_value(void_type{}); } + + public: + auto set_state(::beman::task::detail::state_base* s) noexcept -> void { this->state_ = s; } + auto get_state() const noexcept -> ::beman::task::detail::state_base* { return this->state_; } + + private: + ::beman::task::detail::state_base* state_{}; +}; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/promise_env.hpp b/subprojects/task/include/beman/task/detail/promise_env.hpp new file mode 100644 index 000000000..41f8b6870 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/promise_env.hpp @@ -0,0 +1,40 @@ +// include/beman/task/detail/promise_env.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_PROMISE_ENV +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_PROMISE_ENV + +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +template +struct promise_env { + const Promise* promise; + + auto query(const ::beman::execution::get_scheduler_t&) const noexcept -> typename Promise::scheduler_type { + return this->promise->get_scheduler(); + } + auto query(const ::beman::execution::get_allocator_t&) const noexcept -> typename Promise::allocator_type { + return this->promise->get_allocator(); + } + auto query(const ::beman::execution::get_stop_token_t&) const noexcept -> typename Promise::stop_token_type { + return this->promise->get_stop_token(); + } + + template + requires requires(const Promise* p, Q q, A&&... a) { + ::beman::execution::forwarding_query(q); + q(p->get_environment(), std::forward(a)...); + } + auto query(Q q, A&&... a) const noexcept { + return q(promise->get_environment(), std::forward(a)...); + } +}; +} // namespace beman::task::detail + +// ---------------------------------------------------------------------------- + +#endif diff --git a/subprojects/task/include/beman/task/detail/promise_type.hpp b/subprojects/task/include/beman/task/detail/promise_type.hpp new file mode 100644 index 000000000..987e22f06 --- /dev/null +++ b/subprojects/task/include/beman/task/detail/promise_type.hpp @@ -0,0 +1,122 @@ +// include/beman/task/detail/promise_type.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_PROMISE_TYPE +#define INCLUDED_INCLUDE_BEMAN_TASK_DETAIL_PROMISE_TYPE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::task::detail { +namespace meta { +template +struct list_contains; +template