基础知识
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
|
但相反,第一个实现从未被调用,因为第二个实现最终会无限循环调用自己。
- printme的第一个实现被创建,并作为该名称的命令提供。之前没有这个名字的命令存在,所以不需要进一步的行动。
- 当遇到printme的第二个实现,CMake找到了之前一个同名的命令,所以它定义了_printme这个名称,指向旧的命令,并设置printme指向新的定义。
- 当遇到printme的第三个实现,同样,CMake找到了一个同名的命令,所以它重新定义了_printme这个名字以指向旧的命令(第二个实现),并设置printme指向新的定义。
当printme被调用时,执行进入第三个实现,它调用_printme。进入了第二个实现,该实现也调用了_printme,但是_printme指向了第二个实现,结果是无限递归。执行过程永远不会到达第一个实现。