連結: Day 5 - Colab
在進入今天的主題前, 我們需要了解 Variable Scope 和 Variable Expansion 是什麼東西, 才能更好的理解 CMake functions 的運作模式和 CMake 中很重要的 Modules 要如何使用, 並且避免不好的寫法
Variable scope, 可以理解為變數的 存活範圍 或是 可以被看見的範圍, 在這個範圍內建立的變數
來看看下面的例子
set(x 1)
block()
set(x 2)
set(y 3)
message("inner x: ${x}")
message("inner y: ${y}")
endblock()
message("outer x: ${x}")
message("outer y: ${y}")
set()
message()
message()
是用來將訊息印到 terminal 上即可block()
block()
的用法來看看上面的 message()
分別會輸出什麼結果
inner x: 2
inner y: 3
outer x: 1
outer y:
可以看到, block()
裡面是新的 variable scope, 所以即使覆蓋了外層 copy 進來的 x
和 y
也不會影響
有幾種方法
block(PROPAGATE)
return([PROPAGATE vars...])
由於 回傳變數 或是 修改外層變數 的方法違背 CMake3.0 主打的 target-centered 設計理念
本系列不會詳細介紹, 如果有興趣可以參考官方文件~
Target-centered 的概念會在 [Day 8] Target 類型 介紹
在介紹 functions 前的最後一步, 要先來講講 Variable Expansion 是什麼
記得在 Day 4 有提到, 設定 normal variables 時 CMake 會把所有的值都轉成字串
有多個空白分隔的值的話, 則會用分號 ;
合併成一個字串
當我們用 ${varName}
取值的這個行為, 就叫做 Variable Expansion
也可以理解成, 將變數替換為他儲存的值, 所以也稱作 Variable Replacement
因此, 我們可以將一個變數賦值給另一個變數, 比如
set(name "ItHome2023")
set(it_home_name ${name})
message("name: ${name}") # name: ItHome2023
message("it_home_name: ${it_home_name}") # it_home_name: ItHome2023
很簡單
那麼, 有趣的問題來了, 如果空白分隔會被 CMake 用分號合併成字串
請問: 將用空白分隔的字串賦值給另一個變數時, 被賦值的變數是拿到多個值, 還是一個字串呢?
set(name "ItHome Ironman 2023")
set(it_home_name ${name})
message("name: ${name}") # name: ItHome Ironman 2023
message("it_home_name: ${it_home_name}") # it_home_name: ???
答案是
name: ItHome Ironman 2023
it_home_name: ItHome Ironman 2023
要記得, CMake 會先處理空白分隔的變數, 然後才進行 Variable Expansion
有了這個概念後, 我們就可以進入 Function 的世界了
記得上面提到的 Variable Scope 嗎? 由於使用每個 function 時, 都會建立新的 variable scope (就像 block()
)
所以我們可以用 functions 來封裝一些常見的邏輯, 而不會影響到使用 function 的人 (caller)
話不多說, 直接來看 function 怎麼寫
function(<name> [<arg1> ...])
<commands>
endfunction()
name
arg1
commands
來看個例子
function(hello name)
message("Hello ${name}!")
endfunction()
hello(Eric) # Hello Eric!
Function 的基本用法就這麼簡單, 只要定義 function name 和 argument 就好
但如果
下面就來介紹要如何達到這些目的
在 function 被呼叫時, CMake 會自動幫我們在新 varaible scope 建立幾個變數
ARGC
ARGV
ARGV#
取得對應變數ARGN
可以參照下面的例子會比較清楚
function(hello firstNamed surName)
message("ARGN: ${ARGN}") # ARGN: unnamedVariable1;unnamedVariable2
message("ARGV: ${ARGV}") # ARGV: eric;hung;unnamedVariable1;unnamedVariable2
message("ARGV0: ${ARGV0}") # ARGV0: eric
endfunction()
hello(eric hung unnamedVariable1 unnamedVariable2)
那如果想要用 keyword arguments 呢?
這時候就需要 cmake_parse_arguments()
了!
cmake_parse_arguments()
Note: CMake 3.7 開始有加入新的語法, 但由於 Colab 環境是用 CMake 3.27, 所以這邊不會介紹新的語法, 當然, 下面語法也適用 CMake 3.27 以上版本
有興趣的可以去官網查看 cmake_parse_arguments
cmake_parse_arguments(
<prefix>
<options> <one_value_keywords> <multi_value_keywords>
<args>...
)
include(CMakeParseArguments)
CMakeParseArguments
是 CMake 提供的一個 module, 將他 include()
近來就可以使用裡面的各種變數include(moduleName)
指令會將 module 中的變數都加到現在的環境prefix
${prefix}
options
false
, 相對的, 有給就會是 true
one_value_keywords
multi_value_keywords
args
一樣可以和下面例子對照, 注意在使用 func()
的時候, keyword arguments 不會受到傳入的順序影響
利用這個特性, 我們就可以寫出很有彈性的 function 了, 讚讚🎉🎉🎉
function(func)
set(prefix FOO)
set(options OPTION1 OPTION2)
set(keywordOneValue KEYWORD_ONE_VALUE)
set(keywordMultiValue KEYWORD_MULTI_VALUE)
include(CMakeParseArguments)
cmake_parse_arguments(
${prefix}
"${options}"
"${keywordOneValue}"
"${keywordMultiValue}"
${ARGN} # args...
)
message("prefix: ${prefix}")
message("Options: ")
foreach(arg IN LISTS options)
if(${prefix}_${arg})
message(" ${arg} enabled")
else()
message(" ${arg} disabled")
endif()
endforeach()
message("Keywords: ")
foreach(arg IN LISTS keywordOneValue keywordMultiValue)
message(" ${arg} = ${prefix}_${arg} = ${${prefix}_${arg}}")
endforeach()
message("args: ${ARGN}")
endfunction()
func(
KEYWORD_ONE_VALUE keyword_one_value
KEYWORD_MULTI_VALUE keyword multi value
OPTION2
rest arg1 arg2
)
會得到
prefix: FOO
Options:
OPTION1 disabled
OPTION2 enabled
Keywords:
KEYWORD_ONE_VALUE = FOO_KEYWORD_ONE_VALUE = keyword_one_value
KEYWORD_MULTI_VALUE = FOO_KEYWORD_MULTI_VALUE = keyword;multi;value
args: KEYWORD_ONE_VALUE;keyword_one_value;KEYWORD_MULTI_VALUE;keyword;multi;value;OPTION2;rest;arg1;arg2
到此, 我們已經能夠寫出相當有彈性的 function 了
接下來, 就該思考如何把這些棒棒的 functions 集中管理, 方便自己或是其他人使用了
所以, 就我們來看看 Modules 吧
到目前為止, 我們的專案已經有了 source directory (src
) 和 build directory (build
), 是時候加入新的資料夾了
通常 CMake modules 檔案會命名為 *.cmake
, 並放在名為 cmake
的資料夾底下統一管理, 這樣就只需要將 CMake 的 predefined variableCMAKE_MODULE_PATH
設為該路徑, 就可以很輕易的找到該 module 了
我們的專案結構會長這樣
cmake-example/
├─ cmake/
├─ src/
├─ build/
├─ CMakeLists.txt
我們可以在 cmake
資料夾底下新增一個 module Func.cmake
, 並將剛剛寫的 func
改寫在這裡
所以, 現在我們的專案架構會變成
cmake-example/
├─ cmake/
│ ├─ func.cmake 👈
├─ src/
├─ build/
├─ CMakeLists.txt
那麼, 由於 module 是不能直接執行的, 那要怎麼在 CMakeLists.txt
使用我們的 func
呢?
沒錯! 就像上面的 include(CMakeParseArguments)
一樣, 只要在 CMakeLists.txt
中用 include(Func)
即可, 不過為了讓 CMake 能知道我們把 modules 放在哪裡, 需要先將路徑加入 CMAKE_MODULE_PATH
CMakeLists.txt
cmake_minimum_required(VERSION 3.27)
project(ItHome2023)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(Func)
func(
KEYWORD_ONE_VALUE keyword_one_value
KEYWORD_MULTI_VALUE keyword multi value
OPTION2
rest arg1 arg2
)
就是這麼簡單, 大家可以到 Colab 上執行看看結果和沒有 module 時是否相同
另外, 注意到了嗎? Module 的命名是對應其提供的 function 的
比如 CMakeParseArguments
包含 cmake_parse_arguments()
function, 我們的 Func
module 包含 func
function
這並非硬性規定, 只是 CMake 的 conventions, 但個人建議依照這個原則去寫 module, 才能讓使用者或是未來的自己能 預期 這個 module 會提供哪些東西
學會了 function 和 module 的用法後, 離我們的 CMake 專案又更近一步了! 下一篇讓我們來看看到底什麼是 Generator 吧