使用 C 來編寫 Python 模組 – 以 jump consistent hash 為例

Extending Python with C – Jump Consistent Hash

本文的目標是以 C 完成 Jump Consistent Hash 的主體,接著透過 Python-C-API 轉為 Python 可以使用的套件。也就是說,模組的本體將會以 C 語言寫成,接著透過 setuptools 編譯為 so 檔,如此編成的模組將可以讓 Python 無縫接軌使用。

參考文章與程式原始碼:

 

1.我們的目標

閱讀完 <A Fast, Minimal Memory, Consistent Hash Algorithm> 這樣的經典文章後,我們希望透過 Python 來實現這個 hash function,一點簡單的實驗後發現無法重現論文中的程式碼,因為 Python 沒有整數型態大小的限制,我們無法簡單的把 Key 乘起來然後不 care 溢位的地方。接著採用 Python ctypes 的型態實,實作後發現過於複雜。

因此,我們轉而使用 Python-C-API 的方式來實作 jump consistent hash 演算法。

預期達到的目標 – 透過 jchash 即可使用 C 寫成的 Python modules

2. 資料夾結構

  • tests
    • test_jchash.py
  •  src
    • jchash.c
    • jchash.h
  • setup.py

3. Unittest

首先撰寫 Unittest,透過 github 我們能搜尋到已經有人實作過這個演算法,我們可以從中擷取一些 test 當作我們的測試資料:

有了 unittest 後,我們之後可以在 setup.py 裏面設定 test 的值,就可以使用 python setup.py test 來跑測試。

4. jchash.c

開始撰寫 jchash.c,檔案的第一行必須要載入 Python 的標頭檔,將 Python API 載入:

必須要注意,Python 會預設一些預處理定義,因此這個標頭檔必須放在第一個引入的位置。

接下來我們在檔案中加入函式本體,之後在 Python 裏面我們會以 jchash.jchash(key, buckets) 的方式呼叫。

最主要的重點,是要將 Python 的 Arguments 轉換為 C types (舉例而言,key 以及 buckets 轉換為 uint64 以及 int32)。而 C function 永遠都只有兩個參數,就是 self 以及 args。

  • 參數 self ,如果是 module-level function 將指向 module object,如果是 method 將指向 object instance。
  • 參數 args 會是一個指向 Python tuple object 的指標,內有所有的 arguments。每個在 tuple 裡的 item 都代表著一個參數。這些參數目前都是 PyObjects,如果要轉換為 C 的值的話,我們可以透過 PyArg_ParseTuple() 來檢查型態並且轉換為 C 的值。
  • 當 PyArg_ParseTuple 回傳非零值的時候,代表所有的轉換都成功。如果回傳 NULL,就代表其中有參數轉換失敗,同時會 raises 一個適當的 Exception 來解決這樣的情況。

4.1 Parsing Arguments and Building Values

參考資料: Parsing arguments and building values

有關轉換型態的字元表示方式,請參考上列參考資料,這邊擷取幾個有用的來說明:

  • K (int) [unsigned long long]
  • i (int) [int]
  • s (str) [const char *]
  • O (object) [PyObject *]

我們可以透過 PyArg_ParseTuple()、PyArg_ParseTupleAndKeywords()、PyArg_Parse() ,將 Python Objects 轉換為 C values,而透過 Py_BuildValue() 我們可以把 C values 轉換為 Python Objects。

具體的用法範例如下:

4.2 Multiple types argument

參考資料: Concrete Objects Layer

從上面的範例我們可以看到,PyArg_Parse 有如 c string format 一樣簡單好用,但是有個問題:

如果同個 argument 有多種型態呢?

在 jchash 裏面有遇到這個問題,jchash 的 key 可以是 bytes 或是 ulonglong,我們沒有辦法使用兩個 PyArg_ParseTuple 來解決這個問題,因為 ParseTuple 出錯的時候會 raises exception。

因此我們必須要先將第一個 argument 轉為 PyObject,透過函式判斷後在將正確數值存入 key 當中。

4.3 Determine PyObject’s  C Type

我們有兩種方法可以檢查 PyObject 的 C 型態,第一個使用 PyObject_IsInstance()

code example from: embl_writer_python.c

或是使用各個 PyObject Layer 的函式:

4.4 Convert PyObject to C values

確定 PyObject 的型態後,我們可以使用相對應的 API 來轉換到 C values:

4.5 Error and Exception

參考資料: Intermezzo: Errors and Exceptions

這邊只介紹 PyErr_SetString,在 jchash 中 key 只支援使用 bytes 以及 int,其餘型態都不是合法的型態。如果使用者傳入的話,我們希望可以丟出 TypeError 來警告使用者。我們可以透過 PyErr_SetString 達成這個任務。

標準 Excpetion 可以參考這裡,只要將 PyErr_SetString 第一個參數替換,就會引發相對應的 Exception。

5. Module’s Method Table and Initialization Function

我們必須要先建立一個 “method table”,將 jchash_jchash 放入其中:

METH_VARARGS 參數是告訴 function 預期會得到 Python-level 的參數以 tuple 型式傳入並且可以被 PyArg_ParseTuple()  解析。

接著 method table 要在 module definition structure 中參照:

這個 structure 需要在 module’s 初始化函式傳入 interpreter。初始化函式必須被命名為 PyInit_name(),name 是 module 的名稱,而且是模組中唯一一個 non-static 的函數。

5. 測試,編譯,執行

一切就緒後,在 setup.py 中設定相關的值:

在 command line 上,我們可以測試剛剛的 unittest:

沒有問題的話可以安裝:

或是編譯成套件

或是上傳到 PyPI

 

完整原始碼:jchash – Jump Consistent Hash for Python




如果你覺得這篇文章不錯,歡迎打賞 IOTA:RFHEIVXVIZWJFXTZORZZRCMHZF9PSGFUFWAAKXTFNZE9JQUY9HFQREJYYSPSXDRLECKXCAQQDOMSMYJYDKPCKWXBKD

或是點選下方圖片贊助我一杯咖啡:

Leave a reply:

Your email address will not be published.

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料