Jan 20

给Python的C模块增加一个“析构函数” 不指定

felix021 @ 2013-1-20 00:50 [IT » Python] 评论(0) , 引用(0) , 阅读(4604) | Via 本站原创 | |
这周写了个模块,让python可以跟后台的C服务通信了。

总体上来说给python写个模块还是比较容易的,比给php写模块要舒服多了,但是还是遇到一个问题:给php写模块的时候可以用MINIT完成初始化、MSHUTDOWN完成清理;给Python写模块,有对应的模块初始化函数,但是却没有对应的清理函数,实在是令人蛋疼。

需求其实很简单(应该也很普遍吧?),只是要在python退出之前执行一点代码,保证在初始化的时候分配的一些资源能够被释放;但是模块本身并没有提供这样的机制,只好想其他办法了。

最简单的是用C语言本身提供的 atexit ,不过这是在main()结束或者调用exit()时 - 也就是说,在整个C环境要结束了的情况下才会运行(但是在关闭所有打开的文件之前)。

虽然有效,但是由于它不是python提供的机制,多少让人有点不太放心,所以还是看看别人怎么用的吧。

Google搜了一下"python  module  destructor",stackoverflow上说的是,可以用python的 atexit 模块。这是一个纯python模块,源码可以在 /usr/lib/pythonX.X/atexit.py 看到,其实就是通过 atexit.register() 注册退出函数,并将 atexit._run_exitfuncs() 函数绑定到 sys.exitfunc 。sys.exitfunc会在Py_Finalize()这个函数中被调用(call_sys_exitfunc())。

在C模块里调用它,最简单的办法是在模块的初始化函数里加入类似这样一句(注意换行和缩进):
    PyRun_SimpleString(
            "import atexit\n"
            "def __modname_clean():\n"
            "    import modname\n"
            "    modname.clean()\n"
            "atexit.register(__modname_clean)\n");

这大致相当于在python脚本里用eval执行了一段代码(没有指定globals、locals和compile_flag)。也可以用如下几乎等价的纯C代码
//注:m是在模块初始化函数中的定义的当前模块对象
    PyObject *pClean = PyObject_GetAttrString(m, "clean");
    if (pClean != NULL)
    {
        PyObject *pName = PyString_FromString("atexit");
        PyObject *pMod = PyImport_Import(pName);
        Py_DECREF(pName);
        if (pMod != NULL)
        {
            PyObject *pFunc = PyObject_GetAttrString(pMod, "register");
            if (pFunc && PyCallable_Check(pFunc))
            {
                PyObject *pArgs = Py_BuildValue("(O)", pClean);
                PyObject *ret = PyObject_CallObject(pFunc, pArgs);
                //if (ret == NULL) ; //sth went wrong
                Py_XDECREF(pArgs);
                Py_XDECREF(ret);
            }
            Py_XDECREF(pFunc);
        }
        Py_XDECREF(pMod);
    }
    Py_XDECREF(pClean);

由于错误处理、引用计数的代码占了一大半,所以代码这么长……

这种方法的好处是,它在python运行环境结束之前执行注册的函数,所以注册的函数仍然可以使用绝大部分python提供的功能(你甚至可以再import新的模块,但是最好别用thread...)。然而atexit有一个蛋疼的缺陷:尽管python在sys模块的doc里写的是“Assigning to sys.exitfunc is deprecated; use the atexit module instead.”,但是sys.exitfunc仍然是任意python脚本都可以修改的!所以通过atexit.register()注册的函数并不能100%保证被运行。

再回过头来看Py_Finalize()函数,这里其实是有很大槽点的:它有一个长达十一行的"sundry finalizers"段!
    /* Sundry finalizers */
    PyMethod_Fini();
    PyFrame_Fini();
    PyCFunction_Fini();
    PyTuple_Fini();
    PyList_Fini();
    PySet_Fini();
    PyString_Fini();
    PyByteArray_Fini();
    PyInt_Fini();
    PyFloat_Fini();
    PyDict_Fini();

也就是说,它为许多内置的module逐一硬编码了清理函数,却没有实现一个清理机制!

甚至在这后面还有一段注释:
    /* XXX Still allocated:
      - various static ad-hoc pointers to interned strings
      - int and float free list blocks
      - whatever various modules and libraries allocate
    */
坑爹啊。。。

再往下看,在Py_Finalize()的最后一行终于出现了本篇的另一个主角:call_ll_exitfuncs(); 这个函数的内容很简单:逐一执行 exitfuncs 这个数组里保存的函数。而 exitfuncs 这个数组的内容,则是由 Py_AtExit() 函数填进去的:
#define NEXITFUNCS 32  //另一个坑:最多只能注册32个exitfunc
static void (*exitfuncs[NEXITFUNCS])(void);
static int nexitfuncs = 0;

int Py_AtExit(void (*func)(void))
{
    if (nexitfuncs >= NEXITFUNCS)
        return -1;
    exitfuncs[nexitfuncs++] = func;
    return 0;
}

我最终采用的解决方案是这个:Py_AtExit()。

最后总结对比一下上述3种方案:

1. C语言的 atexit() : 使用链表保存注册的函数,只要内存够,数量没限制。在Python完全结束后执行。
2. Python的 atexit 模块:使用 list 保存注册的函数,在register的时候还可以带参数,在Python解释器仍然完整的情况下执行,一般来说很够用;但由于使用的sys.exitfunc可能会被其他脚本使用,并不能100%保证有效,故,有强迫症的慎用。或者应当在doc里明确说明。
3. Python C API的 Py_AtExit() :在Python环境几乎完全结束的时候被调用,最多只能注册32个函数(所以最好要检查返回值)。其实从流程上来说,在这之后马上就是main的return或者exit()函数调用,所以跟atexit基本上差不多。

p.s. 另一个尝试但是失败了的方案:建立一个新的type X_Type,在它的tp_dealloc里头写入清理代码,以X_Type建立一个新的class X,在模块的初始化函数中new一个X,撂着,并期望Py_Finalize()里的PyGC_Collect()会把它回收掉。不知道还差了点什么,如果哪位大牛知道,还望不吝赐教。



欢迎扫码关注:




转载请注明出自 ,如是转载文则注明原出处,谢谢:)
RSS订阅地址: https://www.felix021.com/blog/feed.php
发表评论
表情
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
打开HTML
打开UBB
打开表情
隐藏
记住我
昵称   密码   *非必须
网址   电邮   [注册]