第1章 函数与宏:function()与macro()的区别、参数传递、返回值处理、函数重载模拟、实用函数封装
各位同学,欢迎来到《CMake与Android NDK集成开发从入门到精通》的第一章。今天咱们聊聊CMake里两个特别容易搞混的东西——function()和macro()。
说实话,我刚开始接触CMake那会儿,也觉得这俩玩意儿差不多。直到有一次在NDK项目里封装了一个日志宏,结果变量作用域乱成一锅粥,排查了整整一个下午。嗯,从那以后,我就再也不敢小看这个知识点了。
1.1 function()与macro()的本质区别
先看一个最直观的例子:
# 定义一个函数
function(my_func arg)
set(result "Hello from function: ${arg}")
message("${result}")
endfunction()
# 定义一个宏
macro(my_macro arg)
set(result "Hello from macro: ${arg}")
message("${result}")
endmacro()
# 调用
my_func("World")
my_macro("World")
message("After call: result = ${result}")
猜猜最后一行打印什么?
答案是:After call: result = Hello from macro: World。
为什么?因为function()会创建新的作用域,而macro()直接在调用者的作用域里展开。说白了,function像是一个黑盒子,你扔进去参数,它干完活就退出,里面的变量不会污染外面。macro呢?它就像把代码直接粘贴到调用位置,里面的变量会“泄漏”出来。
核心区别一句话总结:
function()— 有独立作用域,变量隔离macro()— 无独立作用域,变量共享
我在项目中遇到过这样一个坑:用macro封装了一个编译选项设置,结果因为变量泄漏,导致后面好几个target的编译参数都被污染了。从那以后,我给自己定了个规矩——能用function就别用macro。
1.2 参数传递的细节
CMake里参数传递有几个容易踩的点,我一个个说。
1.2.1 位置参数
不管是function还是macro,参数都通过${ARGV}、${ARGN}、${ARGC}这些变量访问:
function(print_args arg1 arg2)
message("arg1 = ${arg1}")
message("arg2 = ${arg2}")
message("All args: ${ARGV}")
message("Extra args: ${ARGN}")
message("Arg count: ${ARGC}")
endfunction()
print_args("Hello" "World" "Extra1" "Extra2")
输出:
arg1 = Hello
arg2 = World
All args: Hello;World;Extra1;Extra2
Extra args: Extra1;Extra2
Arg count: 4
注意${ARGN}只包含超出形参列表的那些参数。这个特性在实现可变参数函数时特别有用。
1.2.2 关键字参数
CMake 3.13之后支持了cmake_parse_arguments,可以像Python一样传关键字参数:
function(configure_target)
set(options STATIC SHARED)
set(oneValueArgs NAME OUTPUT_DIR)
set(multiValueArgs SOURCES DEPENDENCIES)
cmake_parse_arguments(
PARSE_ARGV 0
MY
"${options}"
"${oneValueArgs}"
"${multiValueArgs}"
)
if(MY_STATIC)
add_library(${MY_NAME} STATIC ${MY_SOURCES})
elseif(MY_SHARED)
add_library(${MY_NAME} SHARED ${MY_SOURCES})
endif()
target_link_libraries(${MY_NAME} ${MY_DEPENDENCIES})
set_target_properties(${MY_NAME} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${MY_OUTPUT_DIR}
)
endfunction()
# 调用
configure_target(
NAME mylib
SOURCES src/a.cpp src/b.cpp
DEPENDENCIES log z
SHARED
OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs
)
我个人习惯在封装复杂函数时,一定用关键字参数。为什么?因为可读性太强了。你看调用处,每个参数前面都有名字,别人一看就知道传的是什么。
小技巧:使用PARSE_ARGV 0可以让cmake_parse_arguments自动从${ARGV}里解析参数,不用手动传参。这个特性是CMake 3.7引入的,建议用起来。
1.3 返回值处理
CMake的函数没有return语句返回值这一说。那怎么把结果传出来?
常用的方式有两种:
1.3.1 通过变量名传回
function(get_platform out_var)
if(ANDROID)
set(${out_var} "android" PARENT_SCOPE)
elseif(IOS)
set(${out_var} "ios" PARENT_SCOPE)
else()
set(${out_var} "unknown" PARENT_SCOPE)
endif()
endfunction()
get_platform(platform)
message("Platform: ${platform}")
注意PARENT_SCOPE关键字。因为function有独立作用域,不写这个,变量只在函数内部有效。macro不需要,因为它没有独立作用域。
1.3.2 通过全局属性
function(collect_sources dir)
file(GLOB_RECURSE sources "${dir}/*.cpp" "${dir}/*.h")
set_property(GLOBAL APPEND PROPERTY ALL_SOURCES ${sources})
endfunction()
collect_sources(src/core)
collect_sources(src/ui)
get_property(all_sources GLOBAL PROPERTY ALL_SOURCES)
message("All sources: ${all_sources}")
这种方式适合需要累积多个函数调用结果的场景。我在构建大型NDK项目时,经常用这个模式来收集所有源文件。
1.4 函数重载模拟
CMake不支持函数重载。但我们可以用ARGC和ARGN来模拟:
function(log_message)
if(ARGC EQUAL 1)
message("[INFO] ${ARGV0}")
elseif(ARGC EQUAL 2)
message("[${ARGV0}] ${ARGV1}")
elseif(ARGC EQUAL 3)
message("[${ARGV0}] [${ARGV1}] ${ARGV2}")
else()
message(FATAL_ERROR "log_message: invalid argument count")
endif()
endfunction()
log_message("Hello")
log_message("WARN" "Something went wrong")
log_message("ERROR" "MODULE_A" "Connection failed")
输出:
[INFO] Hello
[WARN] Something went wrong
[ERROR] [MODULE_A] Connection failed
你想想看,这不就是重载的效果吗?虽然CMake没有原生支持,但通过参数个数判断,完全可以实现类似的功能。
注意:这种模拟重载的方式,参数个数判断一定要严谨。我曾经漏掉一个边界条件,结果传了4个参数时静默失败,排查了好久才发现是ARGC判断没覆盖到。
1.5 实用函数封装
最后,分享几个我在NDK项目中常用的封装。这些函数经过实战检验,可以直接拿来用。
1.5.1 安全的消息打印
function(ndk_log)
set(level "INFO")
set(message "")
set(tag "NDK")
if(ARGC GREATER 0)
set(level "${ARGV0}")
endif()
if(ARGC GREATER 1)
set(message "${ARGV1}")
endif()
if(ARGC GREATER 2)
set(tag "${ARGV2}")
endif()
message("[${level}] [${tag}] ${message}")
endfunction()
# 使用
ndk_log("INFO" "Building for ${ANDROID_ABI}" "BUILD")
ndk_log("WARN" "Deprecated API used" "COMPAT")
1.5.2 条件编译开关
function(add_feature_flag name description default)
option(${name} "${description}" ${default})
if(${name})
add_definitions(-D${name}=1)
ndk_log("INFO" "Feature ${name} enabled" "FEATURE")
else()
ndk_log("INFO" "Feature ${name} disabled" "FEATURE")
endif()
endfunction()
# 使用
add_feature_flag("ENABLE_OPENGL" "Enable OpenGL support" ON)
add_feature_flag("ENABLE_VULKAN" "Enable Vulkan support" OFF)
1.5.3 自动收集源文件
function(auto_sources output_var)
set(sources "")
foreach(dir ${ARGN})
file(GLOB_RECURSE dir_sources
"${dir}/*.cpp"
"${dir}/*.c"
"${dir}/*.h"
"${dir}/*.hpp"
)
list(APPEND sources ${dir_sources})
endforeach()
set(${output_var} ${sources} PARENT_SCOPE)
endfunction()
# 使用
auto_sources(MY_SOURCES src/core src/ui)
add_library(mylib SHARED ${MY_SOURCES})
1.6 本章知识体系
下面这张图总结了function和macro的核心区别,以及参数传递、返回值处理的整体脉络:
这张图把function和macro的差异、各自特性、以及最佳实践都串起来了。你可以把它当作一个快速参考,写CMakeLists.txt时瞄一眼,能避免不少坑。
好了,第一章的内容就到这里。function和macro的区别,说白了就是作用域的问题。记住一句话:function是隔离的,macro是透明的。写代码时多想想这个,能省去很多调试时间。
下一章我们会深入CMake的变量作用域机制,到时候你会更深刻地理解为什么function和macro的行为差异这么大。咱们下章见。