Skip to content

Commit 773a5f7

Browse files
Copilotgreenc-FNALknoepfel
authored
Armor shared libraries with symbol visibility; add _internal companions and build-type-aware optimizations (#322)
* Enforces hidden-by-default symbol visibility on Phlex shared libraries * Introduces non-installed `_internal` companion libraries for testing and comparison * Adds optimization flags that automatically exploit the visibility model Specifically: * `Modules/private/PhlexSymbolVisibility.cmake` supplies the CMake machinery for symbol-hiding * `Modules/private/PhlexOptimization.cmake` supplies the CMake machinery for optimization * Non-template classes and free functions intended for public use (with .cpp implementations) are annotated with a `phlex_X_EXPORT` macro * Template classes and header-only code remains unannotated --------- Co-authored-by: Chris Green <greenc@fnal.gov> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: greenc-FNAL <2372949+greenc-FNAL@users.noreply.github.com> Co-authored-by: Kyle Knoepfel <knoepfel@fnal.gov>
1 parent 977f897 commit 773a5f7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+576
-128
lines changed

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,12 @@ set(CTEST_TEST_TIMEOUT 90 CACHE STRING "Per-test timeout (s) for CTest")
8989

9090
# Make tools available
9191
FetchContent_MakeAvailable(Catch2 GSL mimicpp)
92+
# Avoid use of __COUNTER__ to prevent -pedantic warnings in Catch2's TEST_CASE macro
93+
set(CATCH_CONFIG_COUNTER OFF)
9294

9395
include(Modules/private/CreateCoverageTargets.cmake)
96+
include(Modules/private/PhlexSymbolVisibility.cmake)
97+
include(Modules/private/PhlexOptimization.cmake)
9498

9599
option(ENABLE_TSAN "Enable Thread Sanitizer" OFF)
96100
option(ENABLE_ASAN "Enable Address Sanitizer" OFF)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Provides phlex_apply_optimizations(target), which applies safe,
2+
# performance-oriented compiler flags to a Phlex shared library target.
3+
# Also defines the PHLEX_HIDE_SYMBOLS and PHLEX_ENABLE_IPO options and
4+
# documents how they interact.
5+
#
6+
# Two flags are applied (subject to compiler support and platform):
7+
#
8+
# -fno-semantic-interposition (GCC >= 9, Clang >= 8)
9+
# The compiler may assume that exported symbols in this shared library are
10+
# not overridden at runtime by LD_PRELOAD or another DSO. This is the
11+
# natural complement of -fvisibility=hidden: once the exported-symbol
12+
# surface is bounded by explicit export macros, treating those symbols as
13+
# non-interposable allows the compiler to inline, devirtualise, and
14+
# generate direct (non-PLT) calls for same-DSO accesses to exported
15+
# functions.
16+
#
17+
# External plugins continue to call Phlex symbols through the standard
18+
# PLT/GOT mechanism; only code compiled as part of a Phlex shared library
19+
# itself benefits.
20+
#
21+
# Applied only when PHLEX_HIDE_SYMBOLS=ON (export macros are present and
22+
# the exported-symbol set is well-defined).
23+
#
24+
# -fno-plt (GCC >= 7.3, Clang >= 4, ELF platforms)
25+
# Calls FROM a Phlex library TO symbols in other shared libraries (TBB,
26+
# Boost, spdlog, ...) bypass the PLT stub and load the target address
27+
# directly from the GOT. This replaces one level of indirection on every
28+
# cross-DSO call after first resolution. Semantics are unchanged; only
29+
# the dispatch mechanism differs.
30+
#
31+
# Not applied on Apple platforms: Mach-O uses two-level namespaces and the
32+
# PLT abstraction does not map directly to ELF semantics.
33+
#
34+
# Intentionally excluded:
35+
# -ffast-math / -funsafe-math-optimizations
36+
# Physics and numerical code relies on well-defined floating-point
37+
# semantics (NaN propagation, exact rounding, signed-zero behaviour).
38+
# These flags may silently produce incorrect numerical results and are
39+
# therefore not enabled.
40+
#
41+
# Options and their interaction with CMAKE_BUILD_TYPE
42+
#
43+
# Both PHLEX_HIDE_SYMBOLS and PHLEX_ENABLE_IPO are defined here so that their
44+
# defaults can be derived together. The effective defaults are:
45+
#
46+
# CMAKE_BUILD_TYPE PHLEX_ENABLE_IPO PHLEX_HIDE_SYMBOLS
47+
# ───────────────── ─────────────── ─────────────────
48+
# Release ON ON
49+
# RelWithDebInfo ON ON
50+
# Debug / sanitizer OFF OFF
51+
# not set OFF OFF
52+
#
53+
# Both options can be overridden independently on the command line:
54+
#
55+
# -DPHLEX_HIDE_SYMBOLS=ON -DPHLEX_ENABLE_IPO=ON → LTO + -fno-semantic-interposition
56+
# (maximum optimization)
57+
# -DPHLEX_HIDE_SYMBOLS=OFF -DPHLEX_ENABLE_IPO=ON → LTO only; -fno-semantic-interposition
58+
# is NOT applied (valid, useful for
59+
# benchmarking against the ON case)
60+
# -DPHLEX_HIDE_SYMBOLS=ON -DPHLEX_ENABLE_IPO=OFF → -fno-semantic-interposition only
61+
# -DPHLEX_HIDE_SYMBOLS=OFF -DPHLEX_ENABLE_IPO=OFF → no special optimization flags
62+
#
63+
# The per-target flags in phlex_apply_optimizations() self-adjust automatically
64+
# to reflect whichever combination is in effect.
65+
#
66+
# PHLEX_ENABLE_IPO (default ON for Release/RelWithDebInfo, OFF otherwise)
67+
# When ON, enables interprocedural optimization (LTO) for Release and
68+
# RelWithDebInfo configurations. LTO is safe with or without symbol hiding
69+
# because export attributes preserve the complete exported-symbol set.
70+
# External plugins compiled without LTO link against the normal
71+
# exported-symbol table and are unaffected.
72+
#
73+
# PHLEX_HIDE_SYMBOLS (default ON for Release/RelWithDebInfo, OFF otherwise)
74+
# When ON: hidden-by-default visibility; export macros mark the public API.
75+
# When OFF: all symbols visible; _internal targets become thin INTERFACE
76+
# aliases of their public counterparts.
77+
78+
include_guard()
79+
80+
include(CheckCXXCompilerFlag)
81+
82+
# Probe flag availability once at module-load time (results are cached in the
83+
# CMake cache and reused across reconfigures).
84+
85+
if(NOT APPLE)
86+
check_cxx_compiler_flag("-fno-plt" PHLEX_CXX_HAVE_NO_PLT)
87+
88+
# Apple/Clang accepts -fno-semantic-interposition but the Apple toolchain
89+
# driver treats it as unused for Mach-O builds.
90+
check_cxx_compiler_flag("-fno-semantic-interposition" PHLEX_CXX_HAVE_NO_SEMANTIC_INTERPOSITION)
91+
endif()
92+
93+
# ---------------------------------------------------------------------------
94+
# Interprocedural optimization (LTO) — defined first so its value can inform
95+
# the PHLEX_HIDE_SYMBOLS default below.
96+
# ---------------------------------------------------------------------------
97+
cmake_policy(SET CMP0069 NEW)
98+
include(CheckIPOSupported)
99+
100+
if(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo)$")
101+
set(_phlex_ipo_default ON)
102+
else()
103+
set(_phlex_ipo_default OFF)
104+
endif()
105+
106+
option(
107+
PHLEX_ENABLE_IPO
108+
[=[Enable interprocedural optimization (LTO) for Release and RelWithDebInfo
109+
builds. Defaults to ON when CMAKE_BUILD_TYPE is Release or RelWithDebInfo.
110+
Can be combined with PHLEX_HIDE_SYMBOLS=ON for maximum optimization, or with
111+
PHLEX_HIDE_SYMBOLS=OFF to benchmark LTO benefit without symbol hiding.]=]
112+
"${_phlex_ipo_default}"
113+
)
114+
115+
# ---------------------------------------------------------------------------
116+
# Symbol hiding — default follows CMAKE_BUILD_TYPE independently of IPO.
117+
# The two options are orthogonal: both ON/OFF combinations are valid and
118+
# produce different optimization profiles for benchmarking.
119+
# ---------------------------------------------------------------------------
120+
if(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo)$")
121+
set(_phlex_hide_default ON)
122+
else()
123+
set(_phlex_hide_default OFF)
124+
endif()
125+
126+
option(
127+
PHLEX_HIDE_SYMBOLS
128+
[=[Hide non-exported symbols in shared libraries (ON = curated API with
129+
hidden-by-default visibility; OFF = all symbols visible). Defaults to ON for
130+
Release/RelWithDebInfo builds.
131+
When ON, -fno-semantic-interposition is also applied (when supported) because
132+
the exported-symbol surface is explicitly bounded by export macros. This flag
133+
is independent of PHLEX_ENABLE_IPO; either option may be set without the other.
134+
Setting OFF while PHLEX_ENABLE_IPO=ON is valid and useful for comparing LTO
135+
performance with and without symbol hiding.]=]
136+
"${_phlex_hide_default}"
137+
)
138+
139+
# ---------------------------------------------------------------------------
140+
# Activate LTO (if enabled and supported)
141+
# ---------------------------------------------------------------------------
142+
if(PHLEX_ENABLE_IPO)
143+
check_ipo_supported(RESULT _phlex_ipo_supported OUTPUT _phlex_ipo_output LANGUAGES CXX)
144+
if(_phlex_ipo_supported)
145+
# Set defaults for all targets created in this scope and below. The
146+
# *_RELEASE and *_RELWITHDEBINFO variants leave Debug/Coverage/sanitizer
147+
# builds unaffected (those configs override optimization independently).
148+
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
149+
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
150+
message(STATUS "Phlex: LTO enabled for Release and RelWithDebInfo builds")
151+
else()
152+
message(WARNING "Phlex: PHLEX_ENABLE_IPO=ON but LTO is not supported: ${_phlex_ipo_output}")
153+
endif()
154+
endif()
155+
156+
# ---------------------------------------------------------------------------
157+
function(phlex_apply_optimizations target)
158+
# -fno-semantic-interposition pairs with PHLEX_HIDE_SYMBOLS: the compiler
159+
# may only treat exported symbols as non-interposable once the exported-
160+
# symbol surface has been explicitly bounded by export macros.
161+
if(PHLEX_HIDE_SYMBOLS AND PHLEX_CXX_HAVE_NO_SEMANTIC_INTERPOSITION)
162+
target_compile_options("${target}" PRIVATE "-fno-semantic-interposition")
163+
endif()
164+
165+
# -fno-plt reduces cross-DSO call overhead on ELF (Linux) platforms.
166+
if(PHLEX_CXX_HAVE_NO_PLT)
167+
target_compile_options("${target}" PRIVATE "-fno-plt")
168+
endif()
169+
endfunction()
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
include(GenerateExportHeader)
2+
3+
function(phlex_apply_symbol_visibility target)
4+
set(EXPORT_HEADER "${PROJECT_BINARY_DIR}/include/phlex/${target}_export.hpp")
5+
set(EXPORT_MACRO_NAME "${target}_EXPORT")
6+
7+
generate_export_header(
8+
${target}
9+
BASE_NAME ${target}
10+
EXPORT_FILE_NAME ${EXPORT_HEADER}
11+
EXPORT_MACRO_NAME ${EXPORT_MACRO_NAME}
12+
STATIC_DEFINE "${target}_STATIC_DEFINE"
13+
)
14+
15+
if(PHLEX_HIDE_SYMBOLS)
16+
set_target_properties(
17+
${target}
18+
PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON
19+
)
20+
endif()
21+
22+
target_include_directories(${target} PUBLIC $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>)
23+
24+
install(FILES "${EXPORT_HEADER}" DESTINATION include/phlex)
25+
endfunction()
26+
27+
# Create a companion library <target>_internal:
28+
#
29+
# When PHLEX_HIDE_SYMBOLS is ON (default): a non-installed shared library with
30+
# default (visible) symbol visibility, compiled from the same sources as
31+
# <target>. This allows tests to access non-exported implementation details
32+
# without requiring every internal symbol to carry an EXPORT macro, and enables
33+
# before/after comparison of library/executable sizes and link/load times.
34+
#
35+
# When PHLEX_HIDE_SYMBOLS is OFF: an INTERFACE target that simply links to
36+
# <target>. Since all public library symbols are already visible in this mode,
37+
# no separate compilation is needed and _internal targets are effectively
38+
# identical to their public counterparts.
39+
#
40+
# Usage (in the same CMakeLists.txt that defines <target>):
41+
# phlex_make_internal_library(<target> LIBRARIES [PUBLIC ...] [PRIVATE ...])
42+
#
43+
# The LIBRARIES arguments mirror those of the original cet_make_library call but
44+
# may substitute other _internal targets for the corresponding public ones so
45+
# that the full transitive symbol set is visible (PHLEX_HIDE_SYMBOLS=ON only).
46+
function(phlex_make_internal_library target)
47+
cmake_parse_arguments(ARG "" "" "LIBRARIES" ${ARGN})
48+
49+
set(internal "${target}_internal")
50+
51+
if(NOT PHLEX_HIDE_SYMBOLS)
52+
# All public symbols already visible — _internal is a thin INTERFACE wrapper.
53+
add_library(${internal} INTERFACE)
54+
target_link_libraries(${internal} INTERFACE ${target})
55+
return()
56+
endif()
57+
58+
# Retrieve sources and source directory from the public target so we don't
59+
# have to maintain a separate source list.
60+
get_target_property(srcs ${target} SOURCES)
61+
if(NOT srcs)
62+
message(FATAL_ERROR "phlex_make_internal_library: ${target} has no SOURCES property")
63+
endif()
64+
get_target_property(src_dir ${target} SOURCE_DIR)
65+
get_target_property(bin_dir ${target} BINARY_DIR)
66+
67+
# Convert relative paths to absolute. Generated sources (e.g. configure_file
68+
# output) live in the binary directory rather than the source directory.
69+
set(abs_srcs "")
70+
foreach(s IN LISTS srcs)
71+
if(IS_ABSOLUTE "${s}")
72+
list(APPEND abs_srcs "${s}")
73+
elseif(EXISTS "${src_dir}/${s}")
74+
list(APPEND abs_srcs "${src_dir}/${s}")
75+
else()
76+
list(APPEND abs_srcs "${bin_dir}/${s}")
77+
endif()
78+
endforeach()
79+
80+
# Use add_library directly (not cet_make_library) so that cetmodules does not
81+
# register this target for installation or package export.
82+
add_library(${internal} SHARED ${abs_srcs})
83+
84+
if(ARG_LIBRARIES)
85+
target_link_libraries(${internal} ${ARG_LIBRARIES})
86+
endif()
87+
88+
# Cetmodules automatically adds $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}> for
89+
# libraries it manages; replicate that here so consumers (e.g. layer_generator_internal)
90+
# can resolve project headers such as #include "phlex/core/...".
91+
# The _export.hpp headers live in PROJECT_BINARY_DIR/include/phlex.
92+
# Without CXX_VISIBILITY_PRESET hidden the export macros expand to the default
93+
# visibility attribute, making every symbol visible — exactly what we want here.
94+
target_include_directories(
95+
${internal}
96+
PUBLIC
97+
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>"
98+
"$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>"
99+
)
100+
101+
# Propagate compile definitions and options that the public target carries
102+
# (e.g. BOOST_DLL_USE_STD_FS for run_phlex) so the internal build is equivalent.
103+
get_target_property(defs ${target} COMPILE_DEFINITIONS)
104+
if(defs)
105+
target_compile_definitions(${internal} PRIVATE ${defs})
106+
endif()
107+
108+
get_target_property(opts ${target} COMPILE_OPTIONS)
109+
if(opts)
110+
# The _internal library is built with default (interposable) visibility, so
111+
# -fno-semantic-interposition must not be propagated: that flag is only safe
112+
# when -fvisibility=hidden bounds the exported-symbol set (PHLEX_HIDE_SYMBOLS=ON
113+
# on the *public* target), and applying it to fully-visible code violates the
114+
# safety assumption and would make internal/public benchmarks inaccurate.
115+
list(FILTER opts EXCLUDE REGEX "^-fno-semantic-interposition$")
116+
if(opts)
117+
target_compile_options(${internal} PRIVATE ${opts})
118+
endif()
119+
endif()
120+
endfunction()

phlex/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ cet_make_library(
3333
Boost::json
3434
phlex::core
3535
)
36+
phlex_apply_symbol_visibility(phlex_configuration_internal)
37+
phlex_apply_optimizations(phlex_configuration_internal)
3638

3739
cet_make_library(
3840
LIBRARY_NAME

phlex/app/CMakeLists.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ add_library(version_obj OBJECT version.cpp)
55
# version.cpp is generated into the build directory; clang-tidy cannot find
66
# .clang-tidy by walking the build-dir path, so disable linting for this file.
77
set_target_properties(version_obj PROPERTIES CXX_CLANG_TIDY "" POSITION_INDEPENDENT_CODE TRUE)
8-
target_include_directories(version_obj PRIVATE ${PROJECT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/include)
8+
target_include_directories(version_obj PRIVATE ${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}/include)
99

1010
cet_make_library(
1111
LIBRARY_NAME
@@ -26,10 +26,20 @@ cet_make_library(
2626
)
2727

2828
install(FILES load_module.hpp run.hpp version.hpp DESTINATION include/phlex/app)
29+
phlex_apply_symbol_visibility(run_phlex)
30+
phlex_apply_optimizations(run_phlex)
2931

3032
# We'll use C++17's filesystem instead of Boost's
3133
target_compile_definitions(run_phlex PRIVATE BOOST_DLL_USE_STD_FS)
3234

35+
phlex_make_internal_library(
36+
run_phlex
37+
LIBRARIES
38+
PUBLIC phlex_core_internal Boost::json Boost::boost
39+
)
40+
# BOOST_DLL_USE_STD_FS is propagated from run_phlex's COMPILE_DEFINITIONS by
41+
# phlex_make_internal_library, so no explicit repeat is needed here.
42+
3343
cet_make_exec(
3444
NAME phlex
3545
SOURCE phlex.cpp

phlex/app/load_module.hpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#ifndef PHLEX_APP_LOAD_MODULE_HPP
22
#define PHLEX_APP_LOAD_MODULE_HPP
33

4+
#include "phlex/run_phlex_export.hpp"
5+
46
#include "phlex/core/fwd.hpp"
57
#include "phlex/driver.hpp"
68

@@ -10,12 +12,17 @@ namespace phlex::experimental {
1012
namespace detail {
1113
// Adjust_config adds the module_label as a parameter, and it checks if the 'py'
1214
// parameter exists, inserting the 'cpp: "pymodule"' configuration if necessary.
13-
boost::json::object adjust_config(std::string const& label, boost::json::object raw_config);
15+
run_phlex_EXPORT boost::json::object adjust_config(std::string const& label,
16+
boost::json::object raw_config);
1417
}
1518

16-
void load_module(framework_graph& g, std::string const& label, boost::json::object config);
17-
void load_source(framework_graph& g, std::string const& label, boost::json::object config);
18-
driver_bundle load_driver(boost::json::object const& config);
19+
run_phlex_EXPORT void load_module(framework_graph& g,
20+
std::string const& label,
21+
boost::json::object config);
22+
run_phlex_EXPORT void load_source(framework_graph& g,
23+
std::string const& label,
24+
boost::json::object config);
25+
run_phlex_EXPORT driver_bundle load_driver(boost::json::object const& config);
1926
}
2027

2128
#endif // PHLEX_APP_LOAD_MODULE_HPP

phlex/app/run.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#ifndef PHLEX_APP_RUN_HPP
22
#define PHLEX_APP_RUN_HPP
33

4+
#include "phlex/run_phlex_export.hpp"
5+
46
#include "boost/json.hpp"
57

68
#include <optional>
79

810
namespace phlex::experimental {
9-
void run(boost::json::object const& configurations, int max_parallelism);
11+
run_phlex_EXPORT void run(boost::json::object const& configurations, int max_parallelism);
1012
}
1113

1214
#endif // PHLEX_APP_RUN_HPP

0 commit comments

Comments
 (0)