본문 바로가기
CMake

[CMake] Functions and Macros

by 별준 2021. 11. 1.

References

  • Professional CMake : A Practical Guide

Contents

  • Functions and Macros
  • Argument 특징
  • Keyword Arguments (using cmake_parse_arguments())
  • Overriding functions/macros

CMake의 함수와 매크로는 C/C++에서의 함수와 매크로와 매우 유사한 특성을 가지고 있습니다. 함수는 새로운 scope를 가지고 함수의 argument들은 함수 내부(body)에서 참조할 수 있는 변수가 됩니다.

반면에 매크로는 호출 지점으로 매크로 본문(body)을 복사하고, 매크로의 argument는 간단한 문자열로 대체됩니다.

이러한 동작들은 C/C++에서 함수와 #define으로 정의되는 매크로의 방식을 그대로 따라갑니다. 그리고 다른 프로그래밍 언어에서의 역할과 마찬가지로 이 함수와 매크로는 프로젝트와 개발자를 위해 CMake의 기능을 확장하고 반복적인 작업들을 효율적으로 할 수 있도록 해주는 주요 메커니즘입니다.

 


Functions and Macros

CMake의 함수와 매크로는 아래의 문법으로 사용됩니다.

함수와 매크로는 일단 정의되면 다른 CMake 명령과 똑같은 방식으로 호출됩니다. 호출되면, 그 지점에서 함수 또는 매크로의 본문이 실행됩니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

function(print_me)
    message("Hello from inside a function")
    message("All done")
endfunction()

# Called like as:
print_me()

 

함수와 매크로의 name argument(함수명 또는 매크로명)은 호출하는데 사용되는 이름을 정의하며, 문자, 숫자 및 밑줄만 포함해야합니다. 이름은 대소문자를 구분하지 않고 처리되므로, 대/소문자 규칙은 스타일대로하면 됩니다.

(CMake 문서에는 명령어 이름은 모두 소문자라는 규칙을 따른다고 되어있습니다.)

그리고 함수와 매크로의 끝에는 endfunction() / endmacro()로 마무리합니다.

 

그리고 함수를 호출할 때에는 대소문자를 구분하지 않고, cmake_language를 통해서 호출할 수도 있습니다.

 


Argument 특징

함수와 매크로의 argument 처리는 한 가지 중요한 차이점을 제외하고 동일합니다. 

함수에서 각 argument는 CMake의 변수이며, CMake의 변수의 특징을 그대로 가집니다. 예를 들어, 함수 내 if() 문에서 변수를 테스트할 수 있습니다.

반면에 매크로의 argument는 문자열 대체이므로, argument로 사용된 어떤 것이든지 본질적으로는 매크로의 본문(body)에 argument가 나타나는 모든 곳에 붙여넣어 집니다. 즉, 매크로의 if() 문에서 매크로 argument를 사용하면 변수가 아닌 문자열로 처리되는 것입니다.

예제를 통해 살펴보도록 하겠습니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

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)
macr(foobar)

위 결과를 보면, foobar가 arg 변수의 값으로 입력될 것이고, 매크로는 단순 문자열로 그냥 대체되고 있습니다. 

즉, 함수의 경우 argument가 전달받은 값을 저장하는 변수 역할을 하는 것이고, 매크로는 본문 내의 argument 이름이 전달받은 값으로 대체가 되는 것입니다. (매크로 내의 arg가 foobar로 대체됨)

 

하지만 매크로의 argument가 변수로 취급이 되지는 않더라도, 일반적인 변수 표기법을 사용하여 매크로 본문에서 해당 인수의 값에 접근할 수 있습니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

function(func myArg)
    message("myArg = ${myArg}")
endfunction()

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

func(foobar)
macr(foobar)

함수와 매크로 모두 정상적으로 전달받은 값을 변수 표기법으로 접근하고 있습니다.

 

