Написание модулей расширения C для Python сложно: программист должен вручную управлять подсчетом ссылок и состоянием исключения, в дополнение к обычным опасностям кодирования в C. CPyChecker — это новая статическая проверка, разработанная Дэвидом Малкомом, чтобы спасти нас от наших ошибок. Я познакомился с ним на PyCon, когда Малкольм выступил с докладом « Смерть от тысячи утечек» . Инструмент находится в стадии разработки, глючит и сложен в установке, но чрезвычайно полезен для выявления ошибок кодирования. Я покажу вам, как его установить и для чего он нужен.
Установка
CPyChecker скрыт в общем наборе расширений для GCC, называемом GCC Python Plugin. Его код и система отслеживания ошибок находятся на fedorahosted.org, а документы — на ReadTheDocs . Дэвид Малколм называет CPyChecker «примером использования» плагина GCC Python и прямо говорит о его статусе:
Этот код находится в стадии разработки и все еще содержит ошибки. Нет ничего необычного в том, чтобы увидеть трассировку Python при запуске программы проверки. Вы должны проверить, что сообщает контролер, прежде чем действовать по нему: это может быть неправильно.
Я не смог собрать последний плагин GCC Python для Ubuntu, поэтому наш первый шаг — установить коробку Fedora 18 с Vagrant :
$ vagrant box add fedora-18 http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box
$ vagrant init fedora-18
Я добавил следующую строку в мой Vagrantfile, чтобы разделить каталоги Python virtualenv между хост-и гостевой ОС:
config.vm.share_folder "v-data", "/virtualenvs", "/Users/emptysquare/.virtualenvs"
Сейчас vagrant up
и vagrant ssh
. Как только мы окажемся в Fedora, установите зависимости времени сборки в соответствии с инструкциями GCC Python Plugin , затем получите исходный код GCC Python Plugin и соберите его с помощью make
. (По крайней мере, некоторые из самопроверок, которые он запускает после сборки, всегда заканчиваются неудачей.)
Я хотел свободно переключаться между Python 2.7 и 3.3, поэтому я дважды клонировал исходный код и собрал плагин для обеих версий Python в их собственных проверках.
проверки
Пересчет ошибок
Я сделал небольшой модуль Python на C, который увеличивает строку, которую нельзя увеличивать:
static PyObject* leaky(PyObject* self, PyObject* args) {
PyObject *leaked = PyString_FromString("leak!");
Py_XINCREF(leaked);
return leaked;
}
Теперь я строю свой модуль, вызывая CPyChecker вместо обычного компилятора:
$ CC=~/gcc-python-plugin/gcc-with-cpychecker python setup.py build
CPyChecker выплевывает свой вывод в терминал, но это едва понятно. Хорошая вещь находится в файле HTML, который это помещает в build/temp.linux-x86_64-2.7
:
CPyChecker указывает, что «ob_refcnt возвращаемого значения на 1 слишком высоко», когда PyString_FromString
успешно.
Нулевые указатели
Он также может помечать разыменование нулевого указателя. Если я заменил Py_XINCREF
на небезопасный Py_INCREF
, CPyChecker предупредит: «разыменование NULL (p-> ob_refcnt) при сбое PyString_FromString ()». То есть при PyString_FromString
возврате NULL
моя программа зависнет.
Аргумент Парсинг
Инструмент замечает несоответствия между строкой формата для PyArg_ParseTuple
и ее параметрами. Если у меня есть две единицы в строке формата, но я передаю три параметра, например:
int i;
const char* s;
float f;
PyArg_ParseTuple(args, "is", &i, &s, &f);
… CPyChecker предупреждает в консоли:
warning: Too many arguments in call to PyArg_ParseTuple with format string "is"
expected 2 extra arguments:
"int *" (pointing to 32 bits)
"const char * *"
but got 3:
"int *" (pointing to 32 bits)
"const char * *"
"float *" (pointing to 32 bits)
По некоторым причинам это предупреждение не появляется в выводе HTML, только в stdout, поэтому, увы, вы должны контролировать оба места, чтобы увидеть все предупреждения.
Состояние исключения
CPyChecker может пометить функцию, которая возвращается NULL
без установки исключения. Если я передам ему этот код:
static PyObject* randerr(PyObject* self, PyObject* args) {
PyObject *p = NULL;
if ((float)rand()/(float)RAND_MAX > 0.5)
p = PyString_FromString("foo");
return p;
}
Он предупреждает о последствиях принятия ложного пути:
Действительно, этот код выдает, SystemError
когда возвращается NULL
:
>>> import modtest
>>> modtest.randerr()
'foo'
>>> modtest.randerr()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
SystemError: error return without exception set
К сожалению, эта проверка является большим источником ложных срабатываний. Допустим, функция maybe_error
устанавливает исключение и возвращает 1, если она имеет ошибку, и возвращает 0 в противном случае:
static int maybe_error() {
if ((float)rand()/(float)RAND_MAX > 0.5) {
PyErr_SetString(PyExc_Exception, "error");
return 1;
} else {
return 0;
}
}
Его вызывающий знает это, поэтому, если maybe_error
возвращается 1, вызывающему не нужно устанавливать само исключение:
static PyObject* caller(PyObject* self, PyObject* args) {
if (maybe_error()) {
/* I know the error has been set. */
return NULL;
} else {
return PyString_FromString("foo");
}
}
На практике это работает правильно:
>>> modtest.caller()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: error
>>> modtest.caller()
'foo'
Но CPyChecker анализирует только пути кода через одну функцию за раз, поэтому он ошибочно критикует caller
за исключение исключения:
Расширения C, которые я помогаю поддерживать — те, что для PyMongo — используют этот шаблон в нескольких местах, поэтому у нас есть постоянные ложные срабатывания. Если CPyChecker вырастет во взрослый инструмент, такой как Coverity, который используется в системах CI, он должен будет либо выполнить межфункциональный анализ, либо иметь способ пометить определенные предупреждения как ложные срабатывания .
Вывод
Это первые дни CPyChecker, но это многообещающе. С более сложными функциями CPyChecker начинает действительно сиять. Он четко показывает, как различные пути в коде могут пересчитывать или занижать ссылки, разыменовывать нулевые указатели и тому подобное. Он достаточно хорошо понимает как Python C API, так и C stdlib. Я надеюсь, что Дэвид Малкольм и другие могут вскоре превратить его в настоящий продукт.