CPython 開發筆記: CMake 設定

Dboy Liao
6 min readNov 1, 2024

--

最近比較有時間,就打算好好研究一下 TorchDynamo。中間無意間看到 PEP-523 這個 TorchDynamo 實現 JIT compile 的主要 CPython API ,好奇心驅使下就開始跟 CPython 玩了一下。過程中用了 CMake 來管理實驗用的 extension module,但由於我習慣用 pyenv 來管理我的 python 環境,而在這個情況下透過 find_package 總是連結到系統用的 python。經過一番波折之後,總算整理出一個解法可以更精準地控制 cmake 中 python 相關 header 與 object files 的設定,就想說記錄一下這個過程。

我的老朋友:python-config

最早開始實驗的時候,其實我很偷懶地用 make 來管理而已,只是想說可以少打一些指令 XD

當時管理的方式思路也很單純,就是想用 python-config 這個指令幫我找到針對目前使用的 python 編譯時所需要的 include paths 跟 linking flags。我自己跟一些身邊的朋友聊,發現好像不是太多人知道這個指令,那這邊是它的 help message:

上網找了一下,這邊有一些簡單的說明:https://www.commandlinux.com/man-page/man1/python-config.1.html

如果用 editor 把這個指令打開來看,就會發現它其實就是個 python script:

它是在安裝 python 時通常會ㄧ併被生成的,也就是說可以透過這個 script 去得到當時編譯時所需要的 flags:

所以當時最簡單的方式就是透過 Makefile 跟 shell script interpolation 去控制相關編譯的 flags:

my_module: my_module.c
clang $$(python-config --includes --libs --embed) -shared $< -o $@$$(python-config --extension-suffix)

但以前這樣做碰到過些問題,換一個 platform 可能就不能 compile 之類的,思來想去還是想說用上 cmake 好了,結果就發現 find_package(Python REQUIRED) 沒辦法很正確的跟 pyenv 整合,總是用到系統的 python header 還有 share objects。

其實想想也正常,畢竟 pyenv 背後幫我做的,不過也是把 python 的原始碼下載下來進行 configure 跟 compile,而 CPython 本身也不是用 cmake 管理的,兩邊搭不起來實屬正常,那又該怎麼辦呢?

這時候我就把腦筋動到 python-config 身上了。

CMakeLists.txt Example

基本思路也簡單,大概步驟是這樣:

  1. 透過 execute_process 這個 cmake 指令去執行 python-config,並且輸出用變數存起來。
  2. 定義一個 interface target (譬如說 python),然後讓我的 module 去 link 這個 interface target

大概就這樣。

首先第一步,先執行 python-config 並把結果存在變數中 (譬如下面範例的 PYTHON_INCLUDES, PYTHON_LIBSPYTHON_LIB_SUFFIX)

# python3 configurations
execute_process(
COMMAND python3-config --includes --embed
OUTPUT_VARIABLE PYTHON_INCLUDES
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND python3-config --libs --embed
OUTPUT_VARIABLE PYTHON_LIBS
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND python3-config --extension-suffix
OUTPUT_VARIABLE PYTHON_LIB_SUFFIX
OUTPUT_STRIP_TRAILING_WHITESPACE)
separate_arguments(PYTHON_INCLUDES)
separate_arguments(PYTHON_LIBS)

裡面一個比較特別的地方在於透過 separate_arguments 把這些變數從一個 string 改變成一個 list,這在後續定義 interface target 時可以避免一些 bug。

再來定義一個 python 的 interface target 方便後續我們拿其他 target 跟它進行 link:

# Python3 interface
add_library(python3 INTERFACE)
target_compile_options(python3 INTERFACE ${PYTHON_INCLUDES})
target_link_options(python3 INTERFACE ${PYTHON_LIBS})

最後就簡單的設定好我們想要跟 python 進行 link 的 target:

# my_module
add_library(my_module SHARED my_module.c)
set_target_properties(
my_module PROPERTIES
PREFIX ""
SUFFIX ${PYTHON_LIB_SUFFIX}
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}
)
target_link_libraries(my_module PRIVATE python3)

這樣進行編譯之後就可以得到一個 my_module.cpython-311-darwin.so 可以用了🎉🎉🎉

有沒有感覺很簡單?😛

結語

總而言之,算是在看 TorchDynamo 原始碼的過程中發生的一個小插曲,想說應該也有人跟我有一項的需求吧(?) 寫個小記錄,紀念我某一天查 cmake 文件一個上午的時間。

順道一提,我嘗試過問 ChatGPT 這些問題,但可能我問的方式不對,最後沒問出來還是摸摸鼻子去查文件,還好我之前 cmake 還算熟,沒花多久時間
就找到我需要的東西,拼湊出上面的解法,所以歡迎大家跟我分享用 LLM 問出來的方法,我自己也很好奇 🤓

Happy Python Programming!

--

--

Dboy Liao
Dboy Liao

Written by Dboy Liao

Code Writer, Math Enthusiast and Data Scientist, yet.

No responses yet