+) 함수와 매크로에는 명명된 argument 이름 외에도 argument를 처리할 수 있는 자동으로 정의된 변수들을 사용할 수 있습니다. 

  • ARGC : 전달된 argument의 수입니다. 명명된 argument와 명명되지 않은 argument들을 모두 포함합니다.
  • ARGV : 명명된 argument와 명명되지 않은 argument들을 모두 포함하여 전달된 argument를 포함하는 리스트 변수입니다.
  • ARGN : 명명되지 않은 argument만 포함한다는 것을 제외하고, ARGV와 동일합니다. (리스트 변수)

위 3개의 변수 외에도, 전달된 argument들은 ARGV# 형식의 이름으로 참조할 수 있습니다. 여기서 #은 argument의 번호 입니다.(ex, ARGV0, ARGV1) ARG#은 명명된 argument들도 포함되어, 첫 번째 argument를 ARGV0 이름으로 접근할 수 있습니다. (ARGV50과 같이 전달되지 않은 argument는 빈 문자열로 취급됩니다.)

cmake_minimum_required(VERSION 3.10)
project(functions)

function(func myArg)
    message("ARGC = ${ARGC}")
    message("ARGV = ${ARGV}")
    message("ARGN = ${ARGN}")
    message("myArg = ${myArg}")
    message("ARGV0 = ${ARGV0}")
    message("ARGV1 = ${ARGV1}")
    message("ARGV50 = ${ARGV50}")
endfunction()

macro(macr myArg)
    message("ARGC = ${ARGC}")
    message("ARGV = ${ARGV}")
    message("ARGN = ${ARGN}")
    message("myArg = ${myArg}")
    message("ARGV0 = ${ARGV0}")
    message("ARGV1 = ${ARGV1}")
    message("ARGV50 = ${ARGV50}")
endmacro()

func(foobar a b c)
message("======")
macr(foobar a b c)

 

ARGN 변수는 유용하게 사용할 수 있는데, 이는 함수나 매크로가 가변 개수의 argument를 취할 수 있도록 하면서 제공되어야하는 argument를 지정할 수 있습니다. 

하지만 예상치못한 동작이 발생할 수 있는 예외가 존재할 수 있습니다.

아래 예시를 살펴보겠습니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

# 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)

1 2가 출력될 것으로 예상했지만, 3이 출력되고 있습니다. foreach 문에서 LISTS 키워드를 함께 사용할 때 변수 이름을 제공해야하지만, 매크로의 ARGN는 변수 이름이 아닙니다. 매크로가 다른 함수 내에서 호출되면 매크로는 매크로 자체의 ARGN이 아닌 매크로를 호출하는 함수의 ARGN 변수를 사용하게 됩니다. 

이런 경우에는 매크로를 아래처럼 호출해야 정확한 의도로 동작합니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

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

func(1 2)

이런 경우가 발생할 수 있기 때문에 매크로를 사용할 때는 유의해야 합니다.


Keyword Arguments

방금까지 ARG... 변수들을 사용하여 argument의 변수 집합을 처리하는 방법에 대해서 알아봤습니다. 이 기능은 하나의 변수 또는 전체 arguments/명명되지 않은 arguements 집합만 필요한 경우에는 충분하지만, 선택적으로 argument들의 집합을 지원해야하는 경우에는 처리가 상당히 까다로워집니다.

게다가 기본적인 argument 처리는 키워드-베이스 arguments와 유연한 arguments 순서를 지원하는 CMake의 내장된 명령들과 비교하면 꽤 엄격합니다.

 

한 가지 예시로, CMake에서 제공되는 target_link_libraries() 명령을 살펴보겠습니다.

이 명령은 첫 번째 인수로 targetName이 필요하지만, 그 이후에는 호출하는 쪽에서 원하는 수의 PRIVATE, PUBLICL 또는 INTERFACE 섹션을 순서에 관계없이 제공할 수 있고, 각 섹션에서 항목의 수에 제한이 없습니다. 이런 명령처럼 함수와 매크로에서도 cmake_parse_arguments() 명령을 사용하면 동일하게 이 기능을 제공할 수 있습니다.

해당 명령어는 CMake 3.5에서 기본 제공되는 명령어로 포함되었으며, 이전 버전에서는 CMakeParseArguments 모듈을 include 해주어야 사용할 수 있습니다.

