Search code examples
c++cmakedllshared-libraries

In CMake, how can I configure a Windows shared library target installation so that a user executable linking to it will always find it during runtime?


I wrote a small CMake project for a shared library:

add_library(MySharedTarget SHARED "public.h" "implementation.cpp")

target_include_directories(
    MySharedTarget 
    INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> # when using within the build, the include directories is the current source dir
    $<INSTALL_INTERFACE:include> # when used from installation folder, the include directories is the exported path
)
# copy the public headers to the install directory
install(FILES "public.h" DESTINATION include)

# copy the .lib and dll to lib & bin directory
install(
    TARGETS MySharedTarget 
    EXPORT MySharedTarget 
    ARCHIVE DESTINATION lib/$<CONFIG>
    RUNTIME DESTINATION bin/$<CONFIG>
)

# create a targets file that will define the target for use by clients
install(EXPORT MySharedTarget 
  FILE MySharedTargetTargets.cmake
  DESTINATION cmake
)

include(CMakePackageConfigHelpers)

# generate the config file that includes the exports
# this is for find_package to work
configure_package_config_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
  "${CMAKE_CURRENT_BINARY_DIR}/MySharedTargetConfig.cmake"
  INSTALL_DESTINATION "cmake"
  NO_SET_AND_CHECK_MACRO
  NO_CHECK_REQUIRED_COMPONENTS_MACRO
  )

install(
  FILES
  ${CMAKE_CURRENT_BINARY_DIR}/MySharedTargetConfig.cmake
  DESTINATION cmake
  )

All this creates a nice folder hierarchy in the installation path. If this folder is added to the CMAKE_PREFIX_PATH of a different project, find_package and target_link_libraries work as expected.

list(PREPEND CMAKE_PREFIX_PATH
    "my_shared_library_folder"
)

find_package(MySharedLibrary CONFIG REQUIRED)

Compilation of a different project linking this shared library works fine. However, during runtime, it fails to find the shared library (DLL).

So what is the CMake idiomatic way of handling this scenario? Can I tell CMake to copy runtime artifacts to the executable path? Is there a different recommended way?


Solution

  • Disclaimer: I have limited experience with DLLs and the Windows platform. I could very well be wrong here in my conclusion.

    As far as I'm aware, nothing that will "just work" without much effort on the part of the consumer of your library. To read about how DLL searching works on Windows, see https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-unpackaged-apps, which states:

    [...] the search order is as follows:

    1. DLL Redirection.
    2. API sets.
    3. SxS manifest redirection.
    4. Loaded-module list.
    5. Known DLLs.
    6. Windows 11, version 21H2 (10.0; Build 22000), and later. The package dependency graph of the process. This is the application's package plus any dependencies specified as <PackageDependency> in the <Dependencies> section of the application's package manifest. Dependencies are searched in the order they appear in the manifest.
    7. The folder from which the application loaded.
    8. The system folder. Use the GetSystemDirectory function to retrieve the path of this folder.
    9. The 16-bit system folder. There's no function that obtains the path of this folder, but it is searched.
    10. The Windows folder. Use the GetWindowsDirectory function to get the path of this folder.
    11. The current folder.
    12. The directories that are listed in the PATH environment variable. This doesn't include the per-application path specified by the App Paths registry key. The App Paths key isn't used when computing the DLL search path.

    [...] To change the standard search order used by the system, you can call the LoadLibraryEx function with LOAD_WITH_ALTERED_SEARCH_PATH. You can also change the standard search order by calling the SetDllDirectory function.

    If I understand correctly, every solution to the problem you have posed currently requires the user of your library to do something that is complicated enough that CMake just steps back and either won't or can't do anything automatically to lift that burden of choice and implementation from you (your library) and your library's user: modifying their executable to use alternate search order, modifying the way they start their executable to give it a modified PATH variable or CWD, modifying their registry keys, etc.

    Can I tell cmake to copy runtime artifacts to the executable path?

    As far as I know, that's outside the control of your library's installation configuration. Software would need to gain the ability to see the future for that to work. That's something the user of your library could do, though. They would probably use file(COPY) and the $<TARGET_FILE:tgt> and $<TARGET_RUNTIME_DLLS:tgt> generator expressions. I suppose you could create a helper script that defines a helper function for that and make that helper script be part of the installation of your library.

    On platforms that support the RPATH mechanism, CMake can use it where possible to take some of that work off peoples' hands (see the dedicated CMake wiki page for more info), but Windows is not one of those platforms (see Is there a Windows/MSVC equivalent to the -rpath linker flag?).

    Based on what people say in the above linked Q&A, it's a common pattern to have a .bat file which modifies the PATH environment variable and then calls the executable. I suppose you could try to set up a pattern where your library installation comes with a .bat script which adds the directory where your DLL's have been installed-to to the PATH, and then the consumer of your library has their own .bat script that calls your .bat and then runs its executable. That would still require some action on your user's part, but it seems like relatively less. See also the docs for the $<TARGET_RUNTIME_DLL_DIRS:tgt> generator expression.

    Or you could tell your user to modify their system PATH, but there are cases where that would be undesirable. But if that's an acceptable solution, you could even automate that as part of an installer program / script. Alas, installers are out of my area of knowledge, and I'm pretty sure the configuration implementation details for each installer would differ.

    If your library's consumer's maintainer is okay with doing all their own local running and testing of their own executable from an install tree instead of the build tree, I believe they could look into using install(IMPORTED_RUNTIME_ARTIFACTS ...) and install(RUNTIME_DEPENDENCY_SET ...), but again, that'd be out of the hands of your library and its installation configuration.

    I kind of hope I'm wrong and that someone who knows better than me will come correct me and write a better answer.