From 65aeabe599d3bb41f8a7705b78c004aa8116248e Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Mon, 13 Jul 2026 17:06:44 +0200 Subject: [PATCH 1/4] Re-enable /Zc:inline: CMake until 4.1 was adding it. Makes for a smaller binary (and standard compliance is a good thing anyways) --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 78fce9a5d2..cfe2292d28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -590,6 +590,10 @@ if(MSVC) # warning level 3 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3") + # Remove unreferenced COMDATs (functions/data) before they reach the linker. + # Shrinks object files; does NOT reduce dllexport'd symbol counts (verified empirically). + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:inline") + # warning level 4 - DLM: we should shoot for this #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") From 7e2cfb1070c1e68fc8f2c35525c98524cacdc18c Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Mon, 13 Jul 2026 16:40:17 +0200 Subject: [PATCH 2/4] #5631 - Split openstudiolib.dll into 4 DLLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On `os-4.0-dev`, adding `epmodel` alongside `model` pushes the single `openstudiolib.dll` to ~123.8k unique exported symbols. MSVC's import library (and DLL export ordinal table) has a hard 16-bit limit of 65,535 → `LNK1189`. Removing `model` isn't enough (`epmodel` alone is 60.4k; the pre-epmodel total was already 63.4k). The only fix is splitting into multiple DLLs — **on Windows/MSVC only**; Linux/macOS keep the single `openstudiolib` unchanged. Symbol budget per group (from my dumpbin analysis): | DLL | contents | exported symbols | % of 65,535 | |---|---|---|---| | `openstudioutilitieslib` | utilities | ~6.4k | 10% | | `openstudiomodellib` | model + energyplus | ~53.5k | 82% | | `openstudioepmodellib` | epmodel | ~60.4k | **92%** | | `openstudiolib` (keeps name) | airflow, gbxml, sdd, radiance, gltf, epjson, isomodel, osversion, alfalfa, modelica, measure | ~5.5k | 8% | Rationale for this grouping: - `epmodel` will eventually replace `model` + `energyplus`; `openstudiomodellib` then gets deleted wholesale — no boundary redesign later. - `measure` depends on **both** `model` and `epmodel`, so it (and the auxiliaries) must sit above both — it lives in the top-level `openstudiolib`, which keeps its name so **no consumer changes** (CLI, rubyengine, pythonengine, csharp, workflow, all test targets keep linking `openstudiolib`). - Dependency DAG verified clean (no cycles): `utilities ← {model+energyplus, epmodel} ← auxiliaries/measure`. energyplus is used by epjson/gbxml/sdd (all in the top DLL). epmodel is used only by measure/workflow/cli. Link order is not an issue for DLLs. - ⚠️ `openstudioepmodellib` is born at 92% of the limit and epmodel grows with each E+ release. Not solved here; future mitigation = trim exports (e.g. detail impls) or split epmodel internally. ## Design **Components stay OBJECT libraries on all platforms** — no changes to any `src//CMakeLists.txt` library type. The split is done purely at the composition layer in `src/lib/CMakeLists.txt`: on MSVC, create 4 `SHARED` libs each folding its OBJECT libs `PRIVATE` (same `empty.cpp` pattern as today); on other platforms, keep the current single target. The existing per-component export macros (`UTILITIES_API`, `MODEL_API`, `EPMODEL_API`, … defined via `openstudio__EXPORTS` set PRIVATE per OBJECT lib) already produce correct dllexport/dllimport across DLL boundaries, since each OBJECT lib lands in exactly one DLL. --- CMakeLists.txt | 4 +- src/lib/CMakeLists.txt | 112 +++++++++++++++++++++++++++++++++-------- 2 files changed, 93 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cfe2292d28..3c7627e7e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1675,7 +1675,9 @@ if (MSVC) # within the same library. This may result in less efficient function calls. # The potential inefficiency goes away if we use LTO linking - target_link_libraries(openstudiolib PRIVATE -IGNORE:4217,4049) + foreach(_os_lib openstudiolib openstudiomodellib openstudioepmodellib openstudioutilitieslib) + target_link_libraries(${_os_lib} PRIVATE -IGNORE:4217,4049) + endforeach() if(CMAKE_SIZEOF_VOID_P EQUAL 8) # only applies to 64 bit windows platforms if (BUILD_RUBY_BINDINGS) target_link_libraries(openstudio_rb PRIVATE -IGNORE:4217,4049) diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 84de8f6f92..ce0313594a 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -1,28 +1,94 @@ # Some operating systems don't like compiling a library with # no source files -add_library(openstudiolib SHARED empty.cpp) +if(MSVC) + # MSVC import libraries (and the DLL export ordinal table) are limited to 65,535 exported + # symbols (LNK1189). model + epmodel together export ~123,000 unique symbols, far beyond + # that limit, so on Windows the object libraries are split across four DLLs: + # + # openstudioutilitieslib + # ▲ ▲ + # openstudiomodellib openstudioepmodellib + # ▲ ▲ + # openstudiolib + # + # openstudiolib keeps its name and PUBLIC-links the others so that no consumer + # (CLI, ruby/python/csharp bindings, tests) needs any change. + # Each OBJECT library still lands in exactly one DLL, so the per-component + # openstudio__EXPORTS / _API macros produce correct dllexport/dllimport. + add_library(openstudioutilitieslib SHARED empty.cpp) + target_link_libraries( + openstudioutilitieslib + PRIVATE + openstudio_utilities + ) -target_link_libraries( - openstudiolib - PRIVATE - openstudio_utilities - openstudio_airflow - openstudio_model - openstudio_modelica - openstudio_energyplus - openstudio_epmodel - openstudio_epjson - openstudio_alfalfa - openstudio_measure - openstudio_osversion - openstudio_sdd - openstudio_isomodel - openstudio_gbxml - openstudio_gltf - openstudio_radiance -) + add_library(openstudiomodellib SHARED empty.cpp) + target_link_libraries( + openstudiomodellib + PRIVATE + openstudio_model + openstudio_energyplus + PUBLIC + openstudioutilitieslib + ) + + add_library(openstudioepmodellib SHARED empty.cpp) + target_link_libraries( + openstudioepmodellib + PRIVATE + openstudio_epmodel + PUBLIC + openstudioutilitieslib + ) + + add_library(openstudiolib SHARED empty.cpp) + target_link_libraries( + openstudiolib + PRIVATE + openstudio_airflow + openstudio_modelica + openstudio_epjson + openstudio_alfalfa + openstudio_measure + openstudio_osversion + openstudio_sdd + openstudio_isomodel + openstudio_gbxml + openstudio_gltf + openstudio_radiance + PUBLIC + openstudiomodellib + openstudioepmodellib + ) + + set(openstudio_lib_targets openstudiolib openstudiomodellib openstudioepmodellib openstudioutilitieslib) +else() + add_library(openstudiolib SHARED empty.cpp) + + target_link_libraries( + openstudiolib + PRIVATE + openstudio_utilities + openstudio_airflow + openstudio_model + openstudio_modelica + openstudio_energyplus + openstudio_epmodel + openstudio_epjson + openstudio_alfalfa + openstudio_measure + openstudio_osversion + openstudio_sdd + openstudio_isomodel + openstudio_gbxml + openstudio_gltf + openstudio_radiance + ) + + set(openstudio_lib_targets openstudiolib) +endif() target_link_libraries( openstudiolib @@ -36,7 +102,9 @@ target_link_libraries( fmt::fmt ) -target_compile_definitions(openstudiolib INTERFACE "-DSHARED_OS_LIBS") +foreach(_os_lib IN LISTS openstudio_lib_targets) + target_compile_definitions(${_os_lib} INTERFACE "-DSHARED_OS_LIBS") +endforeach() # We cannot make the libs that make up the shared library public dependency at all, if we do # then we have to export and install them as well, which we probably don't want to do. @@ -59,7 +127,7 @@ target_include_directories( $ ) -install(TARGETS openstudiolib +install(TARGETS ${openstudio_lib_targets} EXPORT openstudio DESTINATION ${LIB_DESTINATION_DIR} COMPONENT "CLI" # CLI is no longer self-contained, it needs it From 58da206644497ef429848149345e57b420ddb1cf Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Mon, 13 Jul 2026 23:56:17 +0200 Subject: [PATCH 3/4] Moving openstudio_osversion into openstudiomodellib since it's mutually dependent with openstudio_model (Model.cpp calls VersionTranslator, and osversion links model_depends). --- src/lib/CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index ce0313594a..f663ff6caa 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -17,6 +17,11 @@ if(MSVC) # (CLI, ruby/python/csharp bindings, tests) needs any change. # Each OBJECT library still lands in exactly one DLL, so the per-component # openstudio__EXPORTS / _API macros produce correct dllexport/dllimport. + # + # openstudio_osversion goes in openstudiomodellib (not the top-level openstudiolib) + # because model/Model.cpp calls osversion::VersionTranslator directly (Model::load), + # while osversion links openstudio_model's own dependency chain -- a genuine mutual + # dependency that only compiles when both land in the same DLL. add_library(openstudioutilitieslib SHARED empty.cpp) target_link_libraries( openstudioutilitieslib @@ -30,6 +35,7 @@ if(MSVC) PRIVATE openstudio_model openstudio_energyplus + openstudio_osversion PUBLIC openstudioutilitieslib ) @@ -52,7 +58,6 @@ if(MSVC) openstudio_epjson openstudio_alfalfa openstudio_measure - openstudio_osversion openstudio_sdd openstudio_isomodel openstudio_gbxml From 27847d6082dcdea2d5e0af1cea31dfa950933e28 Mon Sep 17 00:00:00 2001 From: Julien Marrec Date: Mon, 13 Jul 2026 19:24:27 +0200 Subject: [PATCH 4/4] Fix MSVC LNK2005 duplicate symbols from epmodel double-linking Several targets (measure/epmodel/workflow test executables, python and ruby engine tests, the epmodel python SWIG modules) linked both openstudio_epmodel's raw object files directly and openstudiolib, which now PUBLIC-links openstudioepmodellib on MSVC. That embeds epmodel's symbols twice: once as real definitions, once as the DLL's import-lib thunks, which MSVC rejects. Add an INTERFACE-only openstudio_epmodel_headers target that carries epmodel's per-subfolder include directories without its object files, and use it wherever a consumer only needs to compile against epmodel headers rather than embed its objects. --- python/engine/CMakeLists.txt | 2 +- ruby/engine/CMakeLists.txt | 2 +- src/epmodel/CMakeLists.txt | 19 ++++++++++++++++++- src/epmodel/EPModelSwig.cmake | 7 ++++++- src/measure/CMakeLists.txt | 4 ++-- src/workflow/CMakeLists.txt | 2 +- 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/python/engine/CMakeLists.txt b/python/engine/CMakeLists.txt index 970c7b74f6..4b0bbf6a81 100644 --- a/python/engine/CMakeLists.txt +++ b/python/engine/CMakeLists.txt @@ -71,7 +71,7 @@ if(BUILD_TESTING) set(pythonengine_test_depends openstudio_scriptengine openstudiolib - openstudio_epmodel + openstudio_epmodel_headers fmt::fmt ) diff --git a/ruby/engine/CMakeLists.txt b/ruby/engine/CMakeLists.txt index 335637ac40..8a2b2b734d 100644 --- a/ruby/engine/CMakeLists.txt +++ b/ruby/engine/CMakeLists.txt @@ -255,7 +255,7 @@ if(BUILD_TESTING) set(rubyengine_test_depends openstudio_scriptengine openstudiolib - openstudio_epmodel + openstudio_epmodel_headers fmt::fmt ) diff --git a/src/epmodel/CMakeLists.txt b/src/epmodel/CMakeLists.txt index a0fc88506c..c2e9fa481e 100644 --- a/src/epmodel/CMakeLists.txt +++ b/src/epmodel/CMakeLists.txt @@ -2751,6 +2751,23 @@ set(${target_name}_depends set(${target_name}_depends ${${target_name}_depends} PARENT_SCOPE) target_compile_definitions(${target_name} PRIVATE openstudio_epmodel_EXPORTS SHARED_OS_LIBS) + +# Headers-only INTERFACE target: gives consumers epmodel's per-subfolder include +# directories without pulling in its OBJECT library's actual object files. On MSVC, +# openstudio_epmodel's objects already live in openstudioepmodellib.dll; any consumer +# that links both openstudio_epmodel directly AND openstudiolib (which PUBLIC-links +# openstudioepmodellib) gets the same symbols twice -- once as real definitions, once +# as the DLL's import-lib thunks -- which MSVC rejects (LNK2005). Consumers that only +# need to compile against epmodel headers (not embed its objects) should link this +# instead of openstudio_epmodel. +add_library(openstudio_epmodel_headers INTERFACE) +target_include_directories(openstudio_epmodel_headers INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/HVACComponent + ${CMAKE_CURRENT_SOURCE_DIR}/ModelObject + ${CMAKE_CURRENT_SOURCE_DIR}/scaffolds + ${CMAKE_CURRENT_SOURCE_DIR}/StraightComponent + ${CMAKE_CURRENT_SOURCE_DIR}/ShadingMaterial) + target_include_directories(${target_name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/HVACComponent ${CMAKE_CURRENT_SOURCE_DIR}/ModelObject ${CMAKE_CURRENT_SOURCE_DIR}/scaffolds @@ -3626,7 +3643,7 @@ set(${target_name}_test_src set(${target_name}_test_depends openstudiolib - openstudio_epmodel + openstudio_epmodel_headers ) CREATE_SRC_GROUPS("${${target_name}_test_src}") diff --git a/src/epmodel/EPModelSwig.cmake b/src/epmodel/EPModelSwig.cmake index 4ae77a4f52..dca8e98c93 100644 --- a/src/epmodel/EPModelSwig.cmake +++ b/src/epmodel/EPModelSwig.cmake @@ -159,7 +159,12 @@ macro(make_epmodel_swig_bindings NAME SIMPLENAME KEY_I_FILE I_FILES PARENT_TARGE target_include_directories(${python_target} PRIVATE ${common_swig_include_dirs}) target_include_directories(${python_target} SYSTEM PRIVATE ${Python_INCLUDE_DIRS}) target_compile_definitions(${python_target} PRIVATE SHARED_OS_LIBS SWIG_PYTHON_SILENT_MEMLEAK) - target_link_libraries(${python_target} PUBLIC ${PARENT_TARGET} ${${PARENT_TARGET}_depends}) + # Do NOT link PARENT_TARGET (the openstudio_epmodel OBJECT library) directly here: + # every python binding target already gets the real symbols via openstudiolib + # (linked in python/module/CMakeLists.txt). Linking both directly embeds epmodel's + # object code twice, which MSVC rejects (LNK2005) since its import-lib thunks for + # openstudioepmodellib collide with the directly-linked definitions. + target_link_libraries(${python_target} PUBLIC ${${PARENT_TARGET}_depends}) add_dependencies(${python_target} ${PARENT_TARGET}) if(MSVC) diff --git a/src/measure/CMakeLists.txt b/src/measure/CMakeLists.txt index e2c2e66aa1..bcd9759b44 100644 --- a/src/measure/CMakeLists.txt +++ b/src/measure/CMakeLists.txt @@ -46,7 +46,7 @@ CREATE_SRC_GROUPS("${${target_name}_src}") CREATE_SRC_GROUPS("${${target_name}_swig_src}") set(${target_name}_depends - openstudio_epmodel + openstudio_epmodel_headers ${openstudio_osversion_depends} ) set(${target_name}_depends ${${target_name}_depends} PARENT_SCOPE) @@ -63,7 +63,7 @@ add_dependencies(${target_name} GenerateIddFactoryRun) set(${target_name}_test_depends ${COREFOUNDATION_LIBRARY} openstudiolib - openstudio_epmodel + openstudio_epmodel_headers fmt::fmt ) diff --git a/src/workflow/CMakeLists.txt b/src/workflow/CMakeLists.txt index dfef68f5c2..83874071c1 100644 --- a/src/workflow/CMakeLists.txt +++ b/src/workflow/CMakeLists.txt @@ -27,7 +27,7 @@ add_library(openstudio_workflow Timer.cpp ) -target_link_libraries(openstudio_workflow PUBLIC openstudiolib openstudio_epmodel) +target_link_libraries(openstudio_workflow PUBLIC openstudiolib openstudio_epmodel_headers) if(BUILD_TESTING) set(openstudio_workflow_test_depends