Статьи

Анализ расширений Python C с помощью CPyChecker

Написание модулей расширения 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: leaky ()

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;
}

Он предупреждает о последствиях принятия ложного пути:

CPyChecker: randerr ()

Действительно, этот код выдает,  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 за исключение исключения:

CPyChecker: caller ()

Расширения C, которые я помогаю поддерживать — те, что для PyMongo — используют этот шаблон в нескольких местах, поэтому у нас есть постоянные ложные срабатывания. Если CPyChecker вырастет во взрослый инструмент, такой как Coverity, который используется в системах CI, он должен будет либо выполнить межфункциональный анализ, либо иметь  способ пометить определенные предупреждения как ложные срабатывания .

Вывод

Это первые дни CPyChecker, но это многообещающе. С более сложными функциями CPyChecker начинает действительно сиять. Он четко показывает, как различные пути в коде могут пересчитывать или занижать ссылки, разыменовывать нулевые указатели и тому подобное. Он достаточно хорошо понимает как Python C API, так и C stdlib. Я надеюсь, что Дэвид Малкольм и другие могут вскоре превратить его в настоящий продукт.