我给Vim的Python3支持打了个补丁,发到邮件列表上只有Bram表示希望有人来测试就没有下方了。于是,这么久了,这个补丁的内存泄漏问题一直未被发现,直到看到蓝色基因的这篇文章。花了一个下午,发现我原来的补丁不仅没有修正本来就有的内存泄漏,反而雪上加霜,浪费了更多的内存。现在终于弄好了,放在我的陈列室里了,同时还莫名其妙地修正了另一个小问题。
既然是内存泄漏,我首先想到的是valgrind这个工具。于是跑了一下:
valgrind --leak-check=full --show-reachable=yes vim
在开启的 Vim 中我 source 了蓝色基因的测试脚本:
lcd %:h
tabedit tmpbuffer
setlocal buftype=nofile
python3 << EOF
for i in range(3):
flines= ['x'*200] * 50000
vim.command("%s+\\_.*++g")
for fl in flines:
vim.current.buffer.append(fl)
del flines[:]
EOF
整个过程CPU占到100%,而且运行速度极慢,内存消耗也非常多。最后Vim终于按我的指令退出时,valgrind刷屏了大约十几秒钟!而其间我看到除了Python的字样外,还有不少rb的字样。难道Ruby支持也有类似的问题?不过我不管它。重新编译了个只有--enable-python3interp
选项的 Vim,这回跑起来快了一些,也没有那么多不相干的内存泄漏了。我也学聪明了点,把信息重定向到文件:
valgrind --leak-check=full --show-reachable=yes src/vim 2> log
这样可以方便地在log中找“if_py”字符串了。可惜我弄的时候没想到自己会来写博客,所以log文件并没有保存。。。
首先我找到了DoPy3Command
这个函数,valgrind说它里面分配的内存没有被释放。这里边的PyUnicode_AsEncodedString
这块是我加的:
/* PyRun_SimpleString expects a UTF-8 string. Wrong encoding may cause
* SyntaxError (unicode error). */
cmdstr = PyUnicode_Decode(cmd, strlen(cmd), p_enc, NULL);
PyRun_SimpleString(PyBytes_AsString(PyUnicode_AsEncodedString(cmdstr, "utf-8", NULL)));
然后我能怎么办呢?当然是查Python的文档了。于是注意到文档上说PyUnicode_AsEncodedString
返回的是新的引用。又去看官方教程上的示例,才知道如果一个API返回了新的引用,那么用完后应当手动Py_XDECREF
!就像是strdup
函数,它内部帮你malloc了,你自己用完后要记着free掉。(Py_XDECREF
和Py_DECREF
的差别是,前者可以传NULL
。)
于是就改吧,所有通过PyUnicode_AsEncodedString
得到的对象都要Py_XDECREF
下。为此,不仅需要临时变量来存储这个对象,更让我郁闷的是,在两个Python版本共有的函数StringToLine
中有这样一段代码:
str = PyString_AsString(bytes);
len = PyString_Size(bytes);
这里的两个函数/宏我之前是这样定义的:
#define _PyUnicode_AsBytes(obj) PyUnicode_AsEncodedString(obj, p_enc, NULL)
#define PyString_AsString(obj) PyBytes_AsString(_PyUnicode_AsBytes(obj))
#define PyString_Size(obj) PyBytes_GET_SIZE(_PyUnicode_AsBytes(obj))
这下我没辙了,只好又改了if_py_both.h和if_python.c文件,加了两个宏:PyString_AsBytes
和PyString_FreeBytes
。它们在 Python2 的代码中什么也不做,但是在 Python3 的代码中用来保存和释放中间对象:
#define PyString_AsBytes(obj) PyUnicode_AsEncodedString(obj, p_enc, NULL);
#define PyString_FreeBytes(obj) Py_XDECREF(bytes)
#define PyString_AsString(obj) PyBytes_AsString(obj)
#define PyString_Size(obj) PyBytes_GET_SIZE(bytes)
有人说,if it ain't broken, don't fix it。可是,虽然问题只出在 Python3 部分,我还是得改 Python2 部分,感觉很不爽。
这样改完,再次反复运行测试代码,结果不遂人愿,依旧泄漏了不少内存。于是继续valgrind,又找到这里:
static void
BufferDestructor(PyObject *self)
{
BufferObject *this = (BufferObject *)(self);
if (this->buf && this->buf != INVALID_BUFFER_VALUE)
this->buf->b_python3_ref = NULL;
}
然后再次查教程中的示例:
static void
Noddy_dealloc(Noddy* self)
{
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free((PyObject*)self);
}
再看看 Python2 部分的代码,在相应的函数里有Py_DECREF
,于是把这示例的最后一行给BufferDestructor
以及WindowDestructor
和RangeDestructor
加上。再测试,内存不再消耗100多M了,反复source也不会继续增加,于是作出结论:Vim 的 Python3 支持部分没有已知的 bug 了!
做完这一切,我只想说:Vim 这 Python3 支持也太 broken 了吧,中文经常乱码就算了,vim.error
不能用我也忍了,竟然还内存泄漏!难道写这个代码的人也是初学Python C API啊?
不过抱怨归抱怨,还是很感谢原作者的,不然我连修正都不可能。不过,patch 弄好也提交了,却一直没人理我,原作者难道是一时兴起才写的、然后就消失了?
最后,补丁现在放到陈列室了。