Skip to content

Remove macro guards from Poco module#5268

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

Remove macro guards from Poco module#5268
mikomikotaishi wants to merge 4 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.

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