(CMake 버전에 민감한 프로젝트라면 위의 형태로 사용하면 문제없이 모든 버전에서 사용할 수 있도록 해줍니다.)

 

우선 cmake_parse_arguments()의 arguments들에 대해서 알아보겠습니다.

argsToParse는 매개변수로 제공된 argument들을 가져와서 지정된 키워드 세트에 따라 처리합니다. 일반적으로 argsToParse는 ${ARGN}으로 지정되며, 이는 함수 또는 매크로에 전달되는 명명되지 않은 argument 입니다. 각 키워드 argument(noValueKeywords, singleValueKeywords, multiValueKeywords)는 해당 함수 또는 매크로에서 지원하는 키워드의 이름(리스트 변수)이며, 올바르게 파싱되도록 따옴표로 묶어야 합니다.

 

noValueKeywords는 bool switch처럼 동작하는 키워드 arguments를 정의합니다. 

singleValueKeywords는 사용될 때 키워드 뒤에 정확히 하나의 추가 argument가 필요하고, 반면에 multiValueKeywords는 키워드 뒤에 0개 이상의 추가 argument가 필요합니다.

필수는 아니지만 키워드 모두 대문자로 하고 필요한 경우 밑줄로 단어를 구분하는 것이 일반적입니다.

 

그리고 cmake_parse_arguments()가 반환되면, 모든 키워드에 대해 지정된 [접두사(prefix) + "_" + 키워드 이름]으로 구성된 해당 변수를 사용할 수 있습니다. 예를 들어, "ARG"라는 prefix를 사용하면 "FOO"라는 키워드에 해당하는 변수는 "ARG_FOO"가 됩니다. 특정 키워드가 argsToParse에 없으면 해당 변수는 비어 있습니다.

 

예제로 더 자세히 살펴보겠습니다.


cmake_minimum_required(VERSION 3.10)
project(functions)

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)

 

다음은 또 다른 예시입니다. 해당 예시에서는 파싱된 argument들이 어떤 변수에 저장되는지 잘 보여주고 있습니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

macro(my_install)
    set(options OPTIONAL FAST)
    set(oneValueArgs DESTINATION RENAME)
    set(multiValueArgs TARGETS CONFIGURATIONS)
    cmake_parse_arguments(MY_INSTALL
                          "${options}"
                          "${oneValueArgs}"
                          "${multiValueArgs}"
                          ${ARGN})

    message("MY_INSTALL_OPTIONAL = ${MY_INSTALL_OPTIONAL}")
    message("MY_INSTALL_FAST = ${MY_INSTALL_OPTIONAL}")
    message("MY_INSTALL_DESTINATION = ${MY_INSTALL_DESTINATION}")
    message("MY_INSTALL_RENAME = ${MY_INSTALL_RENAME}")
    message("MY_INSTALL_TARGETS = ${MY_INSTALL_TARGETS}")
    message("MY_INSTALL_CONFIGURATIONS = ${MY_INSTALL_CONFIGURATIONS}")
    message("MY_INSTALL_UNPARSED_ARGUMENTS = ${MY_INSTALL_UNPARSED_ARGUMENTS}")
    message("MY_INSTALL_KEYWORDS_MISSING_VALUES = ${MY_INSTALL_KEYWORDS_MISSING_VALUES}")
endmacro()

my_install(TARGETS
               foo bar
           DESTINATION
               bin
           OPTIONAL
               blub
           CONFIGURATIONS)

argument들이 파싱되어 "MY_INSTALL" 이라는 prefix를 가진 여러 변수들이 정의된 것을 볼 수 있습니다.

 

cmake_parse_arguments()은 아래의 장점들을 갖고 있습니다.

  • 키워드 기반이기 때문에 호출하는 쪽에서 가독성이 올라갑니다. 해당 함수를 사용하려는 개발자가 각 인수가 의미하는 바를 찾기 위해서 문서를 따로 살펴볼 필요가 없습니다.
  • argument 순서에 연연하지 않을 수 있습니다.
  • 제공할 필요가 없는 argument들은 생략할 수 있습니다.
  • 지원되는 각 키워드는 cmake_parse_arguments()에 전달되어야 하고, 일반적으로 함수의 상단 근처에서 호출되므로 일반적으로 함수가 지원하는 argument가 명확합니다.
  • 키워드 기반 argument는 ad hoc이나 메뉴얼로 코딩된 파서가 아닌 cmake_parse_arguments()에 의해서 처리되므로, argument 관련 버그는 사실상 없다고 간주할 수 있습니다.

 


