Skip to content

Defining ImGui config macros in app code silently causes an ODR violation #2411

Description

@totalgee

CinderImGui.h sets IMGUI_USER_CONFIG and pulls in imgui.h, so apps naturally reach ImGui through it. But Cinder's bundled src/imgui/*.cpp are compiled without any app-side configuration. If an app defines an ImGui config macro before including CinderImGui.h, its translation units get a different ImGui struct layout than the one compiled into cinder.lib -- an ODR violation with no diagnostic from the compiler or linker.

In my case:

#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS   // in my own header
#include "cinder/CinderImGui.h"

That macro removes a 32-byte block from ImGuiIO (FontGlobalScale, the legacy clipboard function pointers, and ClipboardUserData). Measured in translation units on both sides:

app TU cinder TU
sizeof(ImGuiIO) 3000 3032
sizeof(ImGuiContext) 10784 10816
offsetof(ImGuiContext, InputTextState) 9208 9240

Symptom

Clicking any text field appeared to focus it for one frame, then focus was lost. InputTextEx() was calling ClearActiveID() from its "ActiveId was set through another widget" edge case, because GetInputTextState(id) returned NULL even though g.InputTextState.ID == id (an "impossible" result, from reading the code ;-).

GetInputTextState() is inline in imgui_internal.h. At /Od (which implies /Ob0), every TU referencing it emits an out-of-line COMDAT, and the linker keeps one arbitrarily. My app's copy -- compiled with the smaller ImGuiIO -- won, so it read InputTextState.ID 32 bytes off. Release builds were unaffected, because /O2 expands the function at each call site and no COMDAT is ever selected.

Why it was so hard to find

  • Latent for a long time. It only surfaced once my app referenced an ImGui internal (ImGui::GetInputTextState()) for custom InputText behaviour. Merely compiling that reference was enough; the code never had to run.
  • Debug-only, so it looked like a compiler or optimisation bug.
  • A git bisect landed on the commit that added the reference, which contained no code on any executed path.

What would have caught it immediately

Calling IMGUI_CHECKVERSION() from app code. It compares the caller's sizeof(ImGuiIO) against the library's and asserts on mismatch.

Note that CinderImGui.cpp:822 already calls it, but from a Cinder TU -- so it can only ever compare Cinder against itself and cannot detect an app-side mismatch.

Suggestions

  1. Document that apps must not define ImGui configuration macros; they belong in include/imgui/imconfig.h, which imgui.h includes in every TU on both sides of the library boundary.
  2. Recommend IMGUI_CHECKVERSION() in app setup (or expose a Cinder helper that performs the check from the caller's TU), so this fails loudly.
  3. Consider adding IMGUI_DISABLE_OBSOLETE_FUNCTIONS to include/imgui/imconfig.h, so obsolete functions are removed consistently for Cinder and user apps.

I don't mind submitting a PR for 3); I'm not sure where one would document 1) and 2).

Related latent hazard

CinderImGuiConfig.h is reached only via IMGUI_USER_CONFIG, which Cinder's own src/imgui/*.cpp never define -- they see only imconfig.h. So CinderImGui.cpp and imgui.cpp are already compiled against different configs. That is harmless today because CinderImGuiConfig.h defines nothing layout-affecting (IM_ASSERT, IMGUI_API, IM_VEC2/4_CLASS_EXTRA), but adding any layout-affecting macro there in future would reproduce this bug inside Cinder itself.

Environment

  • Cinder: commit f90f332 (latest on master branch as of 2026-07-28)
  • ImGui: v1.92.3 (bundled in include/imgui + src/imgui)
  • Windows, MSVC v143 (reproduced on 17.14.35 and 17.14.37), static lib, x64 Debug

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions