Shared Library Symbol Visibility

Table of Contents

Symbol visibility controls which names compiled into a shared library (.dll on Windows, .so on Linux, .dylib on macOS) are accessible to other code that links against it. The default in ORE Studio is hidden-by-default: only symbols explicitly annotated for export are visible to consumers. Everything else is an internal implementation detail. Return to External knowledge / Knowledge.

Why it matters

Correct visibility control solves three problems simultaneously:

  • Windows DLL export-table limit. The PE format caps the named-export table at 65 535 entries. A large library that exports every symbol (the lazy approach) can exceed this limit and fail to link.
  • Link-time size and speed. Consumers only see the surface area they need; the linker does not have to resolve thousands of internal names.
  • ABI hygiene. Unexported symbols cannot be accidentally depended on by external code, making it safe to refactor internals without a binary break.

Mechanism

ORE Studio uses boost/config.hpp macros together with a per-library export header. This is the same mechanism already used by ores.qt.api and is the canonical approach.

The export header

Each shared library that exports symbols has an export.hpp file:

// include/ores.X.COMPONENT/export.hpp
#pragma once
#include <boost/config.hpp>

#ifdef ORES_X_COMPONENT_LIBRARY
#  define ORES_X_COMPONENT_EXPORT BOOST_SYMBOL_EXPORT
#else
#  define ORES_X_COMPONENT_EXPORT BOOST_SYMBOL_IMPORT
#endif

BOOST_SYMBOL_EXPORT expands to __declspec(dllexport) on MSVC / Clang-CL and __attribute__((visibility("default"))) on GCC/Clang. BOOST_SYMBOL_IMPORT expands to __declspec(dllimport) on MSVC / Clang-CL and the same visibility attribute on GCC/Clang.

Marking exported types

Apply the macro to every class, function, or variable that must cross the DLL boundary:

#include "ores.X.COMPONENT/export.hpp"

struct ORES_X_COMPONENT_EXPORT my_type { ... };

ORES_X_COMPONENT_EXPORT void my_function();

CMakeLists.txt changes

Two changes are required in the library's src/CMakeLists.txt:

  1. Define the build-side symbol so export.hpp expands to BOOST_SYMBOL_EXPORT:
target_compile_definitions(${lib_target_name} PRIVATE
    ORES_X_COMPONENT_LIBRARY)
  1. Enable the visibility-hidden default on GCC/Clang so non-annotated symbols are hidden automatically:
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    target_compile_options(${lib_target_name} PRIVATE
        -fvisibility=hidden -fvisibility-inlines-hidden)
endif()

Once all libraries carry these annotations, remove the global flag in the root CMakeLists.txt:

# Remove this:
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

Architectural rules

API libraries (ores.X.api)

Export all public-facing types:

  • Domain structs and enums (domain/ headers).
  • Messaging protocol types (messaging/ headers).
  • Generator classes (generators/ headers).
  • Table I/O types (*_table* headers).

These types cross DLL boundaries whenever any consumer links against the library. Mark every class and free function in the public headers with ORES_X_API_EXPORT.

Core libraries (ores.X.core)

Export only the registration entry point.

ores.X.core contains the implementation: repositories, services, mappers, and NATS message handlers.

Export every symbol that crosses a shared-library boundary, determined empirically by linker undefined reference errors. In practice this includes repositories, services, and any helper classes used directly by service executables, test binaries, or ores.cli. Entity structs and mapper helpers that remain internal to the .core library itself do not need export annotations.

The handler-registration entry point is always exported:

ORES_X_CORE_EXPORT void register_message_handlers(
    ores::nats::connection&);

Entity structs (DB row types) and mapper classes are internal implementation details and should not be exported unless a linker error proves otherwise.

Qt plugin DLLs (ores.qt.X)

Qt plugin DLLs load via QPluginLoader. The only required export is the Qt plugin factory function, which Qt's macro system handles automatically via Q_PLUGIN_METADATA and Q_EXPORT_PLUGIN2.

Types shared across Qt plugins must live in ores.qt.api and be exported from there using ORES_QT_API. Plugin-internal types are never exported.

Service libraries and executables (ores.X.service)

Each service project produces two targets: a shared library (ores.X.service.lib) and an executable (ores.X.service.exe).

The library target exports any class whose symbols must be visible to the test binary or to main.cpp after they cross the DLL boundary. In practice this means at minimum:

  • config::parser — called by both main.cpp and the test binary.
  • app::host — called by main.cpp.

Add export.hpp to the project and annotate those classes with ORES_X_SERVICE_EXPORT. Add target_compile_definitions(... PRIVATE ORES_X_SERVICE_LIBRARY) to the lib target in src/CMakeLists.txt.

The executable target (main.cpp only) exports nothing.

CLI and test executables

CLI (ores.cli) and test binaries are executables. They may link against ores.X.core to exercise the DB layer directly, but they do not export symbols.

Dependency rules

A consumer should link against the lowest layer that provides what it needs:

Consumer Link against
Qt plugin ores.X.api
HTTP handler ores.X.api
Wt service ores.X.api
Service executable ores.X.core (and thus ores.X.api transitively)
CLI ores.X.core (exercises DB directly)
Test executable ores.X.core or ores.X.api as needed

Linking a Qt plugin or HTTP handler directly to ores.X.core is an architectural violation: it bypasses the service boundary and couples UI/HTTP code to database implementation details.

See also

Emacs 29.1 (Org mode 9.6.6)