目录

CMake函数和宏

基础知识

CMake的函数和宏与C语言的函数和宏特性相似。函数引入了一个新的作用域,函数参数是函数体内可访问的变量。宏主体在宏调用的地方展开,宏参数只是做简单的字符串替换。一个CMake函数或宏的定义如下:

1
2
3
4
5
6
7
function(name [arg1 [arg2 [...]]])
    # Function body
endfunction()

macro(name [arg1 [arg2 [...]]])
    # Macro body
endmacro()

函数或宏的调用方式与其他CMake命令完全相同。比如:

1
2
3
4
5
6
7
function(print_me)
    message("Hello from inside a function")
    message("All done")
endfunction()

# Called like so:
print_me()

函数或宏的名称应该只包含字母、数字和下划线,不区分大小。

参数处理

对于函数,每个参数都是一个CMake变量。而宏的参数是字符串的替换,如果在if语句中使用宏参数,它将被视为一个字符串,而不是一个变量。比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function(func arg)
    if(DEFINED arg)
        message("Function arg is a defined variable")
    else()
        message("Function arg is NOT a defined variable")
    endif()
endfunction()

macro(macr arg)
    if(DEFINED arg)
        message("Macro arg is a defined variable")
    else()
        message("Macro arg is NOT a defined variable")
    endif()
endmacro()

func(foobar) # Function arg is a defined variable
macr(foobar) # Macro arg is NOT a defined variable

除了这个区别之外,在参数处理方面,函数和宏是一样的。参数的值可以在函数或宏主体中使用通常的变量符号进行访问,尽管宏参数在技术上不是变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function(func myArg)
    message("myArg = ${myArg}")
endfunction()

macro(macr myArg)
    message("myArg = ${myArg}")
endmacro()

func(foobar) # myArg = foobar
macr(foobar) # myArg = foobar

CMake提供了一组和参数相关的变量:

  • ARGC:传递给函数的参数总数。
  • ARGV:包含传递给函数的所有参数的列表。
  • ARGN:只包含命名参数以外的参数的列表。

除此之外,每个单独的参数可以用ARG#形式的名称来引用,其中#是参数的编号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Use a named argument for the target and treat all remaining
# (unnamed) arguments as the source files for the test case
function(add_mytest targetName)
    add_executable(${targetName} ${ARGN})
    target_link_libraries(${targetName} PRIVATE foobar)

    add_test(NAME ${targetName}
        COMMAND ${targetName}
    )
endfunction()

# Define some test cases using the above function
add_mytest(smallTest small.cpp)
add_mytest(bigTest big.cpp algo.cpp net.cpp)

由于宏将其参数视为字符串替换,在宏主体中使用ARGN可能会有意想不到的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# WARNING: This macro is misleading
macro(dangerous)
    # Which ARGN?
    foreach(arg IN LISTS ARGN)
        message("Argument: ${arg}")
    endforeach()
endmacro()

function(func)
    dangerous(1 2)
endfunction()

func(3) # Argument: 3

当把宏主体的内容直接展开到调用的地方时,就很清楚了:

1
2
3
4
5
6
function(func)
    # Now it is clear, ARGN here will use the arguments from func
    foreach(arg IN LISTS ARGN)
        message("Argument: ${arg}")
    endforeach()
endfunction()

关键字参数

许多CMake的内置命令支持基于关键字的参数和灵活的参数排序。用户定义的函数和宏可以通过使用cmake_parse_arguments命令支持同样的灵活性。

1
2
3
4
5
6
include(CMakeParseArguments) # Needed only for CMake 3.4 and earlier
cmake_parse_arguments(prefix
    noValueKeywords
    singleValueKeywords
    multiValueKeywords
    argsToParse)

cmake_parse_arguments接收argsToParse提供的参数,并根据指定的关键字集来处理它们。每个关键字参数都是该函数或宏所支持的关键字名称的列表,所以它们都应该被引号所包围,以确保它们被正确解析。

noValueKeywords定义了独立的关键字参数,其作用类似于布尔开关。每个singleValueKeywords都需要在关键字后面带一个额外的参数,而multiValueKeywords则需要在关键字之后带零个或多个额外参数。关键字惯例是大写字母,用下划线隔开的单词。

