# Windows libraries, Find modules, and TARGET\_RUNTIME\_DLLS (re-re-revisited)

**URL:** https://discourse.cmake.org/t/windows-libraries-find-modules-and-target-runtime-dlls-re-re-revisited/4286
**Category:** Code
**Tags:** os:windows
**Created:** [October 15, 2021, 3:32am UTC](https://discourse.cmake.org/t/windows-libraries-find-modules-and-target-runtime-dlls-re-re-revisited/4286 "2021-10-15T03:32:26Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![ferdnyc](https://discourse.cmake.org/user_avatar/discourse.cmake.org/ferdnyc/32/267_2.png) [@ferdnyc](https://discourse.cmake.org/u/ferdnyc)
#### Post date: [October 15, 2021, 3:32am UTC](https://discourse.cmake.org/t/windows-libraries-find-modules-and-target-runtime-dlls-re-re-revisited/4286/1 "2021-10-15T03:32:26Z")

</div>

The Windows DLL Question™ has come up [numerous](https://discourse.cmake.org/t/copying-dependent-dlls-to-executable-directory/852) times [before](https://discourse.cmake.org/t/runtime-dependencies-cannot-find-dll/3782) in [one](https://discourse.cmake.org/t/get-runtime-dependencies-does-not-seem-to-work-with-mingw/2239/3) form or [another](https://discourse.cmake.org/t/find-library-wont-find-dlls/4050), 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:

1. Use the same code on all three desktop platforms
2. Let CMake discover `.lib` / `.dll.a` import libraries instead of actual DLLs, using `find_library()`.
3. End up creating your targets as `UNKNOWN IMPORTED`, because if you try to create `SHARED IMPORTED` library targets with only an import library it won’t work, but `UNKNOWN IMPORTED` works just fine so meh.
4. Set the import library as the target’s `IMPORTED_LOCATION` since that seems to work fine.
5. 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](https://github.com/OpenShot/libopenshot/) 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.

```cmake
#[=======================================================================[.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:

```cmake
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()

```

---

<div class="post-metadata">

### Author: ![ferdnyc](https://discourse.cmake.org/user_avatar/discourse.cmake.org/ferdnyc/32/267_2.png) [@ferdnyc](https://discourse.cmake.org/u/ferdnyc)
#### Post date: [October 15, 2021, 3:44am UTC](https://discourse.cmake.org/t/windows-libraries-find-modules-and-target-runtime-dlls-re-re-revisited/4286/2 "2021-10-15T03:44:33Z")

</div>

> [@ferdnyc](#):
>
> ```cmake
> get_filename_component(_bindir "../../bin" REALPATH
> BASE_DIR "${_implib}")
> 
> ```

I was worried _this_ was a bug, because `"../../bin"` _looks_ wrong… but I just re-tested, and it’s right.

Because I’m passing a **file** path as `BASE_DIR`, `"../bin"` from `C:\msys2\mingw64\lib\libavcodec.dll.a` would produce `C:\msys2\mingw64\lib\bin`.

---

<div class="post-metadata">

### Author: ![craig.scott](https://discourse.cmake.org/user_avatar/discourse.cmake.org/craig.scott/32/20_2.png) [@craig.scott](https://discourse.cmake.org/u/craig.scott)
#### Post date: [October 15, 2021, 10:23pm UTC](https://discourse.cmake.org/t/windows-libraries-find-modules-and-target-runtime-dlls-re-re-revisited/4286/3 "2021-10-15T22:23:59Z")

</div>

Also see [https://gitlab.kitware.com/cmake/cmake/-/issues/22406](https://gitlab.kitware.com/cmake/cmake/-/issues/22406)

---

<div class="post-metadata">

### Author: ![ben.boeckel](https://discourse.cmake.org/letter_avatar_proxy/v4/letter/b/ea5d25/32.png) [@ben.boeckel](https://discourse.cmake.org/u/ben.boeckel)
#### Post date: [October 15, 2021, 11:28pm UTC](https://discourse.cmake.org/t/windows-libraries-find-modules-and-target-runtime-dlls-re-re-revisited/4286/4 "2021-10-15T23:28:10Z")

</div>

See also [this MR](https://gitlab.kitware.com/cmake/cmake/-/merge_requests/5820).

---

<div class="post-metadata">

### Author: ![ferdnyc](https://discourse.cmake.org/user_avatar/discourse.cmake.org/ferdnyc/32/267_2.png) [@ferdnyc](https://discourse.cmake.org/u/ferdnyc)
#### Post date: [October 16, 2021, 9:24am UTC](https://discourse.cmake.org/t/windows-libraries-find-modules-and-target-runtime-dlls-re-re-revisited/4286/5 "2021-10-16T09:24:29Z")

</div>

So, there were a few bugs here.

This needs to be double-quoted:

> [@ferdnyc](#):
>
> ```cmake
> set(_library_var ${_implib_var})
> 
> ```

And these too:

> [@ferdnyc](#):
>
> ```cmake
> if (${_implib_var} STREQUAL ${_library_var})
> 
> ```

Not really a _bug_ per se, but turns out also checking at least the SAME dir as the import library is a very good idea.

> [@ferdnyc](#):
>
> ```cmake
> # Add a check in ../../bin/, relative to the import library
> get_filename_component(_bindir "../../bin" REALPATH
> BASE_DIR "${_implib}")
> message(DEBUG "Also checking ${_bindir}")
> 
> ```

Beyond that, I was playing crazy fast-and-loose with my variable names in the examples and not following [`cmake_developer(7)` § Standard Variable Names](https://cmake.org/cmake/help/latest/manual/cmake-developer.7.html#id5) at _ **all** _.

Now I use `DLLTOOL_EXECUTABLE` instead of `DLLTOOL_COMMAND`, and my `find_foo()` commands fill cache variables named `blah_INCLUDE_DIR`, `blah_IMPLIB`, and `blah_LIBRARY`. (The example skips over the step of populating the plural variable forms of those.)

Everything mentioned here has been corrected in the original post.
