Skip to content

Use partitions on Poco module to simplify module API#5268

Open
mikomikotaishi wants to merge 6 commits intopocoproject:mainfrom
mikomikotaishi:no-macro-guard-modules
Open

Use partitions on Poco module to simplify module API#5268
mikomikotaishi wants to merge 6 commits intopocoproject:mainfrom
mikomikotaishi:no-macro-guard-modules

Conversation

@mikomikotaishi
Copy link
Copy Markdown
Contributor

@mikomikotaishi mikomikotaishi commented Mar 25, 2026

This pull request makes the Poco module only consist of partitions. This removes all Poco.* modules and turns them to Poco:*, so that only one module is created rather than several. This should simplify the API and make it far simpler to users (so there is no need to decide between import Poco; or import Poco.Foundation;, etc.)

@matejk
Copy link
Copy Markdown
Contributor

matejk commented Mar 26, 2026

Thanks for the PR! I like the goal of simplifying the module API, but I have a concern about how this interacts with Poco's optional component architecture.

The problem: C++20 module partitions (:Foo) must all be compiled as part of their parent module. By moving all components to partitions and unconditionally listing all .cppm files in CMakeLists.txt under if(ENABLE_FOUNDATION), every component's partition file gets compiled even when that component is disabled (e.g. ENABLE_CRYPTO=OFF). The #ifdef guards inside each .cppm make the exported namespaces empty, but the partition source files are still compiled and linked. This breaks minimal/selective builds — a core Poco feature where users choose exactly which components to enable.

The previous design — separate modules (Poco.Foundation, Poco.Crypto, etc.) with conditional export import in Poco.cppm — correctly models the optional-component architecture because .cppm files are only added to the build when their component is enabled.

Could you share more about the use case driving this? If the main goal is removing the #ifdef boilerplate from Poco.cppm, there's an approach that achieves that without breaking optional components:

Recommended: Generate Poco.cppm at configure time

Keep separate modules but have CMake generate the umbrella file, emitting only the export import lines for enabled components:

set(POCO_MODULE_IMPORTS "")
if(ENABLE_FOUNDATION)
    string(APPEND POCO_MODULE_IMPORTS "export import Poco.Foundation;\n")
endif()
if(ENABLE_CRYPTO)
    string(APPEND POCO_MODULE_IMPORTS "export import Poco.Crypto;\n")
endif()
# ... etc
configure_file(Poco.cppm.in Poco.cppm @ONLY)

This eliminates the preprocessor guards while preserving optional components.

Other alternatives:

  1. Keep the current design as-is — separate modules with #ifdef guards in Poco.cppm. It already works correctly with all ENABLE_* flags.

  2. Formalize the two-tier approach — each component stays its own module (Poco.Net, Poco.Crypto), sub-components remain partitions of their parent (Poco.Data:SQLite, Poco.DNSSD:Avahi), and Poco.cppm is the conditional umbrella. This is essentially what the codebase already does today.

@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

mikomikotaishi commented Mar 26, 2026

The #ifdef guards inside each .cppm make the exported namespaces empty, but the partition source files are still compiled and linked. This breaks minimal/selective builds — a core Poco feature where users choose exactly which components to enable.

