第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不支持函数重载。但我们可以用ARGCARGN来模拟:

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的核心区别,以及参数传递、返回值处理的整体脉络:

CMake 函数与宏知识体系 function() vs macro() function() macro() 特性 • 独立作用域,变量隔离 • 需 PARENT_SCOPE 传值 特性 • 无独立作用域,变量共享 • 直接展开,变量会泄漏 核心建议 能用 function 就别用 macro,除非你明确需要变量泄漏

这张图把function和macro的差异、各自特性、以及最佳实践都串起来了。你可以把它当作一个快速参考,写CMakeLists.txt时瞄一眼,能避免不少坑。


好了,第一章的内容就到这里。function和macro的区别,说白了就是作用域的问题。记住一句话:function是隔离的,macro是透明的。写代码时多想想这个,能省去很多调试时间。

下一章我们会深入CMake的变量作用域机制,到时候你会更深刻地理解为什么function和macro的行为差异这么大。咱们下章见。

公众号:蓝海资料掘金营,微信deep3321