Skip to content

ENH: Configure the ObjectFactory autoload symbol name - #6787

Open
hjmjohnson wants to merge 1 commit into
InsightSoftwareConsortium:mainfrom
hjmjohnson:comp-itkload-symbol-macro
Open

ENH: Configure the ObjectFactory autoload symbol name#6787
hjmjohnson wants to merge 1 commit into
InsightSoftwareConsortium:mainfrom
hjmjohnson:comp-itkload-symbol-macro

Conversation

@hjmjohnson

@hjmjohnson hjmjohnson commented Aug 24, 2026

Copy link
Copy Markdown
Member

ObjectFactoryBase looked up the autoload entry point by string literal while plugins define it by name, so the two agreed only by convention. Both now derive from ITK_LOAD_FUNCTION_NAME, configured once into itkConfigure.h. No behavior change — the default is itkLoad.

This is enabling work with no improvement today. It makes one of four names configurable so that independently-distributed ITK builds can eventually coexist in one process — PyPI itk wheels as py_itk, SimpleITK as simple_itk, Slicer as slicer_itk, possibly built from the same source. extern "C" names carry no C++ namespace, so the autoload entry point needs its own configurable name whichever namespace mechanism ITK adopts. That mechanism question is #6786 and is not decided here.

Related work Status
Namespace mechanism decision #6786
release-5.4 backport of this PR #6795
Slicer plugin uses the macro Slicer#9374
Slicer's fork-side patch this supersedes Slicer/ITK#14
Stringify-macro consolidation (blocked on this) #6788

Reviewers: the plugin-facing code is only reachable with BUILD_SHARED_LIBS=ON. ITK guards both factory plugin targets behind if(ITK_BUILD_SHARED_LIBS) (Modules/Core/Common/test/CMakeLists.txt:151, Modules/IO/ImageBase/test/CMakeLists.txt:933), so 3 of the 9 changed files are compiled only in a shared build (itkFactoryTestLib.h, itkFactoryTestLib.cxx, itkFileFreeImageIOFactory.cxx) and a static build does not register itkIOPluginTest. Of the remainder, 2 are CMake and 4 compile in both configurations.

The four naming axes, and why this is one of them

Coexisting builds need all four aligned. Two are already configurable on main:

Axis Variable On main
C++ symbols ITK_NAMESPACE or an ABI inline namespace no — #6786
autoload C symbol ITK_LOAD_FUNCTION_NAME no — this PR
CMake target prefix ITK_LIBRARY_NAMESPACE (CMakeLists.txt:205) yes
library filename ITK_CUSTOM_LIBRARY_SUFFIX (ITKModuleMacros.cmake:731) yes

ITK_LOAD_FUNCTION_NAME is defined at the top level beside ITK_LIBRARY_NAMESPACE, so all four are settable before any module is configured.

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, so the C++ symbols can be perfectly distinct and it still will not work.

Why the name is build-configured rather than a header #ifndef

An earlier revision put an overridable #ifndef ITK_LOAD_FUNCTION_NAME in itkObjectFactoryBase.h. That was unsound and has been replaced.

The lookup side is compiled into libITKCommon when ITK is built. A downstream consumer defining the macro before including the installed header would change only its own plugin's exported symbol, while the prebuilt loader still searched for itkLoad — the plugin would silently never load. Confirmed against the built library:

$ strings libITKCommon-6.0.1.dylib | grep -x itkLoad
itkLoad

Routing the value through the generated itkConfigure.h makes disagreement structurally impossible: there is one generated header, and the loader and every plugin include it.

// itkConfigure.h.in
#define ITK_LOAD_FUNCTION_NAME @ITK_LOAD_FUNCTION_NAME@
# CMakeLists.txt, beside ITK_LIBRARY_NAMESPACE
set(ITK_LOAD_FUNCTION_NAME "itkLoad" CACHE STRING "...")
mark_as_advanced(ITK_LOAD_FUNCTION_NAME)
if(NOT ITK_LOAD_FUNCTION_NAME MATCHES "^[A-Za-z_][A-Za-z0-9_]*$")
  message(FATAL_ERROR "... must be a valid C identifier, got '${ITK_LOAD_FUNCTION_NAME}'")