当cmake_parse_arguments返回时,对于每个关键字,都会有一个相应的变量,其名称由指定的前缀、下划线和关键字的名称组成。如果在argsToParse中不存在一个特定的关键字,它对应的变量将是空的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function(func)
    # Define the supported set of keywords
    set(prefix ARG)
    set(noValues ENABLE_NET COOL_STUFF)
    set(singleValues TARGET)
    set(multiValues SOURCES IMAGES)

    # Process the arguments passed in
    include(CMakeParseArguments)
    cmake_parse_arguments(${prefix}
        "${noValues}"
        "${singleValues}"
        "${multiValues}"
        ${ARGN})

    # Log details for each supported keyword
    message("Option summary:")
    foreach(arg IN LISTS noValues)
        if(${${prefix}_${arg}})
            message(" ${arg} enabled")
        else()
            message(" ${arg} disabled")
        endif()
    endforeach()

    foreach(arg IN LISTS singleValues multiValues)
        # Single argument values will print as a simple string
        # Multiple argument values will print as a list
        message(" ${arg} = ${${prefix}_${arg}}")
    endforeach()
endfunction()

# Examples of calling with different combinations
# of keyword arguments
func(SOURCES foo.cpp bar.cpp TARGET myApp ENABLE_NET)
func(COOL_STUFF TARGET dummy IMAGES here.png there.png gone.png)

相应的输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Option summary:
    ENABLE_NET enabled
    COOL_STUFF disabled
    TARGET = myApp
    SOURCES = foo.cpp;bar.cpp
    IMAGES =
Option summary:
    ENABLE_NET disabled
    COOL_STUFF enabled
    TARGET = dummy
    SOURCES =
    IMAGES = here.png;there.png;gone.png

作用域

函数和宏之间的一个根本区别是,函数引入了一个新的变量作用域,而宏则没有。在一个函数内定义或修改的变量对函数外的同名变量没有影响。函数并不引入新的策略作用域。set命令的PARENT_SCOPE关键字可以用来修改调用者范围内的变量,而不是函数中的局部变量。

1
2
3
4
5
6
7
8
function(func resultVar1 resultVar2)
    set(${resultVar1} "First result" PARENT_SCOPE)
    set(${resultVar2} "Second result" PARENT_SCOPE)
endfunction()

func(myVar otherVar)
message("myVar: ${myVar}") # myVar: First result
message("otherVar: ${otherVar}") # otherVar: Second result

宏的处理方式与函数相同,通过将变量作为参数传入,指定要设置的变量名称。唯一不同的是,PARENT_SCOPE关键字不应该在宏中使用,因为它已经修改了调用作用域中的变量。

如果在一个函数中调用return,处理过程立即返回给调用者,即跳过函数的其余部分。因为宏没有引入一个新的作用域,所以return语句的行为取决于宏被调用的位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
macro(inner)
    message("From inner")
    return() # Usually dangerous within a macro
    message("Never printed")
endmacro()

function(outer)
    message("From outer before calling inner")
    inner()
    message("Also never printed")
endfunction()

outer()

输出如下:

1
2
From outer before calling inner
From inner

重写命令

当使用function或macro来定义一个新的命令时,如果已经存在一个同名的命令,那么旧命令前面会被加一个下划线,无论旧命令是内置命令还是自定义函数或宏。开发者有时会想利用它,试图像给现有的命令创建一个包装器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function(someFunc)
    # Do something...
endfunction()

# Later in the project...
function(someFunc)
    if(...)
        # Override the behavior with something else...
    else()
        # WARNING: Intended to call the original command, but it is not safe
        _someFunc()
    endif()
endfunction()

预留一个下划线来"保存"之前的命令只适用于当前的名字,它不会递归地应用于所有之前的覆盖。这有可能导致无限递归,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function(printme)
    message("Hello from first")
endfunction()

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

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

printme()

有人可能会认为输出结果如下:

1
2
3
Hello from third
Hello from second
Hello from first

但相反,第一个实现从未被调用,因为第二个实现最终会无限循环调用自己。

  1. printme的第一个实现被创建,并作为该名称的命令提供。之前没有这个名字的命令存在,所以不需要进一步的行动。
  2. 当遇到printme的第二个实现,CMake找到了之前一个同名的命令,所以它定义了_printme这个名称,指向旧的命令,并设置printme指向新的定义。
  3. 当遇到printme的第三个实现,同样,CMake找到了一个同名的命令,所以它重新定义了_printme这个名字以指向旧的命令(第二个实现),并设置printme指向新的定义。

当printme被调用时,执行进入第三个实现,它调用_printme。进入了第二个实现,该实现也调用了_printme,但是_printme指向了第二个实现,结果是无限递归。执行过程永远不会到达第一个实现。