Scope

함수와 매크로의 근본적인 차이점은 함수는 새로운 scope를 생성하고 매크로는 그렇지 않다는 것입니다. 함수 내부에서 정의되거나 수정된 변수는 함수 외부에서 같은 이름의 변수에 영향을 미치지 않습니다.

 

C/C++과는 달리 CMake의 함수 및 매크로에서 값 반환은 지원하지 않습니다. 또한 함수는 자체 변수 scope를 생성하므로 호출하는 쪽으로 정보를 다시 전달하는 방법이 없는 것처럼 보일 수 있지만, 그렇지는 않습니다. 이전에 add_subdirectory()를 설명할 때 이야기한 것처럼 동일한 방법을 함수에도 적용할 수 있습니다.

(2021.10.31 - [CMake] - [CMake] add_subdirectory() 와 변수 Scope)

즉, set() 명령의 PARENT_SCOPE 키워드를 사용하여, 함수 내의 지역 변수가 아닌 호출한 부모 scope에 존재하는 변수를 수정할 수 있습니다. 함수에서 값을 반환하는 것과는 다르지만, (하나 혹은 여러개의)값을 호출하는 쪽으로 다시 전달할 수 있습니다.

 

값을 부모 scope에 전달하는 일반적인 방법은 다음과 같습니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

function(func resultVar1 resultVar2)
    set(${resultVar1} "First result" PARENT_SCOPE)
    set(${resultVar2} "Second result" PARENT_SCOPE)
endfunction()

func(myVar otherVar)
message("myVar = ${myVar}")
message("otherVar = ${otherVar}")

위의 방법은 함수에 전달된 argument 값을 호출하는 부모 scope의 변수 이름으로 사용할 수 있도록 하는 것입니다. 이는 cmake_parse_arguments()에서 사용하는 접근 방식입니다.

 

또 다른 방법은 호출하는 쪽에서 변수 이름을 지정하도록 하지 않고, 함수 내에서 설정된 변수 이름을 사용하는 것입니다. 즉, 함수 내에서 특정 이름의 변수를 set() 하는 것인데, 이는 함수의 유연성을 없애고 변수 이름 충돌의 가능성이 발생할 수 있기 때문에 가능한 지양하는 방법입니다.

 

 

매크로도 argument로 설정할 변수의 이름을 전달하여 함수와 같은 방식으로 처리할 수 있습니다.

유일한 차이점은 매크로는 이미 부모 scope에 존재하기 때문에 PARENT_SCOPE 키워드를 사용하면 안됩니다.

(실제로 함수 대신 매크로를 대부분의 이유는 부모 scope의 많은 변수들을 설정해야하는 경우입니다.)

cmake_minimum_required(VERSION 3.10)
project(functions)

macro(macr resultVar1 resultVar2)
    set(${resultVar1} "First result")
    set(${resultVar2} "Second result")
endmacro()

macr(myVar otherVar)
message("myVar = ${myVar}")
message("otherVar = ${otherVar}")

 

 

scope에 관한 마지막 설명으로는 return() 명령이 있습니다. 이전 게시글에서 서브 디렉토리의 처리를 중단하는 것으로 설명했었는데, 이처럼 return()은 값을 반환하지 않고 현재 처리를 종료하고 상위 scope로 반환합니다.

만약 함수 내에서 return()이 호출되면 함수 수행이 즉시 중단되고 호출자에게 반환됩니다. 따라서, 함수의 나머지 부분은 스킵하게 됩니다. 

반면에 매크로 내에서 return()의 동작은 함수와 다릅니다. 매크로는 새로운 scope를 생성하지 않기 때문에 return() 문의 동작은 매크로가 호출되는 위치에 따라서 다릅니다. 매크로의 모든 return() 문은 매크로 자체가 아닌 매크로를 호출한 scope에서 반환되므로 주의해야합니다.

