# ============================================================================= # integration/HotReload.cmake # # Everything another CMake project needs to use this library, in one file, # with no dependency on anything else under this repository's own cmake/ # directory (that's internal build machinery for *this* repo -- see # CONTRIBUTING.md -- and pulling it into someone else's build would also # pull in this project's own test/example discovery, output-directory # layout, and Doxygen integration, none of which a consumer asked for). # Reading this file top to bottom is reading the whole integration surface; # there's nothing else to go find. # # What you get, after including this file: # - A `hotreload_lib` target (alias: `HotReload::hotreload_lib`) -- # built from this repository's own source/HotReload/src, so there is # exactly one implementation, never a second copy to drift out of sync. # - hot_reload_enable_patchable_functions() and # hot_reload_restrict_exports() -- identical in behavior to # cmake/HotReload.cmake's own (see that file for the full reasoning # behind each); duplicated here, not include()'d from there, so this # one file has zero path dependency on the rest of this repo's layout. # - hotreload_add_plugin( SOURCES [RESTRICT_EXPORTS]) # -- the one-call version of "build a SHARED library correctly set up # to be hot-reloaded", for when you don't need anything custom about # the target. Skip it and call add_library() yourself, plus the two # functions above, for anything it doesn't cover (EXTRA_SOURCES, # target_compile_definitions, DEPENDS, ...). # # ---- Three ways to get this file into your own CMake project ---- # # 1) Vendor a copy of this repository (git submodule, or just copy the # tree) and include() it directly -- the simplest option, and the one # this file is tuned for by default: # # include(path/to/hotreload/integration/HotReload.cmake) # target_link_libraries(my_app PRIVATE HotReload::hotreload_lib) # hotreload_add_plugin(my_plugin SOURCES src/my_plugin.cpp) # # With no other configuration, this file finds this repository's own # source/HotReload next to itself (see HOTRELOAD_SOURCE_DIR below) -- # nothing else to set up. # # 2) FetchContent, if you'd rather not vendor a copy. Point SOURCE_SUBDIR # at this directory (integration/) rather than the repository root -- # it has no CMakeLists.txt of its own, so FetchContent_MakeAvailable() # populates the source but does NOT add_subdirectory() this # repository's own top-level CMakeLists.txt (that's the documented # behavior of SOURCE_SUBDIR pointing at a directory with no # CMakeLists.txt, CMake 3.18+ -- not a trick or a workaround), so none # of the internal build system this file exists to let you skip ever # runs. Verified directly: configuring this way produces no warning, # deprecated or otherwise, on CMake versions from 3.18 through 4.4. # # include(FetchContent) # FetchContent_Declare(hotreload # GIT_REPOSITORY # GIT_TAG # SOURCE_SUBDIR integration) # FetchContent_MakeAvailable(hotreload) # include(${hotreload_SOURCE_DIR}/integration/HotReload.cmake) # target_link_libraries(my_app PRIVATE HotReload::hotreload_lib) # # (Older CMake without SOURCE_SUBDIR support (pre-3.18): use # FetchContent_Declare() + FetchContent_Populate() instead of # MakeAvailable -- the same idea, fetch without add_subdirectory(), # just via the older API for it.) # # 3) Point HOTRELOAD_SOURCE_DIR at source/HotReload yourself (any layout # at all -- a sparse checkout, a copy with a different folder name, # anything) and include() this file; see the variable below. # # ---- What this deliberately does NOT do ---- # # No install()/export()/find_package() support -- this is a "build it as # part of your own project" integration, not an installed system package. # No BUILD_EXAMPLES/BUILD_TESTS, no Doxygen, no output-directory # redirection, no warning-flag opinions forced on your own targets, and # this file never calls project() or cmake_minimum_required() itself (see # below) -- all deliberate: a consumer's build should only ever notice # this file added what it explicitly asked for. # ============================================================================= include_guard(GLOBAL) # A version check, not a `cmake_minimum_required()` call: this file is # include()'d into the CALLER's own scope (it's not its own subdirectory # with a fresh policy scope), so calling cmake_minimum_required() here # would silently change policy versions for the rest of *your* CMakeLists # too. 3.16 is the floor this file's own features actually need: # FetchContent (3.11+), file(GLOB CONFIGURE_DEPENDS) (3.12+), # target_compile_features(... cxx_std_20) (3.12+), include_guard (3.10+). if(CMAKE_VERSION VERSION_LESS "3.16") message(FATAL_ERROR "integration/HotReload.cmake needs CMake 3.16 or newer (found ${CMAKE_VERSION}).") endif() # ---- Where source/HotReload actually lives ---- # Defaults to right next to this file, since that's where it is in this # repository; override by setting this BEFORE including this file if # you've laid your checkout out some other way. if(NOT DEFINED HOTRELOAD_SOURCE_DIR) get_filename_component(HOTRELOAD_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../source/HotReload" ABSOLUTE) endif() if(NOT EXISTS "${HOTRELOAD_SOURCE_DIR}/include/hot_reload/hot_reload.h") message(FATAL_ERROR "integration/HotReload.cmake: no hot_reload.h under " "'${HOTRELOAD_SOURCE_DIR}/include/hot_reload/'. Set HOTRELOAD_SOURCE_DIR " "to wherever source/HotReload actually is before including this file.") endif() # ============================================================================= # hotreload_lib -- the library itself. # # Mirrors add_lib_module(hotreload_lib TYPE STATIC)'s own behavior # (cmake/BuildHelpers.cmake + source/HotReload/CMakeLists.txt) exactly, # source-globbing included, so this file and the real project build # identical code: src/*.cpp recursively under source/HotReload (including # src/c-api), include/ as the one PUBLIC include directory, STATIC + # position-independent so it's safe to end up inside a SHARED library or # PIE executable later. cxx_std_20 is PRIVATE, not PUBLIC: that's what # this library's own implementation needs internally, but hot_reload.h # itself is plain C-compatible (valid C11 and valid C++14 through C++20+, # confirmed directly by compiling the header alone under each), so this # deliberately does not force C++20 on your own targets just for calling # into this one. # ============================================================================= if(NOT TARGET hotreload_lib) file(GLOB_RECURSE _hotreload_integration_sources CONFIGURE_DEPENDS "${HOTRELOAD_SOURCE_DIR}/src/*.cpp") file(GLOB_RECURSE _hotreload_integration_headers CONFIGURE_DEPENDS "${HOTRELOAD_SOURCE_DIR}/include/*.h" "${HOTRELOAD_SOURCE_DIR}/include/*.hpp") add_library(hotreload_lib STATIC ${_hotreload_integration_sources} ${_hotreload_integration_headers}) add_library(HotReload::hotreload_lib ALIAS hotreload_lib) target_include_directories(hotreload_lib PUBLIC "${HOTRELOAD_SOURCE_DIR}/include") target_compile_features(hotreload_lib PRIVATE cxx_std_20) set_target_properties(hotreload_lib PROPERTIES OUTPUT_NAME hotreload POSITION_INDEPENDENT_CODE ON) # hot_reload::Context uses std::shared_mutex internally (for the thread-safety guarantee # Context.hpp's own doc comment describes), and Demangle.cpp's MSVC branch uses std::mutex -- # both need pthread linked explicitly on Linux with glibc older than 2.34 (newer glibc merged # libpthread into libc itself, making this a no-op there), matching # source/HotReload/CMakeLists.txt exactly. find_package(Threads REQUIRED) target_link_libraries(hotreload_lib PUBLIC Threads::Threads) if(NOT WIN32) # dlopen/dlclose/dlinfo (ModuleHandle.cpp). PUBLIC: for a STATIC # library this needs to reach whatever executable ultimately # links hotreload_lib, matching source/HotReload/CMakeLists.txt. target_link_libraries(hotreload_lib PUBLIC ${CMAKE_DL_LIBS}) endif() # The MSVC *ABI*, not just MSVC command-line syntax: a GNU-style clang++ targeting the # MSVC ABI has MSVC=false but CMAKE_CXX_SIMULATE_ID=MSVC (CMake sets that exactly when the # compiler defines _MSC_VER, the macro Demangle.cpp's own #if keys on) -- see the comment on # the same condition in source/HotReload/CMakeLists.txt for the full reasoning. if(MSVC OR CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC") # UnDecorateSymbolName (Demangle.cpp) lives in dbghelp.lib, which # MSVC does not link by default. Guarded on the MSVC ABI # (cl.exe, clang-cl, or GNU-style clang++ targeting MSVC; false # for MinGW), matching source/HotReload/CMakeLists.txt exactly. target_link_libraries(hotreload_lib PUBLIC dbghelp) endif() unset(_hotreload_integration_sources) unset(_hotreload_integration_headers) endif() # ============================================================================= # hot_reload_enable_patchable_functions() / # hot_reload_restrict_exports() # # Identical behavior to cmake/HotReload.cmake's versions of the same two # functions -- see that file for the full design reasoning behind each # (the 32-byte patchable-entry size, why /hotpatch is the primary safety # mechanism on MSVC specifically, why restrict_exports can't and doesn't # try to quiet std::string/std::vector noise). Guarded so this plays # nicely if a consumer somehow also ends up with the real # cmake/HotReload.cmake included in the same configure. # ============================================================================= if(NOT COMMAND hot_reload_enable_patchable_functions) function(hot_reload_enable_patchable_functions TARGET_NAME) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") target_compile_options(${TARGET_NAME} PRIVATE -fpatchable-function-entry=32,0) elseif(MSVC) target_compile_options(${TARGET_NAME} PRIVATE /hotpatch) else() message(WARNING "hot_reload_enable_patchable_functions(${TARGET_NAME}): " "unrecognized compiler '${CMAKE_CXX_COMPILER_ID}', no patchable-entry " "flag applied. Hot-reloading ${TARGET_NAME} will still work for any " "function whose compiled body happens to be large enough on its own; " "see HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL in hot_reload.h.") endif() endfunction() endif() if(NOT COMMAND hot_reload_restrict_exports) function(hot_reload_restrict_exports TARGET_NAME) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") target_compile_options(${TARGET_NAME} PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden) elseif(MSVC) # MSVC already exports nothing by default; simply not setting # WINDOWS_EXPORT_ALL_SYMBOLS is this function's Windows # equivalent. Nothing to apply here -- see # hotreload_add_plugin()'s RESTRICT_EXPORTS branch below, # which is what actually turns that property off. endif() endfunction() endif() # ============================================================================= # hotreload_add_plugin( SOURCES [RESTRICT_EXPORTS]) # # The one-call version of "build a SHARED library correctly set up to be # hot-reloaded": add_library(... SHARED), patchable function entries, and # an export policy -- exactly the three steps source/HotReload/README.md's # "Using it in your own module" walks through by hand. Does NOT link the # plugin against hotreload_lib: a plugin loaded via dlopen()/LoadLibrary() # doesn't call into this library's API at all (the HOST does), so it only # gets the include directory, for HOT_RELOAD_EXPORT -- matching # source/examples/HotReloadLiveDemo/Plugin/CMakeLists.txt exactly, which # does the same thing by hand for the same reason. # # Default export policy is "export everything" (GCC/Clang/MinGW's own # default, plus WINDOWS_EXPORT_ALL_SYMBOLS so MSVC matches it) -- zero # extra annotation needed to get a working plugin, matching # source/examples/HotReloadDemo's PluginV1/PluginV2. Pass RESTRICT_EXPORTS # for the narrower, recommended-for-a-real-project surface instead (every # intended entry point then needs `extern "C" HOT_RELOAD_EXPORT` -- # hot_reload.h -- or plain `HOT_RELOAD_EXPORT` on a C++ class/method); # matches HotReloadLiveDemo's plugin. # # For anything beyond a plain list of source files -- EXTRA_SOURCES, # target_compile_definitions, linking your own other libraries into the # plugin -- call add_library() yourself and the two functions above # directly instead; this wrapper only covers the common case. # ============================================================================= function(hotreload_add_plugin TARGET_NAME) cmake_parse_arguments(ARG "RESTRICT_EXPORTS" "" "SOURCES" ${ARGN}) if(NOT ARG_SOURCES) message(FATAL_ERROR "hotreload_add_plugin(${TARGET_NAME}): SOURCES is required.") endif() if(ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR "hotreload_add_plugin(${TARGET_NAME}): unrecognized arguments: ${ARG_UNPARSED_ARGUMENTS}") endif() add_library(${TARGET_NAME} SHARED ${ARG_SOURCES}) target_include_directories(${TARGET_NAME} PRIVATE "${HOTRELOAD_SOURCE_DIR}/include") hot_reload_enable_patchable_functions(${TARGET_NAME}) if(ARG_RESTRICT_EXPORTS) hot_reload_restrict_exports(${TARGET_NAME}) set_target_properties(${TARGET_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS OFF) else() set_target_properties(${TARGET_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() endfunction()