こんにちは。花見の季節ですね! 田原です。

CMakeは一種のプログラミング言語でもあるのでCMakeLists.txtやCMakeスクリプト内で定義できる関数もあります。(ここでは「ユーザ定義コマンド」と呼びます。)ユーザ定義コマンドはfunctionとmacroの2種類あり、それぞれC++の関数やプリプロセッサ・マクロと良く似ています。今回はこの2つについて解説します。

1.ユーザ定義コマンドの基本

functionとmacroの2つの仕組みは全く異なるので微妙に振る舞いも違うのですが、定義方法や呼び出し方法はほぼ同じです。
まず、定義と呼び出しの方法は以下の通りです。

function(コマンド名 仮引数リスト)
    # 処理
endfunction([コマンド名])

macro(コマンド名 仮引数リスト)
    # 処理
endmacro([コマンド名])
コマンド名(実引数リスト)

以下、functionとmacroのサンプルです。(「4.functionとmacroの違い」以外のサンプルは同じ振る舞いとなる範囲で記述していますので、どちらか一方のみを書いている時は、もう片方でも同様です。)

# 関数(仮引数無し)の定義
function(func0)
    message(STATUS "func0()")
endfunction(func0)

# 呼び出し
func0()

# マクロ(仮引数有り)の定義
macro(macro1 ARG_A ARG_B)
    message(STATUS "macro1( '${ARG_A}' '${ARG_B}' )")
endmacro(macro1)

# 呼び出し
macro1(abc def)

# 仮引数より実引数の数が少ないとエラー
## macro1(abc)

# endfunctionやendmacroのコマンド名は省略可
function(func1)
    message(STATUS "func1()")
endfunction()

func1()
-- func0()
-- macro1( 'abc' 'def' )
-- func1()

2.実引数とリスト

1つの仮引数は、1つの値や複数の値を集めたリストの両方を受け取ることができます。リストを渡す時は “”(ダブルクォーテーション)で括ります。くくらなかった場合、複数の実引数が与えられたことになります。

2-1.””で括らないでリストを渡す

要素2つのリストを2つの仮引数へ渡すサンプルです。
なお、CMakeの中では’(シングルクォート)に特別な意味はなく普通の文字です。この例では仮引数に割り当てられた値を分かり易く見るために使ってます。

function(func2 ARG_A ARG_B)
    message(STATUS "func2( '${ARG_A}' '${ARG_B}' )")
endfunction(func2)

func2(abc def)
func2(abc;def)
func2("abc" "def")
-- func2( 'abc' 'def' )
-- func2( 'abc' 'def' )
-- func2( 'abc' 'def' )

2-2.””で括ってリストを渡す

リスト(複数の値)を1つの仮引数に渡したい時は””で括ります。

1つ注意点があります。ブランクで区切ったものを””で括るとリストではなく1つの文字列になり、;(セミコロン)で区切ったものを””で括るとリストになります。
以下、サンプルです。

function(func3 ARG_A ARG_B)
    message(STATUS "func3( '${ARG_A}' '${ARG_B}' )")
        foreach(ITEM IN LISTS ARG_A)
            message(STATUS "ARG_A=${ITEM}")
        endforeach()
        foreach(ITEM IN LISTS ARG_B)
            message(STATUS "ARG_B=${ITEM}")
	endforeach()
endfunction(func3)

func3("abc def" "ghi;jkl")
-- func3( 'abc def' 'ghi;jkl' )
-- ARG_A=abc def
-- ARG_B=ghi
-- ARG_B=jkl

3.可変長パラメータ

C言語の頃から可変長パラメータがありました。C++11以降のC++ではテンプレートを使って型安全な可変長パラメータがサポートされています。プリプロセッサ・マクロも同様です。

CMakeも可変長パラメータをデフォルトでサポートしています。
使い方は簡単です。仮引数よりも多くの実引数を与えることができます。つまり、特別な記述をすることなくいくつでも実引数を渡すことが可能なのです。(逆に、与えすぎても単に無視されるだけで警告等はしてくれません。)
可変長引数にアクセスするには、function~endfunction、macro~endmacroの間でのみ有効ないくつかの特殊変数を使います。

変数名 意味
ARGC 実引数の数(C++ main関数のargcと同じイメージです)
ARGV 実引数のリスト(C++ main関数のargvと同じイメージです)
ARGN 仮引数に割当らなかった実引数のリスト
ARGV<番号> 要するに argv[番号] です。番号は0から始まります。
function(func4 ARG)
    message(STATUS "func4()")
    message(STATUS "ARGC=${ARGC}")

    foreach(ITEM ${ARGV})
        message(STATUS "ARGV=${ITEM}")
    endforeach()

    foreach(ITEM ${ARGN})
        message(STATUS "ARGN=${ITEM}")
    endforeach()

    message(STATUS "ARGV0=${ARGV0}")
    message(STATUS "ARGV1=${ARGV1}")
endfunction(func4)

func4(param0 param1 param2)
-- func4()
-- ARGC=3
-- ARGV=param0
-- ARGV=param1
-- ARGV=param2
-- ARGN=param1
-- ARGN=param2
-- ARGV0=param0
-- ARGV1=param1
macro(macro4 ARG)
    message(STATUS "macro4()")
    message(STATUS "ARGC=${ARGC}")

    foreach(ITEM ${ARGV})
        message(STATUS "ARGV=${ITEM}")
    endforeach()

    foreach(ITEM ${ARGN})
        message(STATUS "ARGN=${ITEM}")
    endforeach()

    message(STATUS "ARGV0=${ARGV0}")
    message(STATUS "ARGV1=${ARGV1}")
endmacro(macro4)

macro4(param0 param1 param2)
-- macro4()
-- ARGC=3
-- ARGV=param0
-- ARGV=param1
-- ARGV=param2
-- ARGN=param1
-- ARGN=param2
-- ARGV0=param0
-- ARGV1=param1

4.functionとmacroの違い

C++の関数とプリプロセッサ・マクロは定義する時の構文が全く異なるので、混同してしまうことはあまりないと思います。
しかし、CMakeの関数とマクロは上述したように定義する時の構文がほぼ同じなので混同しやすいです。にもかかわらず、C++の関数とマクロのような相違があり、実行の仕組みが全く異なるので一見同じに見えても予想外の違いに悩むことがあるのです。

functonはC++の関数と似てます。呼び出したところから普通にfunctionへ飛んでいきfunction文で定義された内容が実行されます。
これに対してmacroはC++のプリプロセッサ・マクロと同じように振る舞います。つまり、macro()~endmacro()で定義した内容が、macroを「呼び出した」ところへ「展開」されて実行されます。
またfunctionの仮引数は「ローカル変数」のように振る舞います。これに対してmacroの仮引数は ${仮引数名} という文字列が実引数へ単純に置き換えられるという仕様です。

例えば、下記のCMakeスクリプトが有ったとします。

macro(foo ARG)
    message(STATUS "ARG=${ARG}")
endmacro(foo)

foo(123)

このマクロ呼び出し macro5(123) は次のようにマクロの内容で置換されてから実行されます。

    message(STATUS "ARG=123")
-- ARG=123

上記の例ではfunctionとの差は見えないですが、次のCMakeスクリプトで相違が見えます。

function(func5 ARG)
    message(STATUS "--- function ---")
    set(ARG 123)
    message(STATUS "ARG=${ARG}")
endfunction(func5)

func5(123)
message(STATUS "ARG=${ARG}")

macro(macro5 ARG)
    message(STATUS "--- macro ---")
    set(ARG 123)
    message(STATUS "ARG=${ARG}")
endmacro(macro5)

macro5(456)
message(STATUS "ARG=${ARG}")

上記このマクロ呼び出し macro5(456) は次のようにマクロの内容で置換されてから実行されます。
この時、マクロ定義内の${ARG}はマクロへ与えた実引数へ展開されていることに注目下さい。

    message(STATUS "--- macro ---")
    set(ARG 123)
    message(STATUS "ARG=456")
-- --- function ---
-- ARG=123
-- ARG=
-- --- macro ---
-- ARG=456
-- ARG=123

次に、function定義内のset(ARG 123) は、ローカル変数としての ARG を修正しているので直後のmessage文に影響しますし、function定義外の変数には影響しません。
これに対して、macro定義内のset(ARG 123)は、上記の置換結果を見ると分かるようにローカル変数ARGではなく通常の変数ARGを設定しています。従って、直後のmessage文には反映されませんし、macro定義の外の通常の変数に影響します。

このようにCMakeのマクロは関数と同じ構文で定義するのですが、振る舞いはC++のプリプロセッサ・マクロと同じように振る舞いますので、混同しないように要注意です。

${ARGC}、${ARGV<番号>}等の特殊変数も同様な仕組みで実引数へ置き換えられます。そのため、function定義内でmacroを呼び出すと、ややこしいことになりますので、本当にご注意下さい。

macro(macro6)
    message(STATUS "--- macro ---")
    message(STATUS "ARGV =${ARGV}")
    message(STATUS "ARGV0=${ARGV0}")
    message(STATUS "ARGV1=${ARGV1}")
endmacro(macro6)

function(func6)
    message(STATUS "--- function ---")
    message(STATUS "ARGV =${ARGV}")
    message(STATUS "ARGV0=${ARGV0}")
    message(STATUS "ARGV1=${ARGV1}")
    macro6(789)
endfunction(func6)

func6(123 456)
実行結果(開いてみる前に結果を予想してみて下さい)
[text] — — function —
— ARGV =123;456
— ARGV0=123
— ARGV1=456
— — macro —
— ARGV =789
— ARGV0=789
— ARGV1=456
[/text] func6に渡した実引数456はmacro6には渡していないし、${ARGV}を見ても確かに渡っていないにも関わらず、${ARGV1}が456として表示されており、まるで渡っているかのように見えます。
マクロは呼び出した位置へ展開されるという仕組みのため、この${ARGV1}はfunc6の${ARGV1}であり、macro6に渡っているわけではありません。ご用心。

5.まとめ

今回は、CMakeのユーザ定義コマンドについて解説しました。同じ構文で定義できるfunctionとmacroの2種類です。前者は普通に関数なのでC++に慣れた方なら比較的スムーズに使いこなせると思います。後者はプリプロセッサ・マクロと同様に呼び出しコードを定義で置き換えてから実行されます。つまり、スクリプトを「書き換える」処理なので意外にハマりやすいです。(functionと同じ構文なので特に)
マクロは、その実装の仕組み上、呼び出し元の変数を容易に変更できるので多用しがちですが、このように仕組みが大きく異なるため、振る舞いも微妙に異なりハマる可能性があります。仮引数が予想外の振る舞いをした時には、スクリプトの書き換え処理であることを思い出して、頭の中で展開してみると解決できるかもしれませんね。

CMakeのマクロ意外に難しいです。それはで今回はこれにて失礼します。お疲れ様でした。