【问题标题】:Creating Python modules from an extension with extra C data structures从具有额外 C 数据结构的扩展创建 Python 模块
【发布时间】:2021-11-23 13:31:43
【问题描述】:

我正在开发一个自定义 Python 加载器,它可以从特定类型的非 Python 文件创建 Python 模块,我们称之为“奶酪文件”。我将我的项目编写为 C 扩展模块,因为这些“奶酪文件”需要由相当复杂的 C 库处理(其次,作为了解 Python/C API 的一种方式)。

在处理奶酪文件时,C 库会分配一些数据结构,这些数据结构需要在删除 Python 模块对象后释放。我的问题是,我如何/应该将这些 C 数据结构与 Python 模块对象一起存储?

我的一些想法:

  • 我认为最干净的选择是从 C 中继承 Python 的 ModuleType 并在子类中添加一个字段来存储数据结构。 extension types tutorial 显然很清楚如何在 C 中对内置 Python 类型进行子类化,所以我希望能够做这样的事情:

    typedef struct {
        PyModuleObject module;
        struct cheese_data* data;
    } PyCheeseModule;
    
    static PyTypeObject PyCheeseModule_Type = {
        PyVarObject_HEAD_INIT(NULL, 0)
        .tp_basicsize = sizeof(PyCheeseModule),
        .tp_flags = Py_TPFLAGS_DEFAULT,
        .tp_base = &PyModule_Type,
        /* other fields */
    };
    

    但是PyModuleObject 没有作为 Python/C API 的一部分公开,所以我不能以这种方式使用它。

  • 我还考虑为每个奶酪模块动态分配PyModuleDef,并使用PyModule_FromDefAndSpec()PyModule_ExecDef() 来创建和执行实际模块。但是我不确定PyModuleDef 是否应该以这种方式使用,因为文档仅使用静态定义的C 扩展模块演示它,无论如何我都必须释放PyModuleDef 对象本身带来的那种我回到同样的问题。

  • 另一种方法是将 C 数据结构包装在 Python 对象中,然后将其添加到模块的字典中。但是,Python 代码可能会更改或取消设置该属性。我想我可以找到解决此问题的方法,但这似乎很不优雅。

  • 也许我应该在这个项目中使用 Cython。但即便如此,如果这个问题在 Cython 中是可以解决的,也应该可能使用普通的旧 Python/C API 来解决,至少对于教育价值我想知道如何。

如有必要,我可以用一些额外的代码来扩展这些尝试。

【问题讨论】:

    标签: python c python-3.x python-c-api


    【解决方案1】:

    几个选项:

    使用堆类型而不是模块?

    很有可能您实际上并不需要模块对象。模块对象有两个主要特性——它们有一个__name__,它们有一个可修改的命名空间(即__dict__),但这些可以通过任何扩展类型来实现。

    在 Python 中您通常希望返回模块的大多数地方实际上并不需要它:您可以在模块初始化过程中返回任何类型,并且可以将任何类型添加到 sys.modules

    模块的通用堆类型的主要优点(对您而言)是堆类型不需要在其生命周期中存在的PyModuleDef - 创建它们的规范由@987654330 复制@(和相关函数),所以只需要为该函数调用而存在。

    一种选择是创建一个通用结构来处理类型的基础:

    typedef struct {
        PyObject_HEAD;
        const char* name;
        PyObject* dict;
        struct cheese_data* data;
    } BaseStruct;
    

    另一种变化是使用灵活的数组成员作为最后一个成员 (char extra_space[];),并在您提供 PyType_Spec.basicsize 时请求额外的空间。

    您可以在Py_tp_new 槽中处理初始化,在Py_tp_dealloc 槽中进行清理(与往常一样)。

    最后,您将为__name____dict__ 提供PyMemberDefs - 类似于:

    static PyMemberDef cheese_members[] = {
        {"__dictoffset__", T_PYSSIZET, offsetof(BaseStruct, dict), READONLY},
        {"__name__", T_STRING, offsetof(BaseStruct, name), READONLY},
        {NULL}  /* Sentinel */
    };
    

    这应该具有模块的大部分行为,但很少有限制。

    使用模块

    模块状态是为存储这种额外信息而设计的。PEP-3121 给出了一个设置模块状态的示例,我将在此尝试总结。我认为您不能直接使用该示例,因为我认为自 PEP 以来字段的顺序略有变化!

    您创建一个用于模块状态的结构

    typedef struct {
        struct cheese_data* data;
        /* alternatively cheese_data might just be part of the struct
         * rather than dynamically allocated in addition to the struct
         */
    } MyModState;
    

    PyModuleDef 中指定m_size 选项,通常为sizeof(MyModState)。这里的复杂之处在于您不能使用静态PyModuleDef,因为您的目标是动态创建此模块:

    PyModuleDef *mod_def = malloc(sizeof(PyModuleDef));
    struct PyModuleDef mod_def_tmp = {
        PyModuleDef_HEAD_INIT,
        .m_name = "some name",
        .m_size = sizeof(MyModState),
        /* etc */
    };
    *mod_def = mod_def_tmp;
    

    初始化状态结构进入Py_mod_exec 槽(假设您正在使用多阶段初始化):

    static int
    my_module_exec(PyObject *m) {
        MyModState* state = (MyModState*)PyModule_GetState(m);
        state->data = malloc(sizeof(struct cheese_data));
        /* etc */
    }
    

    通过在模块规范中指定 m_free function 来处理状态清理,

    static void free_module(PyObject* m) {
        MyModState* state = (MyModState*)PyModule_GetState(m);
        free(state->data);
    }
    
    /* in the PyModuleDef */
        .m_free = free_module,
    /* ... */
    

    最后一个问题是取消分配PyModuleDef,这样它就不会泄漏内存。 Looking into the Python source code 您会看到 PyModuleDef 对象的最后一次使用是查找 m_free 并调用它。

    因此,我相信通过free(m->m_def)m_free 中释放模块def 也应该是安全的(并且还释放动态分配的任何部分,例如名称字符串)。但是 - 这似乎是一个 hacky 解决方案,因此我不会将其视为所有 Python 版本的完全面向未来的解决方案。

    【讨论】:

      猜你喜欢
      • 2018-01-17
      • 2020-09-09
      • 2010-09-20
      • 1970-01-01
      • 1970-01-01
      • 2017-04-04
      • 2016-11-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多