使用面向模塊接口開發,鏈接跨平台的 Python 或 C 包,統一加載到 Python3 環境當中。
目前支持的 Python3 環境,該模塊包描述了如何構建、鏈接、測試、發布的方法。
Python 模塊編譯說明
通常拿到一個 Python 模塊,對它的 setup.py 執行 python setup.py build 即可進行構建,它的內容通常有如下示例。
from setuptools import setup, Extension, find_packages
_maix_module = Extension('_maix', include_dirs=['ext_modules/_maix/include'], sources=get_srcs('ext_modules/_maix'), libraries=['jpeg'])
libi2c_module = Extension('pylibi2c', include_dirs=['ext_modules/libi2c/src'], sources=get_srcs('ext_modules/libi2c/src'))
setup(
name='MaixPy3',
version='0.1.2',
license='MIT',
author='Sipeed',
author_email="support@sipeed.com",
url='https://github.com/sipeed/MaixPy3',
description="MaixPy Python3 library",
long_description=open('README.md').read(),
install_requires=["Pillow"],
ext_modules=[
_maix_module,
libi2c_module,
],
packages = find_packages(), # find __init__.py packages
classifiers=[
'Programming Language :: Python :: 3',
],
)
只需要關心 setup 函數的參數中 packages 、 ext_modules 定義下的模塊。
- find_packages() 會自動尋找根目錄下所有帶有
__init__.py的包導入到 Python3 的 site-packages 中,import 的時候就會找到它。 - ext_modules 是需要經過編譯的 C 模塊。
標准 Python 模塊說明
以 maix 模塊為例,完全用 Python 實現的模塊需要按以下結構進行構建。
- maix/
__init__.py - maix/Video.py
- maix/xxxxx.py
首先 setuptools 打包系統會找到該模塊的 maix 文件夾並將其安裝到 site-packages/maix 下,這樣用戶就可以在 Python3 中 import maix 了,注意它與 setup.py 的相對目錄(/maix)與安裝目錄(site-packages/maix)位置保持一致。
如何控制 from maix import * 的內容可以看 __init__.py 了解。
from .Video import camera
from .import display
__all__ = ['display', 'Video', 'camera']
其中 __all__ 可以控制 import 加載的模塊、對象或變量,這樣一個最基本的 Python 模塊就制作完成了。
關於編寫后的測試看 test_maix.py 代碼可知,關於 tox 測試框架會在最后簡單說明。
關於 CPython 模塊說明
以 libi2c 舉例說明原生 C 開發的模塊。
如果是用 C 開發就需要配合 Makefile 的規則來操作,可以直接在 MaixPy3/ext_modules/libi2c 目錄下直接運行 make all 進行構建,此時就會得到 libi2c.so \ libi2c.a \ pylibi2c.so 等模塊。
這樣目標系統就可以通過 C 代碼鏈接(-l)該 libi2c 模塊執行,而 pylibi2c.so 模塊是可以直接在 Python 里面直接 import 就可以使用的。
juwan@juwan-N85-N870HL:~/Desktop/v831_toolchain_linux_x86/MaixPy3/ext_modules/libi2c$ python3
Python 3.8.5 (default, Jul 28 2020, 12:59:40)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pylibi2c
>>> pylibi2c
<module 'pylibi2c' from '/home/juwan/Desktop/v831_toolchain_linux_x86/MaixPy3/ext_modules/libi2c/pylibi2c.cpython-38-x86_64-linux-gnu.so'>
>>>
注意 pylibi2c.so 是經過 python3 setup.py build_ext --inplace 命令編譯 ext_modules/libi2c/src/pyi2c.c 得到的模塊。
其中 #include <Python.h> 的是來自於系統的 usr/include 目錄,這取決於你的編譯環境。
注意,編譯通過不代表可以運行,如果發現運行時丟失函數(undefined symbol),可以通過 ldd 查詢 .so 依賴函數, 通過 nm -D 查詢 .a 函數,通過 readelf -e 查詢程序編譯版本。
缺啥就往環境里補。
如何編寫 pyXXX.c 的 Python 模塊說明
對於 make / gcc 的模塊包以 ext_modules/xxxx 方式加入 MaixPy3 的編譯環境(setup.py), 請確保該包可以跨平台編譯通過后,同步修改 MaixPy3/setup.py 的 ext_modules 模塊。
from setuptools import setup, Extension, find_packages
_maix_module = Extension('_maix', include_dirs=['ext_modules/_maix/include'], sources=get_srcs('ext_modules/_maix'), libraries=['jpeg'])
libi2c_module = Extension('pylibi2c', include_dirs=['ext_modules/libi2c/src'], sources=get_srcs('ext_modules/libi2c/src'))
setup(
ext_modules=[
_maix_module,
libi2c_module,
],
)
以 _maix_module 為例,在加入編譯之前,該包結構如下。
- ext_modules/_maix
- ext_modules/_maix/include/_maix.h
- ext_modules/_maix/_maix.c
- ext_modules/_maix/pyCamera.c
- ext_modules/_maix/setup.py
- /example/test__maix.py
此時我們可以在該目錄(MaixPy3/ext_modules/_maix)的 setup.py 進行構建。
#!/usr/bin/env python
"""
setup.py file for _maix
"""
from setuptools import setup, Extension
# sudo apt-get install libjpeg-dev
_maix_module = Extension('_maix', include_dirs=['include'], sources=['_maix.c', 'pyCamera.c'], libraries=['jpeg'],)
setup(
name='_maix',
license='MIT',
ext_modules=[_maix_module],
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 3',
],
)
開發完成后再以同樣的方式編寫到 MaixPy3 模塊,注意 Extension 的代碼的鏈接時的相對地址(include_dirs & sources),以及本地打包時鏈接時缺少的(.h)文件,注意 MANIFEST.in 會鏈接本地的文件加入 Python 模塊的打包。
默認配置下打包中不會帶入模塊的(.h)文件,這會導致運行 tox 自動化打包構建模塊時出錯。
include ext_modules/libi2c/src/*.h
include ext_modules/_maix/include/*.h
關於 setup.py 的用法可以參考 2021年,你應該知道的Python打包指南
關於 編譯 CPython 模塊的說明
先從編譯方法說起,在 MaixPy3/ext_modules/_maix/setup.py 的目錄下使用 python3 setup.py build 開始使用構建。
如果在本機 Python 編譯時出現如下錯誤:(夠貼心了吧)
ext_modules/_maix/pyCamera.c:4:10: fatal error: jpeglib.h: 沒有那個文件或目錄
4 | #include "jpeglib.h"
| ^~~~~~~~~~~
compilation terminated.
運行 sudo apt-get install libjpeg-dev 后會在本機 usr/include 和 usr/bin 中加入 libjpeg 的模塊,其他編譯鏈同理。
關於 CPython 的 C 代碼編寫說明
接下來說明 CPython 的代碼編寫規范說明:
- 如何編寫一個 CPython 模塊(PyModule)。
- 如何 CPython 模塊添加類對象(全局對象)、全局函數、全局變量。
- 一個 PyObject 類對象的結構代碼。
- 標准 CPython 模塊的命令規則。
以 MaixPy3/ext_modules/_maix 模塊為例,首先提供一個 C 實現的 Python 模塊入口 _maix.c 。
#include "_maix.h"
#define _VERSION_ "0.1"
#define _NAME_ "_maix"
PyDoc_STRVAR(_maix_doc, "MaixPy Python3 library.\n");
static PyObject *_maix_help() {
return PyUnicode_FromString(_maix_doc);
}
static PyMethodDef _maix_methods[] = {
{"help", (PyCFunction)_maix_help, METH_NOARGS, _maix_doc},
{NULL}
};
void define_constants(PyObject *module) {
PyModule_AddObject(module, "_VERSION_", Py_BuildValue("H", _VERSION_));
}
static struct PyModuleDef _maixmodule = {
PyModuleDef_HEAD_INIT,
_NAME_, /* Module name */
_maix_doc, /* Module _maixMethods */
-1, /* size of per-interpreter state of the module, size of per-interpreter state of the module,*/
_maix_methods,
};
PyMODINIT_FUNC PyInit__maix(void)
{
PyObject *module;
if (PyType_Ready(&CameraObjectType) < 0) {
return NULL;
}
module = PyModule_Create(&_maixmodule);
PyObject *version = PyUnicode_FromString(_VERSION_);
/* Constants */
define_constants(module);
/* Set module version */
PyObject *dict = PyModule_GetDict(module);
PyDict_SetItemString(dict, "__version__", version);
Py_DECREF(version);
/* Register CameraObjectType */
Py_INCREF(&CameraObjectType);
PyModule_AddObject(module, Camera_name, (PyObject *)&CameraObjectType);
return module;
}
此時 Python 在 import 該模塊的時候就會調用 PyInit_xxxx 函數進行初始化,在 Python 里 import 該模塊只會執行一次,想要再次執行需要 reload 函數(from imp import reload)。
通過 PyModule_AddObject 注冊 PyObject 對象到該模塊中,而該對象被公開到一個頭文件當中進行交換,從而給 PyModule 提供多個 PyObject 的實現,添加模塊的全局變量與此同理。
static PyMethodDef _maix_methods[] = {
{"help", ()_maix_help, METH_NOARGS, _maix_doc},
{NULL}
};
通過 _maix_methods 結構體為模塊添加全局函數,如果你認為某個函數是公共函數,則將其放置模塊頂層,表示全局公共函數,Python 基本沒有私有成員。
Python 類對象(PyObject)的結構代碼
一個標准的格式參考如下:
定義一個對象必要的對外引用,將模塊和對象實現分離,模塊再通過(.h)文件鏈接對象實現,可見 MaixPy3/ext_modules/_maix/include/_maix.h 。
#ifdef __cplusplus
extern "C" {
#endif
#include <Python.h>
PyDoc_STRVAR(Camera_name, "Camera");
extern PyTypeObject CameraObjectType;
#ifdef __cplusplus
}
#endif
此時(PyInit__maix)就可以加載該對象(CameraObjectType)到 _maix 模塊當中。
/* Register CameraObjectType */
Py_INCREF(&CameraObjectType);
PyModule_AddObject(module, Camera_name, (PyObject *)&CameraObjectType);
現在該到 PyObject 的實現參考,以 MaixPy3/ext_modules/_maix/include/_maix.h 為范本。
typedef struct
{
PyObject_HEAD;
unsigned int width, height;
} CameraObject;
PyTypeObject CameraObjectType = {
#if PY_MAJOR_VERSION >= 3
PyVarObject_HEAD_INIT(NULL, 0)
#else
PyObject_HEAD_INIT(NULL) 0, /* ob_size */
#endif
Camera_name, /* tp_name */
sizeof(CameraObject), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)Camera_free, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
Camera_str, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
CameraObject_type_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
Camera_methods, /* tp_methods */
0, /* tp_members */
Camera_getseters, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)Camera_init, /* tp_init */
0, /* tp_alloc */
Camera_new, /* tp_new */
};
實現任何模塊時需重點關注如下基本函數接口實現,忽略(Camera)前綴。
- xxxxx_new (對象構造函數)
- xxxxx_free (對象析構函數)
- xxxxx_init (對象初始化函數)
- xxxxx_getseters (對象屬性定義結構)
- xxxxx_methods (對象方法定義結構)
開發上遵循基本結構即可,展示 PyArg_ParseTupleAndKeywords 傳遞參數用法,以 Camera_init 為例,如果不想寫 keyword (kwlist) 就用 PyArg_ParseTuple 函數。
static int Camera_init(CameraObject *self, PyObject *args, PyObject *kwds)
{
// default init value
self->width = 640, self->height = 480;
static char *kwlist[] = {"width", "height", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ii:__init__", kwlist,
&self->width, &self->height))
{
return -1;
}
return 0;
}
為 PyObject 對象鏈接函數符號的時候可以看 xxxxx_getseters 和 xxxxx_methods 的結構定義。
static PyMethodDef Camera_methods[] = {
{"rgb2jpg", (PyCFunction)Camera_rgb2jpg, METH_VARARGS, Camera_rgb2jpg_doc},
{"close", (PyCFunction)Camera_close, METH_NOARGS, Camera_close_doc},
{"__enter__", (PyCFunction)Camera_enter, METH_NOARGS, NULL},
{"__exit__", (PyCFunction)Camera_exit, METH_NOARGS, NULL},
{NULL},
};
static PyGetSetDef Camera_getseters[] = {
{"width", (getter)Camera_get_width, (setter)Camera_set_width, Camera_width_doc},
{"height", (getter)Camera_get_height, (setter)Camera_set_height, Camera_height_doc},
{NULL},
};
以 Python3 的 _maix.Camera 為例:
import _maix
tmp = _maix.Camera()
print("this is method", Camera.rgb2jpg)
print("this is var", Camera.width)
一個簡單的 PyCFunction 函數實現方法如下:
/* str */
static PyObject *Camera_str(PyObject *object)
{
PyObject *dev_desc = PyUnicode_FromString("Camera_str");
return dev_desc;
}
如果是模塊的全局函數則可以配置 METH_NOARGS 並移除函數參數,參考如下代碼。
static PyObject *_maix_help() {
return PyUnicode_FromString(_maix_doc);
}
static PyMethodDef _maix_methods[] = {
{"help", (PyCFunction)_maix_help, METH_NOARGS, _maix_doc},
{NULL}
};
關於編寫 CPython 模塊的參考資料很多,這里只解釋 MaixPy3 模塊的程序設計,具體到函數的如何實現的細節就不在此贅述。
CPython 的內存標記用法
我們知道 Python 擁有自動回收內存的 gc 機制,但在使用 Python C/C++ API 擴展 Python 模塊時,對象指針標記不當可能會導致擴展的模塊存在內存泄漏,可以使用 Py_INCREF(增加) & Py_DECREF(減少) 指針引用計數。
Py_INCREF(ref);
......
Py_DECREF(ref); // Py_XDECREF(ref);
對應 Python 代碼就是:
ref = 1
....
del ref
可以理解為如果想要 gc 主動釋放一個對象,就需要將其引用標志減少到無(0),關於標記指針的使用細節可以看 擴展Python模塊系列(四)----引用計數問題的處理
CPython 模塊的編寫約束
因為強調面向接口編程,所以 Python 模塊下的 libC 模塊都是在各自的倉庫編譯通過后,再通過 setup.py 模塊定義接口之間進行鏈接的。
也就是說不對編寫代碼風格做約束,但會對模塊的接口做約束。
要求每個模塊的層次關系分離,以模塊(PyModule)、對象(PyObject)、方法(PyCFunction)為接口參考,有如下結構。
+----------+ +-------------+
| +---------+ PyCFunction | 全局函數
| PyModule | +-------------+
| +<---+
+----------+ |
|模塊對象
+--+-------+
| PyObject +<---+
+----------+ |
|
+-------+-----+
| PyCFunction | 成員函數
+-------------+
因此請遵循該接口設計進行 Python 模塊的開發。
使用 bdist_wheel 打包對應平台 wheel 包
打包成對應平台的 wheel 的 bdist_wheel 的命令 setuptools 中支持。
而 distutils 只可以構建 bdist 包。
bdist_wheel 是將當前代碼構建的最終文件都打包好,然后在安裝的時候只需要釋放到具體的安裝目錄下就結束了,這對於一些不能進行編譯工作的硬件來說是極好的。
確認 wheel 包是否可以被安裝,只需要看名稱就知道了,例如 python3_maix-0.1.2-cp38-cp38-linux_x86_64.whl 包,我們可以看到 cp38-cp38-linux_x86_64 標識。
pip 在安裝的時候就會通過 from pip._internal.utils.compatibility_tags import get_supported 函數判斷當前系統是否可以支持這個包,如果你改名了,它也是可以安裝進去的,但能不能運行就取決於系統環境了,注意 armv7.whl 和 armv7l.whl 並不相同。
細節閱讀 2021 年 當安裝 wheel 出現 whl is not a supported wheel on this platform. 的時候
自動化測試框架 tox 的使用說明
在本機上使用 pip3 install tox 完成安裝,接着在 MaixPy3 根目錄下運行 tox 即可。
它會自動構建指定的 Python 虛擬測試環境,進行打包構建,安裝解包的測試,最后會收集整個目錄下的 test_*.py 的代碼加入到自動測試當中,如果你不想讓個別代碼參與測試,你可以改名成 no_test_*.py 方便排除和保留文件。
更多請自行查閱 Python 任務自動化工具 tox 教程 和官方文檔 tox.readthedocs.io ,以下為測試結果報告。
juwan@juwan-N85-N870HL:~/Desktop/v831_toolchain_linux_x86/MaixPy3$ tox
GLOB sdist-make: /home/juwan/Desktop/v831_toolchain_linux_x86/MaixPy3/setup.py
py38 inst-nodeps: /home/juwan/Desktop/v831_toolchain_linux_x86/MaixPy3/.tox/.tmp/package/1/MaixPy3-0.1.2.zip
py38 installed: attrs==20.3.0,iniconfig==1.1.1,packaging==20.8,Pillow==8.1.0,pluggy==0.13.1,py==1.10.0,pyparsing==2.4.7,pytest==6.2.1,MaixPy3 @ file:///home/juwan/Desktop/v831_toolchain_linux_x86/MaixPy3/.tox/.tmp/package/1/MaixPy3-0.1.2.zip,scripttest==1.3,toml==0.10.2
py38 run-test-pre: PYTHONHASHSEED='820562099'
py38 run-test: commands[0] | py.test
======================================= test session starts ========================================
platform linux -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
cachedir: .tox/py38/.pytest_cache
rootdir: /home/juwan/Desktop/v831_toolchain_linux_x86/MaixPy3
collected 5 items
ext_modules/_maix/example/test__maix.py . [ 20%]
tests/test_maix.py .... [100%]
======================================== 5 passed in 0.05s =========================================
_____________________________________________ summary ______________________________________________
py38: commands succeeded
congratulations :)
*關於 V831 或其他平台芯片如何使用
以上文檔為通用說明,使用方法差異的地方在於調用 Python 指令有所不同。
例如加載 V831 等其他平台的 SDK 環境后,要將上述命令中的 python3 改成對應 SDK 環境的 python3.8 用以調用交叉編譯的 Python 解釋器,從而完成目標 arm 平台的交叉編譯,這是由 SDK 提供時決定的,其他平台統一按這個結構載入即可。
后記
| 時間 | 備注 |
|---|---|
| 2021年01月10日 | 初稿 |
| 2021年01月11日 | 重審 |