endif()
Why extern "C" needs its own mechanism

extern "C" names carry no C++ namespace, so no namespace scheme reaches this entry point. Demonstrated on one library containing both kinds of symbol:

$ nm -gU libexternc.so
__ZN3itk2v67cppFuncEv     # C++ inside `inline namespace v6` -> mangled, namespaced
_itkLoad                  # extern "C"                       -> untouched

VTK hit the same thing and ships VTK_ABI_NAMESPACE_MANGLE(x) for GetVTKVersion, signal_handler, and its serialization registrars.

Also included: ITK_STRINGIFY in itkMacro.h

The loader needs the configured token as a string, which requires the two-level stringify idiom — # does not expand its operand, so a single level yields "ITK_LOAD_FUNCTION_NAME".

Rather than adding another local copy, this PR puts ITK_STRINGIFY / ITK_STRINGIFY_HELPER in itkMacro.h. ITK already contains 34 hand-rolled copies of this idiom across 17 files — 32 named _STRING/TOSTRING, and _STRING is a reserved identifier ([lex.name]/3.1), so those are undefined behavior as well as duplicates.

Migrating the existing 34 is deliberately not in this PR; it is tracked as #6788. This PR adds only the shared definition its own single call site needs.

Verification

macOS 15 arm64, Apple clang via the pixi cxx environment, Release.

1. BUILD_SHARED_LIBS=ON, default — build clean, itkIOPluginTest PASS.

plugin export:  000000000000717c T _itkLoad
libITKCommon:   itkLoad

2. BUILD_SHARED_LIBS=ON, -DITK_LOAD_FUNCTION_NAME=slicer_itkLoad — build clean, itkIOPluginTest PASS.

generated:      #define ITK_LOAD_FUNCTION_NAME slicer_itkLoad
plugin export:  000000000000717c T _slicer_itkLoad
libITKCommon:   slicer_itkLoad

Loader and plugin move together; the factory still loads.

3. Validation — a value that is not a valid C identifier fails configure:

ITK_LOAD_FUNCTION_NAME must be a valid C identifier, got 'not a valid name'

4. BUILD_SHARED_LIBS=OFF — build clean, 4/4 ObjectFactory tests PASS. ctest -N -R itkIOPluginTest reports Total Tests: 0, confirming the plugin test is absent by ITK's own guard rather than by anything in this change.

pre-commit run --all-files passes on the branch tip.

@github-actions github-actions Bot added type:Enhancement Improvement of existing methods or implementation type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:Core Issues affecting the Core module area:IO Issues affecting the IO module labels Aug 24, 2026
@hjmjohnson
hjmjohnson force-pushed the comp-itkload-symbol-macro branch 2 times, most recently from 3c05614 to 1783a9a Compare August 24, 2026 21:58
@hjmjohnson
hjmjohnson force-pushed the comp-itkload-symbol-macro branch from 1783a9a to 72db5ec Compare August 24, 2026 22:18

@dzenanz dzenanz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How would this be used by Slicer? Where does #define ITK_LOAD_FUNCTION_NAME slicer_itkLoad needs to be put?

@hjmjohnson
hjmjohnson force-pushed the comp-itkload-symbol-macro branch from 72db5ec to 0277890 Compare August 25, 2026 00:49
@github-actions github-actions Bot added the type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots label Aug 25, 2026
@hjmjohnson hjmjohnson changed the title ENH: Route the ObjectFactory autoload symbol through a macro ENH: Configure the ObjectFactory autoload symbol name Aug 25, 2026
@hjmjohnson

Copy link
Copy Markdown
Member Author

Nowhere in source — that's the change from the fork patch. It's a CMake cache variable now, so Slicer sets it once when configuring ITK:

# Slicer/SuperBuild/External_ITK.cmake, alongside the other -DITK_* entries
# in the CMAKE_CACHE_ARGS list (~line 96-147)
-DITK_LOAD_FUNCTION_NAME:STRING=slicer_itkLoad

