The Windows DLL Question™ has come up numerous times before in one form or another, but it’s cast in a new light by $<TARGET_RUNTIME_DLLS>
, so here’s a fresh take on it.
If you’re like me (and roughly 90% of all CMake users/developers out there are like me, from what I’ve been able to observe in public projects’ source trees), your approach to writing Find modules on Windows has probably been something like this:
- Use the same code on all three desktop platforms
- Let CMake discover
.lib
/.dll.a
import libraries instead of actual DLLs, usingfind_library()
. - End up creating your targets as
UNKNOWN IMPORTED
, because if you try to createSHARED IMPORTED
library targets with only an import library it won’t work, butUNKNOWN IMPORTED
works just fine so meh. - Set the import library as the target’s
IMPORTED_LOCATION
since that seems to work fine. - Call it a day, because hey — everything compiles.
That’s served us all for years (decades, really) so we’ve mostly just accepted it as the way CMake works on Windows.
But now along comes $<TARGET_RUNTIME_DLLS>
. If you’ve tried to actually use it on Windows, you’ve probably discovered is that while all of your CONFIG
-mode package dependencies’ DLLs are captured just fine, the generator expression will cheerfully ignore any targets created from a Find module that’s written like I describe above. …Which is probably most of them. (In my own library build, it was all of them, even the ones I didn’t write.)
For $<TARGET_RUNTIME_DLLS>
to work, the IMPORTED
library target has to be correctly defined as a SHARED
library target, and it needs to have its IMPORTED_
properties set correctly: import lib path in IMPORTED_IMPLIB
, DLL path in IMPORTED_LOCATION
.
So, now I have this new module that uses DLLTOOL.EXE
and its handy -I
flag to get the name of an import library’s DLL, then looks it up using find_program()
. (Simply because find_library()
won’t match DLLs, and I wanted to look on the PATH
. I could’ve used find_file()
but I’m pretty sure I’d have to explicitly give it more paths to search.)
The macro takes one argument, the name of your already-configured CACHE
variable <prefix>_IMPLIB
. (Or <prefix>_IMPLIBS
, it’s pluralization agnostic and will follow whichever form your input uses when naming its output variable.)
The variable whose name you pass to it should already contain a valid path for an import library. Typically that’s set by find_library()
, even though we’ve all been treating them like runtime libraries (DLLs) when they are not.
Armed with find_library(<prefix>_IMPLIB ...)
output, implib_to_dll(<prefix>_IMPLIB)
will attempt to discover and automatically populate the corresponding CACHE
variable <prefix>_LIBRARY
with the path to the import lib’s associated runtime DLL.
With all of the correct cache variables set to the correct values, it’s now possible to properly configure SHARED IMPORTED
library targets on Windows. $<TARGET_RUNTIME_DLLS>
can then be used to discover and operate on the set of DLLs defined by those target(s).
Kind of a pain in the Find, and really does sort of feel like something CMake could be doing at-least-semi-automatically. But, at least for now it works.
Now I just have to rewrite all of my find modules to use it. Sigh.
Here’s my current module code, in case it helps anyone. It’ll almost certainly become part of the libopenshot repo eventually, but I wanted to get it out here now both so that anyone who needs can make use of it, and also so you can tease out the bugs it’s sure to have.
#[=======================================================================[.rst:
IMPLIB_UTILS
------------
Tools for CMake on WIN32 to associate IMPORTED_IMPLIB paths (as discovered
by the :command:`find_library` command) with their IMPORTED_LOCATION DLLs.
Writing Find modules that create ``SHARED IMPORTED`` targets with the
correct ``IMPORTED_IMPLIB`` and ``IMPORTED_LOCATION`` properties is a
requirement for ``$<TARGET_RUNTIME_DLLS>`` to work correctly. (Probably
``IMPORTED_RUNTIME_DEPENDENCIES`` as well.)
Macros Provided
^^^^^^^^^^^^^^^
Currently the only tool here is ``implib_to_dll``. It takes a single
argument, the __name__ (_not_ value!) of a prefixed ``<prefix>_IMPLIB``
variable (containing the path to a ``.lib`` or ``.dll.a`` import library).
``implib_to_dll`` will attempt to locate the corresponding ``.dll`` file
for that import library, and set the cache variable ``<prefix>_LIBRARY``
to its location.
``implib_to_dll`` relies on the ``dlltool.exe`` utility. The path can
be set by defining ``DLLTOOL_EXECUTABLE`` in the cache prior to
including this module, if it is not set implib_utils will attempt to locate
``dlltool.exe`` using ``find_program()``.
Revision history
^^^^^^^^^^^^^^^^
2021-10-14 - Initial version
Author: FeRD (Frank Dana) <ferdnyc@gmail.com>
License: CC0-1.0 (Creative Commons Universal Public Domain Dedication)
#]=======================================================================]
include_guard(DIRECTORY)
if (NOT WIN32)
# Nothing to do here!
return()
endif()
if (NOT DEFINED DLLTOOL_EXECUTABLE)
find_program(DLLTOOL_EXECUTABLE
NAMES dlltool dlltool.exe
DOC "The path to the DLLTOOL utility"
)
if (DLLTOOL_EXECUTABLE STREQUAL "DLLTOOL_EXECUTABLE-NOTFOUND")
message(WARNING "DLLTOOL not available, cannot continue")
return()
endif()
message(DEBUG "Found dlltool at ${DLLTOOL_EXECUTABLE}")
endif()
#
### Macro: implib_to_dll
#
# (Win32 only)
# Uses dlltool.exe to find the name of the dll associated with the
# supplied import library.
macro(implib_to_dll _implib_var)
set(_implib ${${_implib_var}})
set(_library_var "${_implib_var}")
# Automatically update the name, assuming it's in the correct format
string(REGEX REPLACE
[[_IMPLIBS$]] [[_LIBRARIES]]
_library_var "${_library_var}")
string(REGEX REPLACE
[[_IMPLIB$]] [[_LIBRARY]]
_library_var "${_library_var}")
# We can't use the input variable name without blowing away the
# previously-discovered contents, so that's a non-starter
if ("${_implib_var}" STREQUAL "${_library_var}")
message(ERROR "Name collision! You probably didn't pass"
"implib_to_dll() a correctly-formatted variable name."
"Only <prefix>_IMPLIB or <prefix>_IMPLIBS is supported.")
return()
endif()
if(EXISTS "${_implib}")
message(DEBUG "Looking up dll name for import library ${_implib}")
# Check the directory where the import lib is found
get_filename_component(_implib_dir ".." REALPATH
BASE_DIR "${_implib}")
message(DEBUG "Checking import lib directory ${_implib_dir}")
# Add a check in ../../bin/, relative to the import library
get_filename_component(_bindir "../../bin" REALPATH
BASE_DIR "${_implib}")
message(DEBUG "Also checking ${_bindir}")
execute_process(COMMAND
"${DLLTOOL_EXECUTABLE}" -I "${_implib}"
OUTPUT_VARIABLE _dll_name
OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(DEBUG "DLLTOOL returned ${_dll_name}")
find_program(${_library_var}
NAMES ${_dll_name}
HINTS
${_bindir}
${_implib_dir}
PATHS
ENV PATH
)
set(${_library_var} "${${_library_var}}" PARENT_SCOPE)
message(DEBUG "Set ${_library_var} to ${${_library_var}}")
endif()
endmacro()
And here’s an example of its use:
macro(find_ffmpeg_component _component _header)
set(_library "lib${_component}")
# Assume pkg_config check here first, for non-Windows
find_path(FFmpeg_${_component}_INCLUDE_DIR ${_header}
HINTS
${PC_${_component}_INCLUDEDIR}
${PC_${_component}_INCLUDE_DIRS}
PATHS
ENV FFMPEG_DIR
PATH_SUFFIXES
ffmpeg
)
# Windows is "special"
if(WIN32)
find_library(FFmpeg_${_component}_IMPLIB
NAMES ${_library}
PATHS
ENV FFMPEG_DIR
PATH_SUFFIXES
ffmpeg
)
implib_to_dll(FFmpeg_${_component}_IMPLIB)
else()
find_library(FFmpeg_${_component}_LIBRARY
NAMES ${_library}
HINTS
${PC_${_component}_LIBDIR}
${PC_${_component}_LIBRARY_DIRS}
PATHS
ENV FFMPEG_DIR
PATH_SUFFIXES
ffmpeg
)
endif()
set_component_found(${_component})
mark_as_advanced(
FFmpeg_${_component}_INCLUDE_DIR
FFmpeg_${_component}_LIBRARY
FFmpeg_${_component}_IMPLIB
)
endmacro()
# And later...
foreach(_component ${FFmpeg_FIND_COMPONENTS})
if(FFmpeg_${_component}_FOUND AND
NOT TARGET FFmpeg::${_component})
add_library(FFmpeg::${_component} SHARED IMPORTED)
set_property(TARGET FFmpeg::${_component} PROPERTY
INTERFACE_INCLUDE_DIRECTORIES
"${FFmpeg_${_component}_INCLUDE_DIR}")
if(WIN32)
set_property(TARGET FFmpeg::${_component} PROPERTY
IMPORTED_IMPLIB "${FFmpeg_${_component}_IMPLIB}")
endif()
set_property(TARGET FFmpeg::${_component} PROPERTY
IMPORTED_LOCATION "${FFmpeg_${_component}_LIBRARY}")
endif()
endforeach()