I think it only adds the empty partitions, but does not link anything (for example, if Poco::Data is not enabled, it does not break the build to link Poco:Data nor does having it in the modules/CMakeLists.txt source list cause it to build the Poco/Data/*.cpp files.

After all, the CI tests do succeed, which does suggest nothing is breaking between this change - it only makes all the module pieces into partitions which may or may not have exported contents. So, again, even though the partition Poco:Crypto is built and re-exported from the module Poco, it doesn't force the Poco::Crypto library to be built, and the partition itself is empty. Only what is specifically declared in the CMake configuration is completely built.

@matejk
Copy link
Copy Markdown
Contributor

matejk commented Mar 27, 2026

The #ifdef guards inside each .cppm make the exported namespaces empty, but the partition source files are still compiled and linked. This breaks minimal/selective builds — a core Poco feature where users choose exactly which components to enable.

I think it only adds the empty partitions, but does not link anything (for example, if Poco::Data is not enabled, it does not break the build to link Poco:Data nor does having it in the modules/CMakeLists.txt source list cause it to build the Poco/Data/*.cpp files.

But what is the point of having empty partitions?

After all, the CI tests do succeed, which does suggest nothing is breaking between this change - it only makes all the module pieces into partitions which may or may not have exported contents. So, again, even though the partition Poco:Crypto is built and re-exported from the module Poco, it doesn't force the Poco::Crypto library to be built, and the partition itself is empty. Only what is specifically declared in the CMake configuration is completely built.

C++ modules are built only in one CI job, not all.

I am trying to understand the goal of these changes. I am probably missing something.

@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

mikomikotaishi commented Mar 27, 2026

But what is the point of having empty partitions?

I recalled at some point finding a paper that was adopted which prohibited import statements from being inside of an #if/#ifdef block. From what I recall the reasoning was to allow compilers to quickly resolve module dependencies before preprocessing. I've been trying to find this paper for some time now, but haven't been able to. Regardless, this is one particular problem that this PR would solve.

@matejk
Copy link
Copy Markdown
Contributor

matejk commented Mar 31, 2026

Is this the article that you had in mind?

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1857r3.html

@matejk
Copy link
Copy Markdown
Contributor

matejk commented Mar 31, 2026

Counter-Proposal: Per-Component Module Targets with POCO_MODULE Macro

After researching the C++ standard and CMake's module support, here's a more detailed analysis and an alternative approach that achieves the goal of removing preprocessor guards while preserving Poco's optional-component architecture.

Clarification on the Standard

The paper you're likely thinking of is P1857R3: Modules Dependency Discovery. It restricts preprocessor conditionals from spanning a module declaration (you can't do #ifdef USE_MODULES / export module foo; / #endif). It does not prohibit import directives inside #ifdef blocks — conditional imports are grammatically valid and accepted by all major compilers (GCC, Clang, MSVC).

That said, there is a practical concern: conditional imports add complexity to build-system dependency scanning. So eliminating them is still a worthwhile goal — just not for standards-compliance reasons.

The Architectural Issue

The root problem isn't the #ifdef guards in Poco.cppm — it's that all modules are bundled into a single monolithic Modules target in modules/CMakeLists.txt. This is what forces the need for preprocessor conditionals and compile definitions.

Converting separate modules (Poco.Foundation, Poco.Net) into partitions of a single Poco module (:Foundation, :Net) doesn't solve this — it makes it worse, because partitions must all be compiled together as part of their parent module.

C++20 modules distinguish between:

  • Separate modules — for independently buildable units with explicit dependencies (what Poco components are)
  • Partitions — for subdividing a single module's implementation (what Data backends are within Data)

Proposed Solution

Attach each .cppm file to its own component's library target using CMake's FILE_SET CXX_MODULES. This is the approach recommended by Kitware:

1. Add a POCO_MODULE macro to PocoMacros.cmake:

macro(POCO_MODULE target_name module_file)
    if(ENABLE_POCO_MODULES)
        target_sources(${target_name}
          PUBLIC FILE_SET CXX_MODULES FILES
            "${POCO_BASE}/modules/${module_file}"
        )
    endif()
endmacro()

2. Each component calls POCO_MODULE in its own CMakeLists.txt:

# Foundation/CMakeLists.txt
POCO_MODULE(Foundation Poco/Foundation.cppm)

# Net/CMakeLists.txt
POCO_MODULE(Net Poco/Net.cppm)

# Crypto/CMakeLists.txt
POCO_MODULE(Crypto Poco/Crypto.cppm)

# Data/CMakeLists.txt
POCO_MODULE(Data Poco/Data.cppm)

# Data/SQLite/CMakeLists.txt
POCO_MODULE(DataSQLite Poco/Data/SQLite.cppm)

3. Inter-module imports use explicit import statements:

// modules/Poco/Net.cppm
export module Poco.Net;
import Poco.Foundation;  // resolved via target_link_libraries(Net PUBLIC Poco::Foundation)

export namespace Poco::Net { ... }

CMake resolves import Poco.Foundation automatically because Net links Poco::Foundation via target_link_libraries. No #ifdef guards needed anywhere.

4. Remove the separate modules/CMakeLists.txt target entirely.

5. Optionally generate an umbrella Poco.cppm at configure time for users who want import Poco;:

set(POCO_MODULE_IMPORTS "")
if(ENABLE_FOUNDATION)
    string(APPEND POCO_MODULE_IMPORTS "export import Poco.Foundation;\n")
endif()
if(ENABLE_NET)
    string(APPEND POCO_MODULE_IMPORTS "export import Poco.Net;\n")
endif()
# ...
configure_file(modules/Poco.cppm.in ${CMAKE_BINARY_DIR}/modules/Poco.cppm @ONLY)

What This Achieves

Aspect Current This PR Proposed
#ifdef guards in .cppm Yes, everywhere Removed (but empty partitions compiled) Not needed — CMake controls what's built
Minimal builds (ENABLE_CRYPTO=OFF) Works (conditional file list) Compiles empty partition anyway Works (.cppm never compiled)
Inter-component dependencies Not expressed in modules Lost (everything is one module) Explicit import + target_link_libraries
Separate Modules target Yes Yes Eliminated
Per-component CMake changes None None One POCO_MODULE() call each
Module feature opt-in Always built if sources present Always built ENABLE_POCO_MODULES=ON/OFF

References

@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

Ah, that was the paper, but indeed it does not restrict conditional imports.

That being said, we should continue with changing them to partitions. It would be a potential point of confusion for users to see both import Poco; and import Poco.Foundation; as suggestions in code completion, and even doing things like import Poco.Data; which expose Poco.Foundation symbols could be hazardous if someone does import Poco.Data; without the necessary import Poco.Foundation; as well.

So, I'll return the CMake to the original form, but leave the files as partitions rather than full modules.

@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

@matejk Any thoughts on this iteration?

Comment thread modules/Poco/Dynamic.cppm Outdated
#endif

export module Poco.Dynamic;
export module Poco:Dynamic;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poco Dynamic is a part of Foundation. Not sure how to declare that in C++ modules syntax.

Copy link
Copy Markdown
Contributor Author

@mikomikotaishi mikomikotaishi Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could move all that code into Poco:Foundation. That would actually be a benefit for compile times as fewer, larger translation units are faster to build than multiple small translation units.

In fact, since ENABLE_FOUNDATION is always forced to be on when building the library, should we just remove all of the macro checks for that here?

Copy link
Copy Markdown
Contributor

@matejk matejk Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a part of Foundation already, not a separate library.

Macro checks could be removed for Foundation, true.

Copy link
Copy Markdown
Contributor Author

@mikomikotaishi mikomikotaishi Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone ahead and moved the stuff originally in Poco:Dynamic just into Poco:Foundation, deleted the Dynamic.cppm file since it's no longer needed, and removed #ifdef guards on Foundation stuff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matejk Any thoughts on this iteration?

@mikomikotaishi mikomikotaishi changed the title Remove macro guards from Poco module Use partitions on Poco module to simplify module API Apr 8, 2026
@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

@matejk any chance you could re-visit this and let me know your thoughts on my change

@matejk
Copy link
Copy Markdown
Contributor

matejk commented Apr 20, 2026

I am sorry for not answering so long. The changes are now minimal and I don't have any objections on them.

However I am wondering if we need a separate library for C++ modules? IMO modules could be seamlessly integrated with the existing Poco modules as proposed above. What do you think?

@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

I'm confused what you mean by a separate library. Do you mean a separate CMake target? As it is the modules feature is still opt-in, but keeping it bundled is to my knowledge the simplest way to make it accessible to those who request it. Considering how much the module itself depends on what parts of the library are built, it seems easier to me to just leave it directly integrated in the same target, but if you have a concern left unaddressed or know something I don't please let me know.

@matejk
Copy link
Copy Markdown
Contributor

matejk commented Apr 20, 2026

I'm confused what you mean by a separate library. Do you mean a separate CMake target? As it is the modules feature is still opt-in, but keeping it bundled is to my knowledge the simplest way to make it accessible to those who request it. Considering how much the module itself depends on what parts of the library are built, it seems easier to me to just leave it directly integrated in the same target, but if you have a concern left unaddressed or know something I don't please let me know.

Yes, I mean separate CMake target that creates a library (.so or .dll) for C++ modules.

As I wrote, I am not sure what is the common practice for other products or what the standard recommends for this.

@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

I'm actually not sure what to do. I'm afraid my experience with whether people prefer to call it directly from the same target or to have it in a separate DLL is a beyond me here. A separate target was the first iteration I made back in #4999, but I think we ended up deciding just to integrate everything into the actual target.

@matejk
Copy link
Copy Markdown
Contributor

matejk commented Apr 20, 2026

Does this article provide any help: https://mropert.github.io/2026/04/13/modules_in_2026/

@mikomikotaishi
Copy link
Copy Markdown
Contributor Author

I unfortunately don't see any mention of advice on what to do about shared objects/DLLs. The post was a very interesting read, though.

After looking a bit more into it making them separate targets makes sense if users want to both import Poco; and #include <Poco/**/*.h> in the same project. I'm not sure how compelling this is though, or whether this is something people actually do. But, I'm not really sure at the end of the day whether this is an accurate assessment either.

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