ITK then configures that into the generated itkConfigure.h, which is an installed header, so the loader inside libITKCommon and every plugin compiled against that ITK see the same value by construction. There is no header for Slicer to patch and no #define for a consumer to get wrong.

What Slicer would still need to change (2 lines)

One Slicer file declares the entry point literally:

// Slicer/Libs/MRML/IDImageIO/itkMRMLIDIOPlugin.h:24
MRMLIDIOPlugin_EXPORT itk::ObjectFactoryBase* itkLoad();

// Slicer/Libs/MRML/IDImageIO/itkMRMLIDIOPlugin.cxx:10
itk::ObjectFactoryBase* itkLoad()

Both become ITK_LOAD_FUNCTION_NAME(), exactly as ITK's own itkFactoryTestLib.cxx and itkFileFreeImageIOFactory.cxx do in this PR. itkMRMLIDIOPlugin.h already includes ITK headers, so the macro is in scope with no new include.

That appears to be the only one. I searched the Slicer ecosystem in the forest build testbed for itkLoad:

Project Files
Slicer 2 (the itkMRMLIDIOPlugin pair above)
SlicerExtensions 0
vtkAddon 0
SimpleITK 0
ANTs 0
BRAINSTools 4 — all false positives (itkLoadWithMetaData, an unrelated name)

So no extension defines a factory plugin, and nothing outside that one Slicer pair needs touching.

Reading the value back from CMake

ITKConfig.cmake now exports it next to ITK_LIBRARY_NAMESPACE:

set(ITK_LOAD_FUNCTION_NAME "@ITK_LOAD_FUNCTION_NAME@")

so Slicer's own CMake (and the extension build system) can read what ITK was actually built with rather than assuming. This is the pattern @blowekamp asked for in the 2022 Discourse thread for the namespace value.

The name is also validated at configure time — a value that is not a valid C identifier is a FATAL_ERROR rather than a mysterious compile failure later.

Scope — what this does and does not cover

This handles only the ObjectFactory autoload symbol. It is not the namespace feature.

The reason it needs its own mechanism is that extern "C" names carry no C++ namespace, so no namespace scheme — the #define itk approach or a VTK-style ABI inline namespace — reaches this entry point:

$ nm -gU libexternc.so
__ZN3itk2v67cppFuncEv     # C++ inside `inline namespace v6` -> mangled, namespaced
_itkLoad                  # extern "C"                       -> untouched

VTK hit the same thing and ships VTK_ABI_NAMESPACE_MANGLE(x) for it.

So this is one piece of what the fork patch does, upstreamed on its own merits. The broader question — whether ITK supports a configurable namespace at all, and by what mechanism — is #6786, which this PR does not decide. #6786's Phase 2 records how ITK_LOAD_FUNCTION_NAME should later be composed with ITK_ABI_NAMESPACE_MANGLE if that lands.

Worth flagging for sequencing: Slicer's current branch computes the name as ITK_NAMESPACE##Load, which does not expand ITK_NAMESPACE — operands of ## are not macro-expanded — so it yields the literal token ITK_NAMESPACELoad in every configuration. Details on Slicer/ITK#14. Setting -DITK_LOAD_FUNCTION_NAME=slicer_itkLoad here produces the name the fork actually intends.

@hjmjohnson
hjmjohnson marked this pull request as ready for review August 25, 2026 01:03
@greptile-apps

This comment was marked as low quality.

Comment thread Modules/Core/Common/CMakeLists.txt Outdated
@hjmjohnson

Copy link
Copy Markdown
Member Author

Backport to release-5.4 is open as #6795 (draft). It is a straight cherry-pick of this commit with one conflict resolution — ITK_LIBRARY_NAMESPACE does not exist on release-5.4, so only the ITK_LOAD_FUNCTION_NAME export was kept in ITKConfig.cmake.in. Verified independently on that branch (default, overridden, validation, static). It should merge after this one.

@hjmjohnson
hjmjohnson force-pushed the comp-itkload-symbol-macro branch from 0277890 to a54b6cd Compare August 25, 2026 01:29

@dzenanz dzenanz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks usable now.

