15.5 从扩展模块中定义和导出C的API

    本节主要问题是如何处理15.4小节中提到的Point对象。仔细回一下,在C代码中包含了如下这些工具函数:

    现在的问题是怎样将 和 Point_FromPoint() 函数作为API导出,这样其他扩展模块能使用并链接它们,比如如果你有其他扩展也想使用包装的Point对象。

    要解决这个问题,首先要为 sample 扩展写个新的头文件名叫 ,如下:

    1. /* pysample.h */
    2. #include "Python.h"
    3. #include "sample.h"
    4. #ifdef __cplusplus
    5. extern "C" {
    6. #endif
    7.  
    8. /* Public API Table */
    9. typedef struct {
    10. Point *(*aspoint)(PyObject *);
    11. PyObject *(*frompoint)(Point *, int);
    12. } _PointAPIMethods;
    13.  
    14. #ifndef PYSAMPLE_MODULE
    15. /* Method table in external module */
    16. static _PointAPIMethods *_point_api = 0;
    17.  
    18. /* Import the API table from sample */
    19. static int import_sample(void) {
    20. _point_api = (_PointAPIMethods *) PyCapsule_Import("sample._point_api",0);
    21. return (_point_api != NULL) ? 1 : 0;
    22. }
    23.  
    24. /* Macros to implement the programming interface */
    25. #define PyPoint_AsPoint(obj) (_point_api->aspoint)(obj)
    26. #define PyPoint_FromPoint(obj) (_point_api->frompoint)(obj)
    27. #endif
    28. #ifdef __cplusplus
    29. }
    30. #endif

    这里最重要的部分是函数指针表 _PointAPIMethods .它会在导出模块时被初始化,然后导入模块时被查找到。修改原始的扩展模块来填充表格并将它像下面这样导出:

    1. /* ptexample.c */
    2.  
    3. /* Include the header associated with the other module */
    4. #include "pysample.h"
    5.  
    6. /* An extension function that uses the exported API */
    7. static PyObject *print_point(PyObject *self, PyObject *args) {
    8. PyObject *obj;
    9. Point *p;
    10. if (!PyArg_ParseTuple(args,"O", &obj)) {
    11. return NULL;
    12. }
    13.  
    14. /* Note: This is defined in a different module */
    15. p = PyPoint_AsPoint(obj);
    16. if (!p) {
    17. return NULL;
    18. }
    19. printf("%f %f\n", p->x, p->y);
    20. return Py_BuildValue("");
    21. }
    22.  
    23. static PyMethodDef PtExampleMethods[] = {
    24. {"print_point", print_point, METH_VARARGS, "output a point"},
    25. { NULL, NULL, 0, NULL}
    26. };
    27.  
    28. PyModuleDef_HEAD_INIT,
    29. "ptexample", /* name of module */
    30. "A module that imports an API", /* Doc string (may be NULL) */
    31. -1, /* Size of per-interpreter state or -1 */
    32. PtExampleMethods /* Method table */
    33. };
    34.  
    35. /* Module initialization function */
    36. PyMODINIT_FUNC
    37. PyInit_ptexample(void) {
    38. PyObject *m;
    39.  
    40. m = PyModule_Create(&ptexamplemodule);
    41. if (m == NULL)
    42. return NULL;
    43.  
    44. /* Import sample, loading its API functions */
    45. if (!import_sample()) {
    46. return NULL;
    47. }
    48.  
    49. return m;
    50. }

    编译这个新模块时,你甚至不需要去考虑怎样将函数库或代码跟其他模块链接起来。例如,你可以像下面这样创建一个简单的 setup.py 文件:

    如果一切正常,你会发现你的新扩展函数能和定义在其他模块中的C API函数一起运行的很好。

    1. >>> import sample
    2. >>> p1 = sample.Point(2,3)
    3. >>> p1
    4. <capsule object "Point *" at 0x1004ea330>
    5. >>> import ptexample
    6. >>> ptexample.print_point(p1)
    7. 2.000000 3.000000

    本节基于一个前提就是,胶囊对象能获取任何你想要的对象的指针。这样的话,定义模块会填充一个函数指针的结构体,创建一个指向它的胶囊,并在一个模块级属性中保存这个胶囊,例如 .

    其他模块能够在导入时获取到这个属性并提取底层的指针。事实上,Python提供了 PyCapsule_Import() 工具函数,为了完成所有的步骤。你只需提供属性的名字即可(比如sample._point_api),然后他就会一次性找到胶囊对象并提取出指针来。

    最后,还有一个重要的原因让你去使用这个技术来链接模块——它非常简单并且可以使得各个模块很清晰的解耦。如果你不想使用本机的技术,那你就必须使用共享库的高级特性和动态加载器来链接模块。例如,将一个普通的API函数放入一个共享库并确保所有扩展模块链接到那个共享库。这种方法确实可行,但是它相对繁琐,特别是在大型系统中。本节演示了如何通过Python的普通导入机制和仅仅几个胶囊调用来将多个模块链接起来的魔法。对于模块的编译,你只需要定义头文件,而不需要考虑函数库的内部细节。

    更多关于利用C API来构造扩展模块的信息可以参考

    原文: