CMake でのメタプログラミングとその応用


はじめに

みなさん、こんにちは。今回は CMake でのメタプログラミングとその応用について書いていきます。

eval()

CMake には、文字列をコードとして評価する機能――いわゆる eval に相当するものがありません。しかし、以下の手法を用いることにより、eval と同等の機能を持つコマンドを作成することができます[参考]。

function(eval code)
  set(path ${CMAKE_CURRENT_LIST_DIR}/.temp.cmake)
  file(WRITE ${path} "${code}")
  include(${path})
  file(REMOVE ${path})
endfunction()

eval("message(1)") # 1

上記では、受け取ったコードcodeをファイルに書き込み、それをinclude()することで、eval を実現しています。これによって、CMake スクリプトで CMake スクリプトの生成と実行が行えるようになます。これは、CMake でメタプログラミングが可能になったということを意味しています。

コマンドのリフレクション

さて、eval()コマンドを用いると、CMake では出来ないコマンドのリフレクションが可能になります。以下の例では、変数command_nameに格納されているコマンド名を元にコマンドを呼び出しています。

set(command_name message)
eval("${command_name}(a b c)") # abc

無名関数

また、eval()コマンドを用いると無名関数を作ることもできます。

まず、引数に Bracket Arguments を使用してみます。このようにすると無名関数のような見た目になり、エスケープや変数参照が無効となるので、無名関数内でのローカル変数を表現できます。

set(value 1)

eval([[
  set(value 2)
  message("value: ${value}") # value: 2
]])

つぎは、無名関数に引数を導入してみましょう。これは以下の様なシンタックスとします。

[[(a b c)
  ...
]]

そして、これを解釈して実行するeval_lambda()というコマンドを作成します。

function(eval_lambda)
  list(GET ARGV -1 lambda)
  list(REMOVE_AT ARGV -1)
  unset(names)

  if(lambda MATCHES [[^\(([^)]*)\)(.*)$]])
    set(names ${CMAKE_MATCH_1})
    set(lambda ${CMAKE_MATCH_2})
  endif()

  eval("
    function(eval_lambda_impl ${names})
      ${lambda}
    endfunction()
  ")
  eval_lambda_impl(${ARGV})
endfunction()

動作として、無名関数を表す文字列の引数部分とコード部分を分離し、それをもとに実際にコードを実行するeval_lambda_impl()というコマンドをeval()コマンドにて生成し、実行するという流れです。さっそく使用してみます。

eval_lambda(1 2 3 [[(a b c)
  message("a: ${a}") # a: 1
  message("b: ${b}") # b: 2
  message("c: ${c}") # c: 3
]])

無名関数に引数を導入するのに成功しました。

無名関数の応用

この無名関数によって得られる実益は、例えばコードの前後に処理を入れたい場合でしょうか。以下は実行の際に作業用のディレクトリを作成し、コードの実行が終わると削除するコマンドです。

function(in_working block)
  get_filename_component(name ${CMAKE_CURRENT_LIST_FILE} NAME_WE)
  set(working_dir ${CMAKE_CURRENT_LIST_DIR}/.${name}-working)

  message("## create working")
  file(REMOVE_RECURSE ${working_dir})
  file(MAKE_DIRECTORY ${working_dir})

  eval_lambda(${working_dir} ${block})

  message("## delete working")
  file(REMOVE_RECURSE ${working_dir})
endfunction()
in_working([[(dir)
  set(path ${dir}/hoge.txt)
  file(WRITE ${path} a b c)

  file(READ ${path} content)
  message("content: ${content}")

  file(READ ${path} content HEX)
  message("content: ${content}")

  file(READ ${path} content LIMIT 2 OFFSET 1)
  message("content: ${content}")
]]) #[[
  ## create working
  abc
  616263
  bc
  ## delete working
]]

おわりに

以上、CMakeでのメタプログラミングとその応用についてでした。この手法を使うと、CMake の使いにくいところをカバーすることができるようになります。

明日は、mrk_21 さんの『CMake の情報と 3.1.0 での変更点』です。