Comment thread Modules/Core/Common/CMakeLists.txt Outdated
@blowekamp

blowekamp commented Aug 25, 2026

Copy link
Copy Markdown
Member

In the current build/configuration of 3D Slicer this change I don't think is needed. See: Slicer/Slicer#8947 for the issue. They disabled adding the itkLoad function to the problematic ImageIO in 3DSlicer.

There is still SimpleITK and ITKPython which can try to load libraries and check for the itkLoad symbol. This can cause problems with the versions or build configuration of ITK does not match.With underlying issue reported above it is unclear to me if loading the library causes broad symbol conflict or if just creating an ITK object from the separate build.

We should consider making a more robust solution to the underlying problem. Perhaps adding a ITK version or build number automatically to the name to more broadly prevent the issue with loading itkLoad.

EDIT: This may just be future work with how to define the function name that is introduced here.

Comment thread CMake/ITKConfig.cmake.in Outdated
@dzenanz

dzenanz commented Aug 25, 2026

Copy link
Copy Markdown
Member

@hjmjohnson Maybe try using this in Slicer before merging?

@hjmjohnson

Copy link
Copy Markdown
Member Author

Ran this through the downstream forest testbed (33 ITK consumers built against
one ITK). No collisions, no downstream breakage — but one finding changes the
follow-on plan in the description.

Two consumers hardcode itkLoad, so a renamed build breaks them silently.
itkMRMLIDIOPlugin.{h,cxx} in Slicer, and MITK's mitkToolFactoryMacro.h +
ToolExtensionITKFactoryLoader.cpp.in. Nothing breaks today — the default is
itkLoad — but Slicer repinning onto an ITK built with
-DITK_LOAD_FUNCTION_NAME=slicer_itkLoad would leave that plugin exporting
itkLoad while the loader looks up slicer_itkLoad, and
LoadLibrariesInPath prints nothing when the symbol is absent. That is the
same silent mismatch this PR's #ifndef revision was rejected for, closed
inside ITK and still open one level out.

The Slicer half is now handled and verified —
Slicer/Slicer#9374 spells the
entry point via the macro, and a full Slicer SuperBuild against this PR with a
renamed symbol comes out consistent (details below). MITK is untouched and
unaffected by default; what remains is a diagnostics gap, below.

Where the two consumers hardcode it, and who actually uses the mechanism
consumer file what it is
Slicer Libs/MRML/IDImageIO/itkMRMLIDIOPlugin.{h,cxx} defines and exports itkLoad() for the MRML IDImageIO factory
MITK CMake/ToolExtensionITKFactoryLoader.cpp.in configured template defining itkLoad()
MITK Modules/Segmentation/include/mitkToolFactoryMacro.h public macro API — MITK_EXTERNAL_TOOL_GUI_HEADER_MACRO / _CPP_MACRO generate itkLoad(), so third-party MITK tools inherit it

A separate sweep for ITK_AUTOLOAD_PATH shows Slicer is the only consumer
that wires the mechanism up at runtime, and it does so for exactly that
plugin: Libs/MRML/IDImageIO/CMakeLists.txt builds it,
CMake/SlicerBlockCTKAppLauncherSettings.cmake sets ITK_AUTOLOAD_PATH for
both build and install trees, and vtkSlicerCLIModuleLogic.cxx /
qSlicerCLIExecutableModuleFactory.cxx propagate it into CLI-module child
processes. Both halves are live in the project the follow-on plan targets.

MITK ships the generator but no ITK_AUTOLOAD_PATH wiring of its own; its
exposure is through third-party callers of the macro. That is the worse case
to reason about: updating MITK's macro fixes new builds and cannot reach
already-built external tools.

(BRAINSTools matches were false positives — MATLAB comments about unrelated
itkLoad* command-line programs.)

