InsightSoftwareConsortium / InsightSoftwareConsortium/ITK
Design: build-configurable ITK namespace — adopt VTK-style ABI inline namespace rather than `#define itk`
- Dominant language
- C++
- Stars
- 1.7k
- Forks
- 748
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 64
Description
> **Status 2026-08-24:** PR #6784 has been closed. Slicer-side work continues on [Slicer/ITK#14](https://github.com/Slicer/ITK/pull/14) (the `itkLoad` symbol), where the `##` defect described below has been reported. **The ITK-side design decision below remains open and unanswered.**
ITK has no supported way to build with a non-default C++ namespace. The target is three
independently-distributed ITK builds coexisting in one process, each addressable and non-colliding:
| Channel | namespace | ships as |
|---|---|---|
| PyPI `itk` wheels | `py_itk` | shared libs |
| SimpleITK | `simple_itk` | shared libs |
| 3D Slicer | `slicer_itk` | shared libs |
They may be the same source, or slightly divergent builds. The acceptance test is concrete: **three ITKs
in one process, each `dlopen`-able, each returning objects of its own types, with no cross-binding under
`LD_DEBUG=bindings`** — the condition @jcfr diagnosed as failing in 2022.
Note what this is *not*: version disambiguation. The three channels may be byte-identical ITK versions,
so anything keyed on version or build number cannot separate them. The distinguishing axis is the
distribution channel, which means the names have to be configurable rather than derived. [PR #6784](https://github.com/InsightSoftwareConsortium/ITK/pull/6784) proposed doing this with `#define itk `; this issue recommends **not** adopting that mechanism for ITK, and instead adopting the ABI inline-namespace design VTK already ships — which [@jcfr proposed for ITK in 2022](https://discourse.itk.org/t/adding-support-for-customizing-itk-namespace/5170) and which is source-transparent to developers.
**Decision requested from maintainers:** does ITK intend to support two ITKs in one process? If yes, the path below is ~3,481 mechanical sites plus infrastructure. If no, the option should stay in Slicer's fork and only the separable bug fixes below should land.
Five mechanisms were compared — the `#define`, an inline namespace, an outer rename plus alias, C++20 modules, and linker/visibility isolation. Only the first two change the mangled name, which is the requirement; of those two, only the inline namespace leaves consumer translation units untouched and fails loudly rather than silently. The comparison, **including the inline namespace's own cost** (bare forward declarations in consumer code break — 53 measured sites), is in the alternatives section below.
**Separable and independently actionable:** the `itkLoad` `##` defect is now tracked on [Slicer/ITK#14](https://github.com/Slicer/ITK/pull/14). The ThirdParty mangling regressions described below (collapsed prefixes, the hardcoded `slicer_itk` literal, and the vendoring-policy bypass) were only ever present in the closed PR and do **not** affect ITK `main`; they are recorded here so they are not reintroduced.
Why #define itk is right for Slicer but not for ITK
This is not a quality judgment. The patch has been stable in Slicer's fork since 2022. It is a question of the contract each project makes.
Slicer builds its ITK **without Python wrapping** under the custom namespace: the use case is importing the normally-built PyPI `itk` package into a process whose own ITK is `slicer_itk`. The macro therefore never has to survive CastXML or SWIG.
ITK shipping this as a documented CMake option would be promising it works for all consumers, including wrapped Python — the use case that originally motivated the work, per Discourse #5170.
Structural findings (measured, not inferred)
**Coverage cannot be closed by inclusion.** Of ITK's 1,996 headers that open `namespace itk`, **35 cannot reach** `itkConfigure.h`, any `*Export.h`, or `itkNamespace.h` by any include path. PR #6784 patches 7; 28 remain. Verified example: `itkExceptionObject.h` has zero project includes (only ``, ``, ``, ``), opens `namespace itk`, and is unpatched.
`#define itk X` is a translation-unit-global textual effect, so which namespace a header's contents land in depends on **what was included before it**, not on the header itself. That is not a property a header can guarantee about itself.
**The failure mode is silent.** Where the halves conflict you get a compile error — the PR author reports `error C2653: 'detail' is not a class or namespace name`. Where they merely differ, you get a silent ODR violation: two distinct types with the same name, linked together, no diagnostic.
**A CI gate cannot cover downstream.** The natural mitigation (assert every file opening `namespace itk` reaches `itkNamespace.h`) covers only ITK's tree. Downstream reopens `namespace itk` in **345 files**: SimpleITK 162, BRAINSTools 118, Slicer 65.
**The macro reaches into consumer translation units.** With the macro active, an unrelated `struct Foo { void itk(); }` in a later-included header is silently rewritten. This is API impact of the least detectable kind, so the mechanism cannot be characterized as ABI-only.
**It is incompatible with ITK's Python wrapping.** Measured with the real toolchain:
| Mechanism | CastXML reports | `.wrap` `"itk::X"` matches |
|---|---|---|
| Inline namespace | `itk::Object` | yes |
| `#define itk slicer_itk` | `slicer_itk::Object` | **no** |
All 1,001 `.wrap` files name classes as `"itk::ClassName"`. PR #6784 sets `ITK_NAMESPACE:STRING=ci_itk` and `ITK_WRAP_PYTHON:BOOL=OFF` in the same CI hunk, which conceals this.
**Repair converges on the approach it was meant to avoid.** The author's `v6NamespaceWIP` branch contains `WIP: attempting to fix compile errors (not working)` and a pass rewriting 18 sites in `itkMacro.h` from `::itk::` to `::ITK_NAMESPACE::` — the explicit-macro style rejected in 2022 as too invasive, now in addition to the `#define` rather than instead of it.
Defects in PR #6784 as written (all verified by execution)
**1. The factory load symbol is wrong in every configuration, including the default.**
```c
#define ITK_LOAD_FUNCTION_NAME ITK_NAMESPACE##Load
```
Operands of `##` are not macro-expanded, so this yields the literal token `ITK_NAMESPACELoad`:
```
$ printf '#define ITK_NAMESPACE ci_itk\n#define L ITK_NAMESPACE##Load\nR: L\n' > t.c
$ cc -E -P t.c | grep R:
R: ITK_NAMESPACELoad
```
A default build would therefore export `ITK_NAMESPACELoad` instead of `itkLoad`. `itkLoad` is a documented public extension point (`CMake/ITKFactoryRegistration.cmake:10`, `itkObjectFactoryBase.h:48`), so every out-of-tree factory plugin would silently stop being found — `LoadLibrariesInPath` just skips libraries lacking the symbol. Needs the standard two-level paste.
**2. The in-tree test cannot detect (1).** `itkFactoryTestLib.cxx` *defines* its entry point through the same macro that `itkObjectFactoryBase.cxx:409` *looks up*. Both sides move together, so the test stays green while the symbol is wrong.
**3. ThirdParty symbols are renamed in default builds.** The `*_mangle.h.in` templates emit `#define @MANGLE_PREFIX@_`. At the default `ITK_NAMESPACE=itk`:
| Library | Before | After |
|---|---|---|
| Expat | `itk_expat_*` | `itk_*` |
| PNG | `itk_png_*` | `itk_*` |
| TIFF | `itk_tiff_*` | `itk_*` |
| OpenJPEG | `${OPENJPEG_LIBRARY_NAME}_*` | `itk_*` |
Four distinct prefixes collapse to one, discarding per-library disambiguation. Additionally `Modules/ThirdParty/JPEG/src/itkjpeg-turbo/CMakeLists.txt` hardcodes `MANGLE_PREFIX "slicer_itk"`, which ignores `ITK_NAMESPACE` entirely.
**4. Vendoring policy.** All eight ThirdParty `CMakeLists.txt` edits belong on the `for/itk-*` overlay branches per `Documentation/Maintenance/ThirdPartyForkConventions.md`, which names mangling changes explicitly. As committed, the next `UpdateFromUpstream.sh` clobbers them.
**5. Smaller gaps.** `ITKConfig.cmake` untouched, so downstream CMake cannot discover the namespace (requested by @blowekamp in 2022); no validation of `ITK_NAMESPACE`; the cache variable is set in `Modules/Core/Common/CMakeLists.txt` but dereferenced in ThirdParty dirs, working only because alphabetic tie-break puts `ITKCommon` early; `doxygen-warnings` CI failure is real and PR-caused (`itkNamespace.h` is generated into the build tree, outside doxygen's `INCLUDE_PATH`).
Prior art: VTK already solved this
Verified against a VTK checkout at `adb88b56ab`:
```cmake
# Common/Core/CMakeLists.txt:45
set(VTK_ABI_NAMESPACE_BEGIN
"inline namespace ${VTK_ABI_NAMESPACE_ATTRIBUTES} ${VTK_ABI_NAMESPACE_NAME} {")
```
| Element | VTK implementation |
|---|---|
| Adoption | 6,701 files |
| C symbols | `VTK_ABI_NAMESPACE_MANGLE(x)` |
| Validation | regex `^[a-zA-Z0-9_]+$`, `FATAL_ERROR` |
| Visibility | `VTK_ABI_NAMESPACE_ATTRIBUTES` hook |
| Forward decls | wrapped in `BEGIN`/`END` (`vtkObject.h:39`) |
| Test | `Testing/Core/CheckSymbolMangling.py`, per module |
| CI | one job, `..._python_qt_tbb_mangling` |
| Rollout | incremental, per-module follow-up fixes |
Two planning details: VTK's mangling CI job **includes Python**, and `ctest_test.cmake:36` runs `INCLUDE_LABEL MANGLING` under it — VTK validates mangling against a labeled subset, not the full suite.
An inline namespace changes the *mangled* symbol while leaving `itk::Image` valid source. It fixes the linker/ODR problem without touching consumer code, and its failure mode is a compile or link error rather than silence.
Why the inline namespace, and not the other alternatives
The mechanism has to satisfy one hard requirement — **two independently-built ITKs loaded into one process must not bind to each other's symbols** — without breaking the source that ~everyone writes (`itk::Image`). That requirement eliminates most of the option space.
**A. `#define itk ` (PR #6784).** Rejected for the reasons in the sections above: coverage cannot be closed by inclusion, the failure mode is a silent ODR split, it reaches into consumer translation units, and it is incompatible with ITK's `.wrap` files. Correct for Slicer, whose build never exercises those paths.
**B. Inline namespace (recommended).** `namespace itk { inline namespace { … } }`. Mangled symbols differ per configuration; `itk::Image` stays valid source; failures are compile/link errors, not silence. Adopted by VTK (6,701 files), libstdc++ (`_GLIBCXX_BEGIN_NAMESPACE_VERSION`), libc++, Abseil, and protobuf.
**C. Outer namespace rename + alias.** `namespace ITK_NAMESPACE_NAME { … }` plus `namespace itk = ITK_NAMESPACE_NAME;`. This was the shape originally floated in Discourse #5170 (Qt's `QT_BEGIN_NAMESPACE` style). It costs the same ~3,481-site conversion as B, and gains nothing over it: `itk::` then works only through an alias, which is weaker than B — an alias cannot be reopened, so *every* `namespace itk { … }` in downstream code breaks, not just forward declarations. Strictly dominated by B.
**D. C++20 modules.** Does not solve the stated requirement. Two modules exporting `itk::Image` make the consumer ill-formed rather than isolating them; entities reached through the global module fragment (i.e. `#include`d headers) get no module-specific mangling at all; and CMake cannot build BMIs from `IMPORTED` targets, which is exactly how ITK ships to consumers. Also a total-rewrite migration, so the cost is not comparable.
**E. Linker-level isolation — `-fvisibility=hidden`, version scripts, `RTLD_LOCAL`, two-level namespaces.** These are the most common counter-proposal and the most important to rule out explicitly. They do not solve the problem, because ITK's exposure is overwhelmingly *vague-linkage* entities: templates, inline functions, and class vtables/typeinfo. Under the Itanium C++ ABI those are emitted into COMDAT groups keyed on **the mangled name alone**, so two builds' `itk::Image` instantiations are eligible to be folded together regardless of visibility flags or dlopen mode. Visibility controls what is *exported*; it does not make two identically-mangled definitions distinct. That is precisely the ODR-violation-with-no-diagnostic case. These techniques are useful hardening but cannot be the mechanism.
Note this is why the problem must be solved by changing the **mangled name**, which is what both A and B do and what D and E do not.
### The cost of the recommendation, stated plainly
B is not free, and its one real downside should be weighed openly rather than discovered later.
**A bare forward declaration in consumer code breaks.** Writing `namespace itk { class Image; }` declares a *new* `itk::Image` in the enclosing namespace rather than referring to the one inside the inline namespace. Verified:
```
fwd.cxx:3:6: error: reference to 'Image' is ambiguous
note: candidate found by name lookup is 'itk::Image'
```
Three points make this acceptable rather than disqualifying:
1. **It fails loudly, at compile or link time**, at the site of the problem. Compare option A, whose analogous failure is a silent ODR split that produces wrong numerical results.
2. **The measured downstream cost is small and bounded**: 53 sites across the ITK ecosystem (SimpleITK 49, Slicer 3, BRAINSTools 1), plus one explicit specialization in `Slicer/Libs/vtkITK/vtkITKNumericTraits.h`. The fix at each site is to include the header instead of forward-declaring.
3. It is a known, documented cost of this design elsewhere — Abseil's response was to instruct consumers not to reopen its namespace. ITK cannot make that decree as bluntly, since downstream reopens `namespace itk` in 345 files, but those reopens are fine *if they use the `ITK_ABI_NAMESPACE_BEGIN`/`END` macros*, which ITK would ship and document.
**Summary comparison**
| | A. `#define itk` | B. Inline ns | C. Rename+alias | D. Modules | E. Visibility |
|---|---|---|---|---|---|
| Solves 2-in-1-process | yes | yes | yes | no | **no** |
| `itk::Image` valid source | yes | yes | via alias | no | yes |
| ITK churn | ~30 files | 3,481 sites | 3,481 sites | rewrite | ~0 |
| Downstream source edits | none intended | 53 fwd decls | all `namespace itk` | all | none |
| Reaches into consumer TUs | **yes** | no | no | no | no |
| Works with `.wrap`/CastXML | **no** | yes | no | n/a | yes |
| Failure mode | **silent** | loud | loud | loud | **silent** |
SWIG: a blocker that turned out not to apply
An earlier draft of this analysis flagged SWIG as the major risk, since SWIG 4.5.0 is not transparent to inline namespaces:
```
%template(ImageF) itk::Image; -> Error: Template 'itk::Image' undefined
%template(ImageF) itk::v6::Image; -> exit 0
```
That is correct but **does not apply to ITK**, because ITK's wrapping never asks SWIG to resolve an `itk::` name. `generate_class` (`igenerator.py:~1055`) emits flat, alias-flattened declarations — `class itkImageF3 : public itkImageBase3 { … };` — with every type mapped through `get_alias()` (`igenerator.py:852`). Confirmed against a real generated `build-python/Wrapping/Typedefs/itkImage.i`: there are **zero** `%template` directives on an `itk::` name in the tree, and no `.i` file `%include`s a real ITK header. `itk::` survives only inside `%{ … %}` verbatim blocks, which the C++ compiler parses.
Since CastXML omits the inline namespace, pygccxml never sees it either, so `.idx` keys, swig aliases, and Python-visible names are unaffected.
Recommended defensive patch (~4 lines) for remote modules shipping hand-written `.i` files: `#ifndef`-guard `ITK_ABI_NAMESPACE_BEGIN/END` and pass `-DITK_ABI_NAMESPACE_BEGIN= -DITK_ABI_NAMESPACE_END=` to the SWIG invocation in `Wrapping/Generators/SwigInterface/CMakeLists.txt`. Verified to work; the macros must be `#ifndef`-guarded or the `-D` is a redefinition error.
The four naming axes, and which already exist
Coexistence needs all four aligned. Two are already configurable on `main`:
| Axis | Variable | On `main`? |
|---|---|---|
| C++ symbols | `ITK_NAMESPACE` or an ABI inline namespace | **no** — the decision this issue asks for |
| autoload C symbol | `ITK_LOAD_FUNCTION_NAME` | **no** — #6787 / #6795 |
| CMake target prefix | `ITK_LIBRARY_NAMESPACE` (`CMakeLists.txt:205`) | yes |
| library filename | `ITK_CUSTOM_LIBRARY_SUFFIX` (`ITKModuleMacros.cmake:731`) | yes |
The filename axis is not cosmetic. Three shared libraries all named `libITKCommon-6.0.so` in one process
means the dynamic loader keys on SONAME and the first one loaded wins — the other two silently resolve to
it, no diagnostic. C++ symbols can be perfectly distinct and it still will not work.
The dangerous state is a *partially* configured build: targets renamed but symbols not configures and
links cleanly, then collides at run time. These should be independent variables with a configure-time
consistency check, not one knob implying the others — changing the CMake target namespace is an API break
for downstream, while the ABI namespace is not, so coupling them would force an API break for an ABI-only
change.
Raised by @blowekamp on this issue.
Status of the work
| Item | Where | State |
|---|---|---|
| Autoload symbol name | #6787 (main), #6795 (release-5.4) | green, open |
| Namespace mechanism decision | **this issue** | **open, undecided** |
| Slicer plugin uses the macro | [Slicer/Slicer#9374](https://github.com/Slicer/Slicer/pull/9374) | open |
| Stringify-macro consolidation | #6788 | blocked on #6787 |
| Installed-header macro leaks | #6789 | open |
| Duplicated test-helper macros | #6790 | open |
Proposed implementation plan
**Phase 0 — the autoload symbol name.** [#6787](https://github.com/InsightSoftwareConsortium/ITK/pull/6787)
(main) and [#6795](https://github.com/InsightSoftwareConsortium/ITK/pull/6795) (release-5.4).
`extern "C"` names carry no C++ namespace, so no namespace scheme reaches the ObjectFactory autoload entry
point; it needs its own configurable name. Both PRs are green and no-behavior-change by default.
*Superseded:* an earlier revision of this phase listed repairs to #6784 — per-library ThirdParty mangle
prefixes, a `slicer_itk` literal, ThirdParty overlay branches, doxygen `INCLUDE_PATH`. #6784 was closed and
none of those defects exist on `main`; verified. They are not work items.
**Phase 1 — single-module prototype (gate).** Convert `Modules/Filtering/Smoothing` to `ITK_ABI_NAMESPACE_BEGIN/END`, build with `ITK_WRAP_PYTHON=ON` and a non-default namespace, import the module. Success = CastXML resolves `.wrap` classes unchanged, SWIG generates cleanly, Python-visible names unchanged, `nm` shows the inner namespace. The analysis predicts this passes; it stays a gate because pygccxml's `decl_string` shape for nested typedefs/enums, warning-clean `_wrap.cxx`, and `_ITKCommonPython` symbol exports cannot be settled without a real wrapped build. ~1–2 days.
**Phase 2 — infrastructure.** `itkABINamespace.h.in` modeled on `vtkABINamespace.h.in`; `ITK_ABI_NAMESPACE_NAME` cache variable at **top level** beside the existing `ITK_LIBRARY_NAMESPACE` (`CMakeLists.txt:205`, exported via `ITKConfig.cmake.in:70` — the in-tree precedent for both placement and export), regex-validated;
On the `ITKConfig.cmake.in` export specifically: this requirement belongs to the **namespace value**, not to every configured name. @blowekamp asked for it in the 2022 Discourse thread so consuming packages could read the value in CMake — SimpleITK re-emitting it into `sitkConfigure.h.in`. That is a real consumer: a downstream project that must reproduce the value in its own generated header.
It does **not** generalize. An analogous export of `ITK_LOAD_FUNCTION_NAME` was added to [#6787](https://github.com/InsightSoftwareConsortium/ITK/pull/6787) and removed on review, because nothing in CMake consumes the load symbol name — the only reference under `CMake/` is a documentation comment at `ITKFactoryRegistration.cmake:10`. The test for whether a configured value belongs in `ITKConfig.cmake` is whether some consumer — downstream code, or one of ITK's own shipped modules re-executing in the consumer's context — actually reads it. `ITK_LIBRARY_NAMESPACE` passes that test via `ITKFactoryRegistration.cmake:180`; the load symbol name does not.
If the per-module symbol-mangling test is ported, it becomes the first genuine CMake-side consumer of an expected symbol name, and the export question should be revisited then — with that consumer to justify it. port `CheckSymbolMangling.py` with a per-module `MANGLING` label; `ITK_ABI_NAMESPACE_MANGLE(x)` modeled on VTK's; the ~4-line SWIG patch.
On `ITK_ABI_NAMESPACE_MANGLE` specifically: [#6787](https://github.com/InsightSoftwareConsortium/ITK/pull/6787) has already introduced the seam this needs to fill, so Phase 2 should **redefine the existing macro in terms of MANGLE rather than add a parallel mechanism**:
```c
#ifndef ITK_LOAD_FUNCTION_NAME
# define ITK_LOAD_FUNCTION_NAME ITK_ABI_NAMESPACE_MANGLE(itkLoad)
#endif
```
The `#ifndef` guard stays, downstream `-DITK_LOAD_FUNCTION_NAME=...` overrides keep working, and no call site changes. #6787 deliberately did *not* add MANGLE, because without an ABI namespace it would be a no-op shipped for an undecided feature.
Two things to settle when it is added:
- **Scope is small.** Of 15 `extern "C"` sites in ITK proper, nearly all are *inbound* — ITK consuming C libraries (libjpeg error callbacks, POSIX casts, netlib f2c declarations) — which need no mangling. The genuinely exported entry points are the two `itkLoad` declarations and `EquivalencyTable()` in `Core/Common/test/ClientTestLibraryB.h`. VTK needed MANGLE for a recurring pattern (`GetVTKVersion`, `signal_handler`, a per-library serialization registrar); ITK does not have that pattern yet.
- **The mangling shape does not match Slicer's existing name.** VTK's form is `@NAME@_##x`, so with namespace `slicer_itk` it yields `slicer_itk_itkLoad` — whereas Slicer's fork exports `slicer_itkLoad`. Either the prefix form or the Slicer name has to give; the `ITK_LOAD_FUNCTION_NAME` override accommodates both today.
**Phase 3 — mechanical conversion.** 3,481 open sites across 3,455 files (Filtering 1,321, Core 999, IO 329, Segmentation 279, Registration 268, Numerics 153, Nonunit 63, Video 51, Bridge 18). Scripted; residue is ~23 files where open/close markers do not correspond, plus ~380 candidate forward-declaration sites. Incremental rollout per VTK, one module group per PR.
**Phase 4 — validation and downstream.** One CI job with a non-default namespace and `ITK_WRAP_PYTHON=ON`, running the `MANGLING` label plus `itk_module_headertest`. Fix 53 downstream forward declarations (SimpleITK 49, Slicer 3, BRAINSTools 1) plus one explicit specialization in `Slicer/Libs/vtkITK/vtkITKNumericTraits.h`.
**Out of scope.** Namespaces do not isolate the `ObjectFactoryBase` / `ImageIOFactory` singleton registries or the several hundred non-namespaced `itk*` macros. Two ITKs in one process still share registry state; this should be documented as a known limitation rather than silently inherited.
Reproduction
```bash
# Token paste
printf '#define ITK_NAMESPACE ci_itk\n#define L ITK_NAMESPACE##Load\nR: L\n' > t.c
cc -E -P t.c | grep R: # -> R: ITK_NAMESPACELoad
# CastXML transparency (compare inline ns vs macro)
castxml --castxml-output=1 -x c++ -std=c++17 -o out.xml header.hpp
# Churn measurement
git grep -c -E '^[[:space:]]*namespace[[:space:]]+itk([[:space:]]|$|\{)' \
-- Modules ':!Modules/ThirdParty/*'
```
Environment: macOS 15 arm64, Apple clang, CastXML (Homebrew), SWIG 4.5.0, VTK `adb88b56ab`, ITK `main` at `6a438fdf10d`.
/cc @dzenanz @jcfr @blowekamp
Contributor guide
Research direction
First resolve the open maintainer decision, then compare the proposed approach with VTK's Common/Core/CMakeLists.txt and its Testing/Core/CheckSymbolMangling.py test. Review CMake/ITKFactoryRegistration.cmake, itkObjectFactoryBase.h, and the Python wrapping constraints; done means the chosen design supports the stated three-build loading test without breaking wrapping or factory symbol discovery.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cmake, cpp, python
- Domain
- backend-api-design, build-system
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100