iT邦幫忙

2023 iThome 鐵人賽

DAY 5
1
Software Development

30 天 CMake 跨平台之旅系列 第 5

[Day 5] Functions 和 Modules

  • 分享至 

  • xImage
  •  

本日內容

  • Variable Scope
  • Variable Expansion
  • Functions
  • Modules

連結: Day 5 - Colab

寫在前

在進入今天的主題前, 我們需要了解 Variable Scope 和 Variable Expansion 是什麼東西, 才能更好的理解 CMake functions 的運作模式和 CMake 中很重要的 Modules 要如何使用, 並且避免不好的寫法

Variable Scope

Variable scope, 可以理解為變數的 存活範圍 或是 可以被看見的範圍, 在這個範圍內建立的變數

  • 不會影響到外面的變數
  • 外面的變數也看不到他
  • 但是該範圍內的變數 可以 看到 外面變數的 copy, 也就是說, 新 variable scope 被建立時, 該 scope 外面的變數會全部被 copy 一份到 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()
    • 還記得嗎? 這是我們在 Day 4 講過的, 用來設定 normal variables 的指令
  • message()
    • 這個指令會在 [Day 7] 如何 Debug 介紹, 這裡只要知道 message() 是用來將訊息印到 terminal 上即可
  • block()
    • 會建立新的 variable scope, 這邊只是方便做講解, 本系列 不會 介紹 block() 的用法
      所以大家只要知道他會建立新的 variable scope 就好

來看看上面的 message() 分別會輸出什麼結果

inner x: 2
inner y: 3
outer x: 1
outer y: 

可以看到, block() 裡面是新的 variable scope, 所以即使覆蓋了外層 copy 進來的 xy 也不會影響

如果我想要讓內部 Variable Scope 更新外部的 Variables 怎麼辦?

有幾種方法

  • block(PROPAGATE)
  • return([PROPAGATE vars...])

由於 回傳變數 或是 修改外層變數 的方法違背 CMake3.0 主打的 target-centered 設計理念
本系列不會詳細介紹, 如果有興趣可以參考官方文件~

Target-centered 的概念會在 [Day 8] Target 類型 介紹

Variable Expansion

在介紹 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 的世界了

Functions

記得上面提到的 Variable Scope 嗎? 由於使用每個 function 時, 都會建立新的 variable scope (就像 block())
所以我們可以用 functions 來封裝一些常見的邏輯, 而不會影響到使用 function 的人 (caller)

話不多說, 直接來看 function 怎麼寫

function(<name> [<arg1> ...])
  <commands>
endfunction()
  • name
    • 用來呼叫的 function name
  • arg1
    • function 的參數
  • commands
    • function 實作內容

來看個例子

function(hello name)
  message("Hello ${name}!")
endfunction()

hello(Eric) # Hello Eric!

Function 的基本用法就這麼簡單, 只要定義 function name 和 argument 就好
但如果

  • 想要多個 arguments 怎麼做?
  • 想要讓使用者輸入任意數量的 arguments?
  • 想要讓使用者指定 keyword, 並不需要照順序給值?

下面就來介紹要如何達到這些目的

Handle arguments

在 function 被呼叫時, CMake 會自動幫我們在新 varaible scope 建立幾個變數

  • ARGC
    • 有多少參數
  • ARGV
    • 呼叫 function 時的所有參數, 或叫 named arguments
    • 以 List 表示 (分號分隔的字串)
    • 可以用 ARGV# 取得對應變數
  • ARGN
    • 定義 function 時沒有的參數, 或叫 unnamed arguments
    • 以 List 表示

可以參照下面的例子會比較清楚

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 中的變數都加到現在的環境
    • 下面的 Modules 會詳細介紹
  • prefix
    • 會將所有參數加上前綴 ${prefix}
  • options
    • 其實就是 boolean, 在 function 中定義但 caller 沒有給這個參數的話, 就會預設是 false, 相對的, 有給就會是 true
  • one_value_keywords
    • 指定 keyword 和對應的值, 但只能有一個值
  • multi_value_keywords
    • 指定 keyword 和對應的值, 但可以有多個值
  • args
    • 所有傳給 function 的參數, 以 List 表示

一樣可以和下面例子對照, 注意在使用 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 吧

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 的命名原則

另外, 注意到了嗎? Module 的命名是對應其提供的 function 的
比如 CMakeParseArguments 包含 cmake_parse_arguments() function, 我們的 Func module 包含 func function
這並非硬性規定, 只是 CMake 的 conventions, 但個人建議依照這個原則去寫 module, 才能讓使用者或是未來的自己能 預期 這個 module 會提供哪些東西

預告

學會了 function 和 module 的用法後, 離我們的 CMake 專案又更近一步了! 下一篇讓我們來看看到底什麼是 Generator 吧


上一篇
[Day 4] Variables 類型
下一篇
[Day 6] 什麼是 Generators?
系列文
30 天 CMake 跨平台之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言