Suggested follow-ups
  1. A debug-level message in the else branch of LoadLibrariesInPath
    naming the symbol looked for and the library skipped. Deliberately not a
    warning: every non-factory shared library on ITK_AUTOLOAD_PATH
    legitimately takes that branch, which is presumably why it is silent
    today. For third-party MITK tool authors this is the only signal they
    would ever get.
  2. Amend the follow-on plan here to say that repinning Slicer also requires
    updating itkMRMLIDIOPlugin to spell the entry point
    ITK_LOAD_FUNCTION_NAME. As written it reads as self-contained on the ITK
    side.
  3. A migration note for plugin authors: use ITK_LOAD_FUNCTION_NAME, include
    itkConfigure.h, read the value from ITKConfig.cmake.

None of these block this PR.

What the forest verified clean

Forest build_forest-itk-main, repointed upstream/main -> pr/6787. The PR
head contains every commit in that forest's upstream/main
(git rev-list --left-right --count upstream/main...HEAD -> 0 8), so this
tests the PR rather than a stale base.

No macro collision anywhere. This PR adds ITK_STRINGIFY /
ITK_STRINGIFY_HELPER to itkMacro.h and ITK_LOAD_FUNCTION_NAME to the
generated itkConfigure.h — all installed headers, so all three leak into
every consumer TU. Across the 33 consumer trees (ANTs, BRAINSTools, Slicer,
MITK, elastix, SimpleITK, RTK, Plastimatch, c3d, TubeTK, ITK-SNAP, PlusLib,
IGSIO, the remote modules, ...): zero downstream definitions of any of the
three, by both per-repo git grep and a recursive filesystem sweep that also
covered build trees and vendored third-party.

The "34 copies across 17 files" claim checks out exactly. 16 files define
a _STRING helper/wrapper pair (32 defines) plus the ITK_VERSION_TO_STRING0
pair = 34 across 17 files, and _STRING is reserved, so the UB claim holds
for 16 of the 17.

Build: ITK pr/6787 builds clean in the forest — 3814/3814, 0 errors,
ObjectFactory tests 5/5, ITK_LOAD_FUNCTION_NAME:STRING=itkLoad in the cache
and #define ITK_LOAD_FUNCTION_NAME itkLoad in the generated itkConfigure.h.

