目录
cmake函数和宏基础
demo
cmake函数和宏的参数处理
cmake函数和宏的基本使用
demo
cmake函数和宏使用变量
demo
demo
cmake函数和宏需要注意的地方
demo
cmake函数和宏的关键字参数
demo
使用第二种形式cmake_parse_arguments()
demo
关键字list
demo
singleValueKeywords未传入参数
demo
cmake_parse_arguments()命令解析二级关键字
demo
函数和宏的返回值
从函数返回值
demo
demo
从宏返回值
demo
cmake命令覆盖
demo
函数相关的的特殊变量
demo
cmake函数和宏基础
cmake里面的函数和宏,其行为和C/C++中的函数和宏很像.下面是定义函数和宏的形式:
function(name [arg1 [arg2 [...l]]])
# function body ...
endfunction()
macro(name [arg1 [arg2 [...l]]])
# Macro body
endmacro()
定义完函数和宏以后,这些函数和宏就能像调用cmake内置命令一样调用.
cmake函数没有返回值,如果要返回某个值,需要特殊方式.除了这个在cmake定义函数和宏的时候,对于函数和宏的名字是不区分大小写的,但是有个约定俗成的习惯,都使用小写字母加下划线的形式命名.
demo
cmake_minimum_required(VERSION 3.26)
function(print_me)
message(STATUS "Hello from inside a function")
message(STATUS "All done function")
endfunction()
macro(print_Macro)
message(STATUS "macro: Hello from inside a macro")
message(STATUS "macro: All done macro")
endmacro()
print_me()
print_Macro()
cmake函数和宏的参数处理
cmake函数和宏的参数在处理上基本相同,但是有一个非常重要的区别.
cmake函数把每个参数都当作cmake变量,并且这些参数也都有cmake变量的行为相似的.
cmake宏把参数都当作字符串,这和C/C++中的宏是一致的,宏调用就相当于把宏参数当作字符串替换到宏主体中,这些参数出现的地方.
cmake函数和宏的基本使用
demo
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(marco arg)
if (DEFINED arg)
message("Marco arg is a defined variable")
else()
message("Marco arg is NOT a defined variable")
endif()
endmacro()
func(foobar)
marco(foobar)
从输出我们可以看到,通过if (DEFINED ..)命令分别判断CMake函数和宏的参数是不是已经定义了的变量,cmake的宏没有,这说明cmake宏只把参数当成字符串处理.
除了上述区别.cmake函数和宏参数的处理都支持相同的特性.我们可以使用访问变量的方式在cmake函数和宏主体中访问其参数,虽然cmake宏把参数当作字符串,但也是剋通过变量的方式访问的.
cmake函数和宏使用变量
demo
function(func1 arg)
message("arg = ${arg}")
endfunction()
macro(marco1 arg)
message("arg = ${arg}")
endmacro()
func1(foobar)
marco1(foobar)
从上面的两个例子,我们可以看到,调用cmake函数和宏都需要传入命名的参数,除了我们传入的命名参数,cmake还会为我们生成默认的参数:
- ARGC
- 这个默认参数是一个值,代表的是传递给函数或者宏的所有参数的个数.
- ARGV
- 这个默认参数是一个列表,其中保存的是传递给函数或者宏的所有参数.
- ARGN
- 这个默认参数和ARGV一样,但是它只包含命名参数之外的参数(也就是可选参数和未命名的参数).
除了上述三个默认参数外,每个参数还可以使用ARGVx的形式引用,其中x代表参数的编号,例如ARGV0,ARGV1等等.
demo
function(add_mytest targetName)
add_executable(${targetName} ${ARGN})
target_link_libraries(${targetName} PRIVATE foobar)
add_test(NAME ${targetName}
COMMAND ${targetName})
endfunction()
add_test(smallTest small.cpp)
add_test(bifTest big.cpp alog.cpp net.cpp)
这个例子,我们定义了一个叫做add_test的函数,这个函数有一个命名参数,第一个参数代表添加单元测试的名字,后续的未命名参数是编译这个单元测试需要的源文件,可以是一个也可以多个.
cmake宏在使用ARGN的时候,需要特别注意,
cmake函数和宏需要注意的地方
一个特别容易出错的例子:
demo
macro(dangerous)
foreach(arg IN LISTS ARGN)
message("Argument: ${arg}")
endforeach()
endmacro()
function(func)
dangerous(1 2)
endfunction()
func(3)
结果:
我们希望调用func()函数然后通过宏传入 1 2两个未命名参数,通过foreach打印两个message,但实际上只打印了一个.参数还是3.
其实宏的本质就是字符串替换,如果把宏替换成字符串就好理解了.
替换后的代码:
function(func)
foreach(arg IN LISTS ARGN)
message("Argument: ${arg}")
endforeach()
endfunction()
如果非要用第一种形式,建议使用函数代替宏,函数不会有上面的问题.
cmake函数和宏的关键字参数
如何写出类似于target_link_libraries()这样的函数?
target_link_libraries(targetName
<PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
[<PRIVATE|PUBLIC|INTERFACE> itme3 [item4 ..]]
...
)
对于cmake内置的target_link_libraries()命令,他的作用就是在链接阶段,将某个target连接上.比如我们的一个可执行程序依赖一个库,这时候就需要用到这个命令.
从这个命令(函数)的签名可以看出,它是有关键字参数的,这些关键字是<PRIVATE|PUBLIC|INTERFACE>
如果我们需要编写这样的函数或者宏,cmake给我们提供了另外一个内置的命令:cmake_parse_arguments(),在这个内置命令的帮助下,我们也能写出target_link_libraries()这样的函数或者宏.
cmake_parse_arguments()命令有两种形式,第一种形式所有的cmake版本都支持.
include(CMakeParseArguments)
cmake_parse_arguments(
prefix //前缀
valuelessKeywords singleValueKeywords multiValueKeywords //关键字参数
argsToparse...
)
在cmake3.5版本以前cmake_parse_atguments()命令是CMakeParseArguments这个模板提供的命令,如果想用其中的功能就要先include它.
在cmake3.5版本开始,cmake_parse_atguments()命令已经是一个默认的命令了,不需要再include(CMakeParseArguments)了.
cmake_parse_arguments(
prefix
valuelessKeywords singleValueKeywords multiValueKeywords
argsToparse...
)
第二种形式再cmake3.7版本中引入的,而且只有cmake函数可以使用,cmake宏是不支持这种形式的.
cmake_parse_arguments(
PARSE_ARGV startIndex
prefix
valuelessKeywords singleValueKeywords multiValueKeywords
)
- valuelessKeywords
- 这种关键字参数没有额外的参数,只是一个独立的关键字,再调用函数的时候,有这个关键字和没有这个关键字代表着两种不同的情况,功能类似于传递了一个BOOL类型的命名参数.
- singleValueKeywords
- 这种关键字参数需要一个额外的参数,而且只能有一个.
- multiValueKeywords
- 这种关键字参数后面可以有零个或者多个额外的参数.
以上三个关键字参数在命名方面,虽然没有约定,但是建议都使用大写字母加下划线的形式,另外关键字的参数的名字不要太长,否则可能带来意想不到的后果.
demo
function(funqwer)
set(prefix ARG)
set(noValue ENABLE_NET COOL_STUFF)
set(singleValues TARGET)
set(multiValue SOURCES IMAGES)
include(CMakeParseArguments)
cmake_parse_arguments(
${prefix}
"${noValue}" "${singleValues}" "${multiValue}"
${ARGN}
)
message("Option summary: ")
foreach(arg IN LISTS noValue)
if (${prefix}_${arg})
message(" ${arg} enabled")
else()
message(" ${arg} disabled")
endif()
endforeach()
foreach(arg IN LISTS singleValues multiValue)
message( "${arg} = ${${prefix}_${arg}}")
endforeach()
endfunction()
funqwer(
SOURCES foo.cpp bar.cpp
TARGET MyApp
ENABLE_NET
)
funqwer(
COOL_STUFF
TARGET dummy
IMAGES here.png gone.png
)
使用第二种形式cmake_parse_arguments()
第二种cmake_parse_arguments()命令形式如下:
cmake_parse_arguments(
PARSE_ARGV 0
${prefix}
"${noValues}" "${singleValues}" "${multiValues}"
)
demo
function(funzxc)
set(prefix ARG)
set(noValue ENABLE_NET COOL_STUFF)
set(singleValues TARGET)
set(multiValue SOURCES IMAGES)
# include(CMakeParseArguments)
# cmake_parse_arguments(
# ${prefix}
# "${noValue}" "${singleValues}" "${multiValue}"
# ${ARGN}
# )
cmake_parse_arguments(
PARSE_ARGV 0
${prefix}
"${noValue}" "${singleValues}" "${multiValue}"
)
message("Option summary: ")
foreach(arg IN LISTS noValue)
if (${prefix}_${arg})
message(" ${arg} enabled")
else()
message(" ${arg} disabled")
endif()
endforeach()
foreach(arg IN LISTS singleValues multiValue)
message( "${arg} = ${${prefix}_${arg}}")
endforeach()
endfunction()
message("-------------------------------------")
funzxc(
SOURCES foo.cpp bar.cpp
TARGET MyApp
ENABLE_NET
)
funzxc(
COOL_STUFF
TARGET dummy
IMAGES here.png gone.png
)
结果是一样的.
关键字list
如果传入的valuelessKeywords singleValueKeywords multiValueKeywords三种关键字参数都不能解析,那就会保存一个叫做<prefix>_UNPARSED_ARGUMENTS的变量中,这个变量是个列表.
demo
message("-------------------------------------")
function(funArges)
set(noValue "")
set(singleValues SPECIAL)
set(multiValue EXTRAS)
cmake_parse_arguments(
PARSE_ARGV 0
ARG
"${noValue}" "${singleValues}" "${multiValue}"
)
message("Left-over args: ${ARG_UNPARSED_ARGUMENTS}")
foreach(arg IN LISTS ARG_UNPARSED_ARGUMENTS)
message("${arg}")
endforeach()
endfunction()
funArges(burger fries "chinese;tomato" SPECIAL secretSauce)
上面例子我们特意传入一个包含分号的参数"chinese;tomato",我们使用的是cmake_parese_arguments()命令的第二种形式去解析参数,所有它有效地识别出"chinese;tomato"是一个参数,而不是两个.如果使用cmake_parse_argument()命令的第一种形式,就会不一样.
singleValueKeywords未传入参数
从cmake3.15开始如果没有给singleValueKeywords关键字需要一个额外的参数,就会给一个默认的变量:
<prefix>_KEYWORDS_MISSING_VALUES将会保存这个关键字.
demo
message("-------------------------------------")
function(demoArgs)
set(noValue "")
set(singleValues SPECIAL)
set(multiValue ORDINARY EXTRAS)
cmake_parse_arguments(
PARSE_ARGV 0
ARG
"${noValue}" "${singleValues}" "${multiValue}"
)
message("Keywords missing values: ${ARG_KEYWORDS_MISSING_VALUES}")
endfunction()
demoArgs(burger fries SPECIAL ORDINARY EXTRAS high low)
在这个例子中,因为SPECIAL关键字参数明确需要一个额外的参数,如果没有传递,可能会导致错误.但是cmake不会报错,所以我们需要特别注意.
cmake_parse_arguments()命令解析二级关键字
cmake_parse_arguments()命令解析二级关键字
demo
function(libWithTest)
set(groups LIB TEST)
cmake_parse_arguments(GRP "" "" "${groups}" "${ARGN}")
set(args SOURCES PRIVATE_LIBS PUBLIC_LIBS)
cmake_parse_arguments(LIB "" "TARGET" "${args}" ${GRP_LIB})
cmake_parse_arguments(TEST "" "TARGET" "${args}" ${GRP_TEST})
add_library(${LIB_TARGET} ${LIB_SOURCES})
target_link_libraries(${TEST_TARGET}
PUBLIC ${LIB_PUBLIC_LIBS}
PRIVATE ${LIB_PRIVATE_LIBS}
)
add_executable(${TEST_TARGET} ${TEST_SOURCES})
target_link_libraries(${TEST_TARGET}
PUBLIC ${TEST_PUBLIC_LIBS}
PRIVATE ${LIB_TARGET} ${TEST_PRIVATE_LIBS}
)
endfunction()
message("-------------------------------------")
libWithTest(
LIB
TARGET Algo
SOURCE algo.cpp algo.h
PUBLIC_LIBS SomeMathHelpers
TEST
TARGET AlgoTest
SOURCE algo.cpp algo.h
PRIVATE_LIBS gtest_main
)
这个例子很好的展示通过cmake_parse_arguments()命令解析多级参数的方式.
虽然这些功能相当强大,但是该命令任然有些限制.内置命令能够支持重复关键字.例如像target_link_libraries()这样的命令允许在同一个命令中多次使用PRIVATE PUBLIC INTERFACE关键字,cmake_parse_arguments()命令不支持这一点.它只会返回与关键字最后一次出现相关的值,并丢弃较早的值.只有当使用多级关键字集并且关键字在任何给定的正在处理的参数集中只出现一次时,关键字才能重复使用.
函数和宏的返回值
cmake函数和宏的主要区别,函数会创建一个新的作用域,而宏不会.函数内部定义或修改的变量对函数外部同名的变量没有影响,除非明确需要传播出去,宏和其调用者共享作用域,因此在宏内部定义变量或者修改变量都会影响到外部同名的变量.
从函数返回值
从cmake3.25开始,我们可以在return()命令中使用关键字PROPAGATE关键字来轻松返回值,所以要使用这个语法我们必须将CMP0140这个策略设置为NEW.
cmake_minimum_requierd(VERSION 3.25 FATAL_ERROR)命令最小cmake版本,这条命令会自动为我们将CMP0140这个策略设置为NEW.
demo
message("-------------------------------------")
function(doSomething outVar)
set(${outVar} 42)
return(PROPAGATE ${outVar})
endfunction()
doSomething(result)
message("result = ${result}")
上面的例子,函数调用者希望通过outVar这个变量返回一个值.所以我们只需要在函数内部对这个变量赋值后通过return()命令的PROPAGATE关键字返回就行.
这里有个技巧,为了不让函数中自己定义的变量覆盖调用者作用域内部的变量,通常在return()命令中使用PROPAGATE关键字传播出去的变量都要求定义成函数的命名变量,这样变量的名字和作用完全由调用者决定,函数只是对其值做操作.
如果函数内部还有新的作用域,return()命令时从函数内部型的作用域调用的.
demo
message("-------------------------------------")
function(doSomething1 outVar)
set(${outVar} 42)
block()
set(${outVar} 27)
return(PROPAGATE ${outVar})
endblock()
endfunction()
doSomething1(result1)
message("result1 = ${result1}")
上面使用的return()命令在cmake 3.25及其以后的版本才支持,如果在小于3.25版本可以使用set()命令的PARENT_SCOPE关键字.
function(doSomthing2 re2 re3)
set(${re2} "first result" PARENT_SCOPE)
set(${re3} "second result" PARENT_SCOPE)
endfunction()
message("-------------------------------------")
doSomthing2(re2 re3)
message("re2 = ${re2}")
message("re3 = ${re3}")
从宏返回值
宏可以像函数那样return()特定的变量,通过将他们作为参数传入,来指定要设置的变量名称.唯一的区别是在调用set()命令时不应该在宏中使用PARENT_SCOPE关键字.因为宏已经修改了调用者范围内的变量.
demo
message("-------------------------------------")
macro(mFunc1 mre1 mre2)
set(${mre1} "qwer")
set(${mre2} "asdf")
endmacro()
mFunc1(mre1 mre2)
message("mre1 = ${mre1}")
message("mre2 = ${mre2}")
在宏中使用return()命令一定要小心,因为宏相当于直接将宏主体粘贴到调用的地方,所以return()命令在宏中使用产生的效果与在地方有很大关系.
cmake命令覆盖
cmake命令覆盖就是我们定义新的函数或者宏与已经存在的函数或者宏重名的情况.
在C/C++中,我们可以是使用作用域来区分,在cmake中我们是完全允许这样的操作的,因为我们可能会有这样的操作的需求: 我们需要包装已有的函数或者宏.但是在使用的时候会有一些坑,需要注意.
demo
function(someFunc)
# dosomething
endfunction()
function(someFunc)
if(...)
# ...
else()
_someFunc()
endif()
endfunction()
在这个例子中,我们先定义了someFunc()这个函数,然后紧接着又定义了一个同名的函数,这时候,第二个定义的地方就会覆盖之前定义的地方.
cmake内部实现在遇到这种情况会将第一处定义的someFunc()函数改成_someFunc()函数.这样,新的someFunc()函数就覆盖了旧的someFunc()函数,然后要访问旧的someFunc()函数就可以通过_someFunc()函数来访问.如上面例子所示.
只在覆盖一次的情况下,没什么大问题,但是当覆盖次数多的时候就会陷入死循环.
在有需要覆盖已有命令的情况下,一定要记得只能覆盖一次,千万不要覆盖多次.最佳实践就是不要试图覆盖已有的命令.
函数相关的的特殊变量
这些变量在cmake3.17版本以后才开始支持.所以使用这些变量保证cmake最小版本是3.17.
- CMAKE_CURRENT_FUNCTION
- 这个变量会保存当前正在执行的函数名字
- CMAKE_CURRENT_FUNCTION_LIST_FILE
- 这个变量会保存当前正在执行的函数所在文件的绝对路径
- CMAKE_CURRENT_FUNCTION_LIST_DIR
- 这个变量会保存当前正在执行的函数所在文件的目录的绝对路径
- CMAKE_CURRENT_FUNCTION_LIST_LINE
- 这个变量会保存当前正在执行的函数的行号
除了在调试的时候用到这些变量,我们在调用函数的时候,如果在函数内部需要使用路径的时候,这些变量也是非常有用的,
demo
function(writeSomeFile toWhere)
configure_file(${CMAKE_CURRENT_FUNCTION_LIST_DIR}/template.cpp.in ${toWhere} @ONLY)
endfunction()
这里的config_file()命令的作用是把${CMAKE_CURRENT_FUNCTION_LIST_DIR}/template.cpp.in文件通过一些规则转换成另一个文件:${toWhere}.这个例子中能够通过CMAKE_CURRENT_FUNCTION_LIST_DIR这个变量准确的定位template.cpp.in的路径,如果没有这个变量是支持,还有中一种实现:
set(__writeSomeFile_DIR ${CMAKE_CURRENT_LIST_DIR})
function(writeSomeFile toWhere)
configure_file(__writeSomeFile_DIR/template.cpp.in ${toWhere} @ONLY)
endfunction()
这种实现就严重依赖与__writeSomeFile_DIR变量,使用起来灵活度有所下降.