cmake_minimum_required(VERSION 3.10)
project(functions)

macro(inner)
    message("From inner")
    return() # Usally dangerous within a macro
    message("Never printed")
endmacro()

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

outer()

매크로에서 return() 문이 실행됬지만, 그 결과 함수에서 return() 한 것과 동일한 결과를 보여주고 있습니다.

이는 위 예시에서 실행되는 코드가 결국 아래와 동일하기 때문입니다.

따라서 매크로 내에서 return()을 사용할 때에는 주의해서 사용해야합니다.

 


Overriding Commands

함수나 매크로는 새로운 커맨드를 생성합니다. 하지만, 해당 커맨드가 같은 이름으로 존재한다면 어떻게 될까요?

만약 함수를 정의할 때 해당 이름의 커맨드가 이미 존재하면, 존재하던 커맨드는 앞에 '_'가 붙어서 사용할 수 있도록 합니다. 이는 이전 커맨드가 함수 또는 매크로에 대한 것인지에 관계없이 적용됩니다.

따라서 아래와 같이 이미 존재하는 커맨드에 wrapper를 만들 수도 있습니다.

 

커맨드가 이렇게 한 번만 재정의되면 잘 동작하는 것처럼 보이지만, 다시 재정의하면 원래 명령에 더 이상 액세스 할 수 없습니다. 이전 커맨드를 저장하기 위해 '_'를 붙이는 것은 현재 이름에만 적용되며, 모든 이전에 재정의된 커맨들은 재귀적으로 적용되지 않습니다. 

 

따라서 다음 경우에는 무한 재귀로 빠질 수 있습니다.

단순히 예상하기로는 아래의 결과를 얻을 것 같지만,

예상과는 달리 첫 번째 printme 함수는 절대로 호출되지 않습니다. 결국 두 번째 printme 함수는 내부에서 _printme 커맨드를 호출할 때 자기 자신을 호출하게 되고, 무한 루프가 발생하기 시작합니다.

 

위 코드는 CMake에서 다음과 같이 처리됩니다.

  1. 첫 번째 printme 함수가 정의되고 해당 이름으로 함수를 호출할 수 있습니다. 이전에 동일한 이름의 커맨드가 없으므로 추가 동작은 없습니다.
  2. 두 번째 printme 함수가 정의되면, CMake는 해당 이름의 커맨드가 이미 존재하므로, 존재하던 커맨드에 '_'를 붙여서 _printme 함수로 새로 정의하고, 두 번째 printme가 새로운 printme가 되도록 설정합니다.
  3. 세 번째 printme 함수가 정의되면, CMake는 다시 해당 이름으로 기존에 존재하는 커맨드가 있는지 찾습니다. 그 결과 이전 커맨드(두 번째 printme)를 _printme 이름으로 재정의하고 새로운 함수 정의가 printme가 되도록 합니다.

printme()가 호출되면 세 번째 printme() 함수가 호출되고 그 함수 내부에서 _printme() 함수를 호출합니다. _printme() 함수가 호출되면 두 번째 printme() 함수가 호출되지만, 두 번째 printme() 함수 내부에서도 _printme() 함수를 호출하게 됩니다. _printme() 함수는 두 번째 printme() 함수를 가리키므로, 결국 무한 재귀가 발생합니다.

 

일반적으로 위에서 살펴봤듯이 이전 구현을 호출하지 않는 함수나 매크로를 재정의하는 것이 좋습니다. 이 경우에는 프로젝트는 단순히 새로운 커맨드가 이전 커맨드를 대체하고, 이전 커맨드는 더 이상 사용할 수 없는 것으로 간주하는 것이 좋습니다.

'CMake' 카테고리의 다른 글

[CMake] Generator Expressions  (0) 2021.11.03
[CMake] Properties  (0) 2021.11.02
[CMake] add_subdirectory() 와 변수 Scope  (0) 2021.10.31
[CMake] Looping - foreach, while  (0) 2021.10.31
[CMake] Flow Control - if  (0) 2021.10.30

댓글