End-to-end, in a real Slicer SuperBuild. Slicer was built against an ITK
carrying this PR configured with -DITK_LOAD_FUNCTION_NAME=slicer_itkLoad,
with Slicer's plugin updated to spell the entry point via the macro
(Slicer/Slicer#9374). Full
SuperBuild green, and both halves moved together:

plugin  libMRMLIDIOPlugin.dylib   exports:  _slicer_itkLoad   (no _itkLoad)
loader  libITKCommon-6.0.1.dylib  looks up:  slicer_itkLoad   (no "itkLoad")

The negative controls are the point: had the plugin not followed, it would
still export _itkLoad while the loader searched for slicer_itkLoad, and
LoadLibrariesInPath emits nothing on that mismatch.

Caveat worth stating: Slicer defaults to Slicer_USE_IDImageIO:BOOL=OFF, so a
stock Slicer build does not compile that plugin at all. The verification above
required enabling it explicitly.

Two notes on the change itself

This invalidates every ccache entry in the ecosystem. itkMacro.h is
included transitively by essentially every ITK TU and itkConfigure.h by
every consumer, so the forest rebuild is a full 3814-target ITK build with
every downstream project rebuilding behind it. Unavoidable and one-time, but
worth a sentence in the description so a slow CI run is not read as a signal.

Minor: itkFactoryTestLib.h uses ITK_LOAD_FUNCTION_NAME in a declarator
but reaches it transitively via itkObjectFactoryBase.h -> ... ->
itkMacro.h -> itkConfigure.h. An explicit #include "itkConfigure.h"
would be IWYU-correct. Severity is genuinely low: if the macro were undefined,
the declaration, the definition, and the loader's
ITK_STRINGIFY(ITK_LOAD_FUNCTION_NAME) would all agree on the literal token
and the plugin would still load.

hjmjohnson added a commit to Slicer/Slicer that referenced this pull request Aug 25, 2026
itkMRMLIDIOPlugin exported itkLoad by literal name while
ObjectFactoryBase looks the symbol up by the name ITK was configured
with. The two agree only while that name is left at its default.

An ITK built with a non-default ITK_LOAD_FUNCTION_NAME searches for
that symbol and finds nothing here, and LoadLibrariesInPath is silent
when a library exports no entry point, so MRMLIDImageIO would simply
never register.

The fallback is transient, kept only while Slicer supports ITK
releases that predate the configured name. It is guarded on the macro
because ITK main carries version 6.0.0 both before and after the
change, so no ITK_VERSION comparison separates the two; the removal
condition is stated in an ITK_VERSION-bearing comment so the shim is
found by the same search as the rest of the version-conditional code.

Refs: InsightSoftwareConsortium/ITK#6787, InsightSoftwareConsortium/ITK#6786
@hjmjohnson
hjmjohnson force-pushed the comp-itkload-symbol-macro branch from a54b6cd to 8c79fbb Compare August 25, 2026 19:23
@hjmjohnson

Copy link
Copy Markdown
Member Author

The version/build-number idea solves a different problem than the one this is aimed at, and I think the two are composable rather than alternatives.

The target scenario is three independently-distributed ITK builds coexisting in one process — PyPI itk wheels as py_itk, SimpleITK as simple_itk, Slicer as slicer_itk — where the three may be byte-identical ITK versions. That is distribution-channel disambiguation, so anything derived from version or build number cannot separate them; the name has to be configurable rather than computed. Version stamping is a reasonable default policy for the configured value, and nothing here forecloses it.

On Slicer specifically: you're right that #8947 took the problematic ImageIO out of play, but the forest scan found a second site — Libs/MRML/IDImageIO/itkMRMLIDIOPlugin.{h,cxx} still spells itkLoad literally, so a slicer_itk build would leave that plugin exporting itkLoad while the loader looks for slicer_itkLoad, silently. Slicer/Slicer#9374 fixes that side. MITK has the same shape in mitkToolFactoryMacro.h and is unaffected by default.

Agreed on SimpleITK and ITKPython being the sharper exposure, and I don't think this PR settles it either — it makes the entry-point name configurable, which is a precondition, not the answer. Your framing in the EDIT matches how I'd scope it: this is enabling work with no behavior change today. No harm, no current improvement, one of four names that have to be configurable before the larger change is possible.

The four axes, and why the other two matter here
Axis Variable On main
C++ symbols ITK_NAMESPACE / ABI inline namespace no — #6786
autoload C symbol ITK_LOAD_FUNCTION_NAME no — this PR
CMake target prefix ITK_LIBRARY_NAMESPACE yes
library filename ITK_CUSTOM_LIBRARY_SUFFIX yes

Your point on the other issue about the CMake interface namespace turns out to be on the critical path rather than an aside. The filename axis in particular is load-bearing: three shared libraries all named libITKCommon-6.0.so in one process means the loader keys on SONAME and the first one loaded wins, so the C++ symbols can be perfectly distinct and it still will not work. That is the LD_DEBUG=bindings behaviour @jcfr reported in 2022.

Which is also why the open question about whether loading causes broad symbol conflict or only affects object creation is worth settling empirically before the mechanism is chosen — I don't think it has been.

The target scenario and the axis table are now written up in #6786, along with a status table for the pieces already in flight.

ObjectFactoryBase looked up the autoload entry point by string
literal while plugins defined it by name, so the two agreed only
by convention. Both now derive from ITK_LOAD_FUNCTION_NAME,
configured once into itkConfigure.h.

The name is configurable because extern "C" names carry no C++
namespace, so a build that renames ITK's symbols cannot reach this
entry point through the namespace. Setting it in the generated
header keeps the loader and every plugin consistent by
construction.

No behavior change; the default is itkLoad.

Co-Authored-By: Bradley Lowekamp <blowekamp@mail.nih.gov>
@hjmjohnson
hjmjohnson force-pushed the comp-itkload-symbol-macro branch from 8c79fbb to da0658e Compare August 25, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Core Issues affecting the Core module area:IO Issues affecting the IO module type:Enhancement Improvement of existing methods or implementation type:Infrastructure Infrastructure/ecosystem related changes, such as CMake or buildbots type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants