Sipeed MaixPy3 CPython 開發文檔


使用面向模塊接口開發,鏈接跨平台的 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日 重審


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM