Professional CMake:

A Practical Guide

Go beyond trivial examples and learn how to use CMake effectively with practical advice direct from a CMake co-maintainer

 

Do Not Redefine CMake Commands

In this article, we take a closer look at a particular example from the popular Effective CMake talk by Dan Pfeifer. The example in question relies on undocumented behavior, the dangers of which may not be immediately obvious and can lead to infinite recursion.

In the section of the talk where package management is discussed, the following example code is presented (around 52m38s):

set(CMAKE_PREFIX_PATH "/prefix")
set(as_subproject Foo)

macro(find_package)
  if(NOT "${ARG0}" IN_LIST as_subproject)
    _find_package(${ARGV})
  endif()
endmacro()

add_subdirectory(Foo)
add_subdirectory(App)

The intent behind the above block of CMake code is to redefine the built-in find_package() command such that it checks if the package to be found is one of those named in the as_subproject list variable. If it is in that list, then it should be assumed that the package will be added to the project directly via add_subdirectory() and the find_package() call should do nothing. Otherwise, the call should be forwarded to the original built-in find_package() command. It relies on undocumented behavior to call through to the original implementation using _find_package().

(The following explanation is largely extracted from the Functions And Macros chapter of the book Professional CMake: A Practical Guide)

When function() or macro() is called to define a new command, if a command already exists with that name, the undocumented CMake behavior is to make the old command available using the same name except with an underscore prepended. This applies whether the old name is for a built-in command, a custom function or a macro. If a command is only ever overridden once, techniques like in the example above appear to work, but if the command is overridden again, then the original command is no longer accessible. The prepending of one underscore to “save” the previous command only applies to the current name, it is not applied recursively to all previous overrides. This has the potential to lead to infinite recursion, as the following contrived example demonstrates:

function(printme)
    message("Hello from first")
endfunction()

function(printme)
    message("Hello from second")
    _printme()
endfunction()

function(printme)
    message("Hello from third")
    _printme()
endfunction()

printme()

One may naively expect the output to be as follows:

Hello from third
Hello from second
Hello from first

But instead, the first implementation is never called because the second one ends up calling itself in an infinite loop. When CMake processes the above, here is what occurs:

  1. The first implementation of printme() is created and made available as a command of that name. No command by that name previously existed, so no further action is required.
  2. The second implementation of printme() is encountered. CMake finds an existing command by that name, so it defines the name _printme to point to the old command and sets printme to point to the new definition.
  3. The third implementation of printme() is encountered. Again, CMake finds an existing command by that name, so it redefines the name _printme to point to the old command (which is the second implementation) and sets printme to point to the new definition.

When printme() is called, execution enters the third implementation, which calls _printme(). This enters the second implementation which also calls _printme(), but _printme() points back at the second implementation again and infinite recursion results. Execution never reaches the first implementation.

For the find_package() example mentioned earlier, the implications of find_package() being redefined more than once are catastrophic.

  • The original find_package() command becomes permanently inaccessible.
  • Any attempt to call find_package() will result in infinite recursion if _find_package() is called by the new implementation.

In all fairness, in his talk Dan highlighted that he envisioned that the code sample in question would only ever be executed once as part of a package manager. The find_package() macro redefinition would be inserted by a package manager as part of how it incorporated external dependencies under its control. Even if find_package() were only redefined once though, it would still be relying on undocumented CMake behavior which may be modified or removed completely in a future version. Reliance on such behavior should be discouraged and as the above discussion shows, the technique is not safe to use in general.

In a future article, we will also look at another aspect of this example to discuss some potential traps when forwarding command arguments. Those gotchas will add further weight to the recommendation to avoid overriding existing commands.


Get the book for more CMake content

Book cover, Professional CMake: A Practical Guide

Leave a Reply (all comments are moderated)

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: