Skip to content

Create a stdexec module#2138

Open
ispeters wants to merge 4 commits into
NVIDIA:mainfrom
ispeters:modularize
Open

Create a stdexec module#2138
ispeters wants to merge 4 commits into
NVIDIA:mainfrom
ispeters:modularize

Conversation

@ispeters

Copy link
Copy Markdown
Contributor

Summary

This diff starts the process of modularizing stdexec so that consumers can say

import stdexec;

rather than

#include <stdexec/execution.hpp>

The initial module exports nothing except stdexec::just_t so there's a test that confirms consumers can utter stdexec::just_t:

TEST_CASE("I can use ex::just in a modules build", "[modules]")
{
  STATIC_REQUIRE(sizeof(ex::just_t) > 0);
}

It builds and passes on my machine.

I've tried to add the above test to CI with the idea that, if this PR is going in the right direction and we merge it with an incomplete module then at least other changes won't break the modularization effort.

Strategy

I'm aping the conditional modules support that the Beman Project's exemplar project scaffolds for new repos. As I understand it, the idea is:

  • There's one main module per "library". I expect it would be useful for stdexec to have stdexec for the contents of execution.hpp and something like stdexec.exec or stdexec.experimental for everything in the exec namespace. This diff introduces only stdexec and it's defined in a new file named modules/stdexec.cppm.
  • Every header has to support being included in both a modules and non-modules context. This diff makes that true for enough headers that the test compiles and runs; I doubt it's all headers.
  • The module definition does a few things:
    • Include non-modularized headers (e.g. std headers that export macros) into the Global Module Fragment (GMF).
    • Declare the module we're exporting
    • #define STDEXEC_IN_MODULE_PURVIEW, which is the signal we're in the "we're defining the module" mode.
    • #include <stdexec/execution.hpp>
  • With the foregoing setup, all headers can now distinguish between:
    • Being included in a traditional non-modules setup (STDEXEC_USE_MODULES() is false)
    • Being included in the module definition (STDEXEC_USE_MODULES() is true and STDEXEC_IN_MODULE_PURVIEW is defined)
    • Being included with modules enabled but not in the module definition (STDEXEC_USE_MODULES() is true and STDEXEC_IN_MODULE_PURVIEW is not defined)
  • If modules are disabled or we're defining the module then a header needs to behave like a traditional header and declare and/or define its symbols.
    • In the case we're defining the module, std includes should not be included because the standard library will already have been imported.
  • If modules are enabled and we're not defining the module then a header should just import stdexec.
  • Throughout the library's headers, certain symbols should be exported from the module. To do this, prefix it with STDEXEC_MODULE_EXPORT; when the header is being included to define the module, that macro will expand to export otherwise it's the empty token otherwise.

Acknowledgements

Claude Sonnet 5 helped me come up with the implementation strategy: https://claude.ai/share/0b35126c-294a-4f5d-844a-3b636381caa8, and @ednolan taught me about The Beman Project's CMake and modules infrastructure.

ispeters added 4 commits July 24, 2026 09:44
This diff doesn't build, but it doesn't build for a good reason: it's
successfully invoking the "build with modules" machinery, which is
exposing implementation gaps.

It looks like the next step is to start updating `stdexec` headers that
include std headers to conditionally rely on `import std` instead, at
least when `STDEXEC_IN_MODULE_PURVIEW` is defined.
This diff doesn't build for a very exciting reson: the new modules-only
test shows that `stdexec::just_t` hasn't been exported from the module.
This diff makes both the modules and non-modules builds pass.

The tests pass in both builds, too!
@copy-pr-bot

copy-pr-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@ednolan

ednolan commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Looks good! Thoughts:

  1. This doesn't solve the hard problems that Claude flagged to me such as:

In stdexec, __sexpr — the type of every sender expression (just, let_value, and the sender sendosio::read returns) — is wrapped in
an unnamed namespace (__basic_sender.hpp:344-441), and its template argument is a lambda appearing in a default template argument (STDEXEC_SEXPR_DESCRIPTOR_FN, line 50). Both constructs produce a distinct type per translation unit. That's an intentional stdexec design
choice (it turns silent cross-TU ODR violations into hard errors), and it's fine when everything is header-only — but it's poison for the exemplar model, where stdexec gets included in the global module fragment of sendosio.cppm and its types leak into the module's
exported API.
But from the commit message, that seems intentional, and the idea is to use this as a starting point from which you can go through and fix those problems separately.

  1. In the exemplar-style Beman libraries (which not every Beman library currently uses), instead of a mechanism like STDEXEC_MODULE_EXPORT, we just give up on granular exporting and do what would be analogous to:
export {
#include <stdexec/execution.hpp>
}

Either approach is fine; the export-everything approach is less work but is sloppier. I imagine that your Claude may have come up with a reason to prefer the STDEXEC_MODULE_EXPORT approach.

  1. For this part:
# TODO: this is broken in the modules build
if(STDEXEC_INSTALL)

The place where this logic lives in the Beman project is this file, if you want to refer to it in the future to try to adapt its logic.

@ispeters

ispeters commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Looks good! Thoughts:

Thanks for looking!

  1. This doesn't solve the hard problems that Claude flagged to me such as:

In stdexec, __sexpr — the type of every sender expression (just, let_value, and the sender sendosio::read returns) — is wrapped in
an unnamed namespace (__basic_sender.hpp:344-441), and its template argument is a lambda appearing in a default template argument (STDEXEC_SEXPR_DESCRIPTOR_FN, line 50). Both constructs produce a distinct type per translation unit. That's an intentional stdexec design
choice (it turns silent cross-TU ODR violations into hard errors), and it's fine when everything is header-only — but it's poison for the exemplar model, where stdexec gets included in the global module fragment of sendosio.cppm and its types leak into the module's
exported API.

But from the commit message, that seems intentional, and the idea is to use this as a starting point from which you can go through and fix those problems separately.

Yeah, this is intentional. I don't think modules support is a high-priority need for stdexec so I want @ericniebler to have a chance to weigh in on the design, direction, etc. before I invest a whole lot of time in this. Once we agree on a plan, I'll extend things to support more of the library. Regarding that specific issue that Claude raised, I'm hoping that by defining the module from within the stdexec project rather than trying to create one non-invasively from outside the problems will be smaller. As it is, the stdexec patch you shared with me that did things like name previously-anonymous types and namespaces isn't necessary in this diff. I'm hoping that's a sign that it's OK for stdexec.cppm to define and use things with static linkage as long as those things are not exported. We'll see.

  1. In the exemplar-style Beman libraries (which not every Beman library currently uses), instead of a mechanism like STDEXEC_MODULE_EXPORT, we just give up on granular exporting and do what would be analogous to:
export {
#include <stdexec/execution.hpp>
}

Either approach is fine; the export-everything approach is less work but is sloppier. I imagine that your Claude may have come up with a reason to prefer the STDEXEC_MODULE_EXPORT approach.

I came up with the idea to try this approach in hopes that it would allow me to keep the various anonymous things in stdexec anonymous as long as they remain reachable-but-not-visible (I think that's the modules term for things declared in the module definition unit but not exported from it). When I asked Claude if it was a viable strategy, it claimed that it's the strategy Microsoft took in modularizing their stdlib, which I take as a good sign.

  1. For this part:
# TODO: this is broken in the modules build
if(STDEXEC_INSTALL)

The place where this logic lives in the Beman project is this file, if you want to refer to it in the future to try to adapt its logic.

Thanks! I'll take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants