Пишем модуль расширения для Питона на C

    OMFG! — может воскликнуть читатель. Зачем писать что-то на С когда есть Python, и будет во многом прав. Однако, к счастьюсожалению наш зелёный друг не всесилен. Итак…

    Описание задачи


    В рамках текущего проекта (система управления виртуальными машинами, на базе Libvirt), понадобилось программно рулить loop девайсом в Linux. Первая версия когда основанная на вызове командлайн-команды losetup через subprocess.Popen() весьма сносно работала на моей Ubuntu 8.04, однако после деплоя пошли баг-репорты о том что на RHEL и некоторых других системах заявленный функционал не работает. После некоторых разбирательств выяснилось что в них losetup принимает немного другие аргументы, и просто нашу задачу реализовать не получится.

    Поковырявшись в исходниках losetup, я увидел что все необходимые мне операции делаются путём отправки IOCTL вызовов в устройство. С питоновским fcntl.ioctl() у меня что-то не заладилось. Было принято решение опуститься на уровень ниже, написать модуль на C.

    Disclaimer


    Как потом выяснилось fcntl.ioctl() вполне достаточен для реализации всего что мне было нужно. Уже не помню что меня в нём испугало в начале. Наверное нужно работать меньше 10 часов в день ;)

    С другой стороны, если бы я сразу его использовал — этого топика бы не было.

    Итак ещё раз, для тех кто читает по диагонали — в Питоне есть отличный модуль fcntl.ioctl(). Всё что ниже читать просто как пример.

    Планирование API


    Всё что можно делать на Питоне — делать на Питоне. То что не получается — выносить в low-level на C.

    Того что не получается сделать на питоне — набралось немного: собственно монтирование/размонтирование образа, и проверка, занят ли девайс.

    В рамках задачи не стояли требования по поддержке шифрования, и прочих наворотов поэтому со стороны C интерфейс получился достаточно простым:
    • mount(device, imagepath) — монтирует imagepath в device.
    • unmount(device, imaepath) — освобождает device.
    • is_used(device) — 1 если устройство смонтировано, и 0 если свободно


    Делаем скелет


    Модуль, по аналогии с командлайновой утилитой будет называться losetup. Запускаем любимый Eclipse + PyDev и создаём проект. В нём создаём losetup.py в котором будет весь питоновский код модуля.

    Модуль который реализует low-level взаимодействие с системой назовём _losetup. Наш losetup будет импортировать _losetup и использовать его для реализации высокоуровнёвого API.

    Создаём папку src, в которой кладём два файла losetupmodule.c и losetupmodule.h

    losetupmodule.c
    #include <Python.h&rt;

    #include "losetupmodule.h"

    // Исключение которое мы будем бросать в случае какой-то ошибки
    static PyObject *LosetupError;

    // Монтирование образа в девайс
    static PyObject *
    losetup_mount(PyObject *self, PyObject *args)
    {
        return Py_BuildValue("");
    }

    // Размонтирование девайса
    static PyObject *
    losetup_unmount(PyObject *self, PyObject *args)
    {
        return Py_BuildValue("");
    }

    // Проверка, смонтировано ли что-то в девайсе
    static PyObject *
    losetup_is_used(PyObject *self, PyObject *args)
    {
        int fd, is_used;
        const char *device;
        struct loop_info64 li;

        if (!PyArg_ParseTuple(args, "s"&device)) {
            return NULL;
        }

        if ((fd = open (device, O_RDONLY)) < ) {
            return PyErr_SetFromErrno(LosetupError);
        }

        is_used = ioctl(fd, LOOP_GET_STATUS64, &li) == ;

        close(fd);
        return Py_BuildValue("i", is_used);
    }

    // Таблица методов реализуемых расширением
    // название, функция, параметры, описание
    static PyMethodDef LosetupMethods[] = {
        {"mount",  losetup_mount, METH_VARARGS, "Mount image to device. Usage _losetup.mount(loop_device, file)."},
        {"unmount",  losetup_unmount, METH_VARARGS, "Unmount image from device.  Usage _losetup.unmount(loop_device)."},
        {"is_used", losetup_is_used, METH_VARARGS, "Returns True is loopback device is in use."},
        {NULLNULLNULL}        /* Sentinel */
    };

    // Инициализация
    PyMODINIT_FUNC
    init_losetup(void)
    {
        PyObject *m;

        // Инизиализруем модуль _losetup
        m = Py_InitModule("_losetup", LosetupMethods);
        if (m == NULL)
            return;

        // Создаём исключение
        LosetupError = PyErr_NewException("_losetup.error"NULLNULL);
        Py_INCREF(LosetupError);
        PyModule_AddObject(m, "error", LosetupError);
    }


    В losetupmodule.h просто набор определений безжалостно выдранный из util-linux-ng

    Настраиваем сборку


    Собирать модули можно по разному, но самый простой и надёжный — это через setuptools (distutils).

    Создаём setup.py
    from setuptools import setup, Extension
    setup(name='losetup',
          version='1.0.1',
          description='Python API for "loop" Linux module',
          author='Sergey Kirillov',
          author_email='serg@rainboo.com',
          ext_modules=[Extension('_losetup', ['src/losetupmodule.c'], include_dirs=['src'])],
          py_modules=['losetup']
    )

    Вся белая магия в строке «ext_modules=[Extension('_losetup', ['src/losetupmodule.c'], include_dirs=['src'])]». Тут описывается расширение с именем _losetup, код которого находится в src/losetupmodule.c, инклуды в src. Этого достаточно чтобы дистутилс мог собрать расширение, установить его, делать из него всяческие пекеджи (в том числе win32 инсталлер, хотя там и не всё так просто).

    Проверяем что всё билдится путём вызова «python setup.py build»

    Наращиваем мышцы


    Реализуем метод mount()
    static PyObject *
    losetup_mount(PyObject *self, PyObject *args)
    {
        int ffd, fd;
        int mode = O_RDWR;
        struct loop_info64 loopinfo64;
        const char *device, *filename;

        // Check parameters
        if (!PyArg_ParseTuple(args, "ss"&device, &filename)) {
            return NULL;
        }

        // Initialize loopinfo64 struct, and set filename
        memset(&loopinfo64, sizeof(loopinfo64));
        strncpy((char *)loopinfo64.lo_file_name, filename, LO_NAME_SIZE-1);
        loopinfo64.lo_file_name[LO_NAME_SIZE-1= ;

        // Open image file
        if ((ffd = open(filename, O_RDWR)) < ) {
            if (errno == EROFS) // Try to reopen as read-only on EROFS
                ffd = open(filename, mode = O_RDONLY);
            if (ffd < ) {
                return PyErr_SetFromErrno(LosetupError);
            }
            loopinfo64.lo_flags |= LO_FLAGS_READ_ONLY;
        }

        // Open loopback device
        if ((fd = open(device, mode)) < ) {
            close(ffd);
            return PyErr_SetFromErrno(LosetupError);
        }

        // Set image
        if (ioctl(fd, LOOP_SET_FD, ffd) < ) {
            close(fd);
            close(ffd);
            return PyErr_SetFromErrno(LosetupError);
        }
        close (ffd);

        // Set metadata
        if (ioctl(fd, LOOP_SET_STATUS64, &loopinfo64)) {
            ioctl (fd, LOOP_CLR_FD, );
            close (fd);
            return PyErr_SetFromErrno(LosetupError);
        }
        close(fd);

        return Py_BuildValue("");
    }



    Вроде бы несложно, однако возможно не совсем понятно что тут происходит. Давайте разберём основные элементы.
    if (!PyArg_ParseTuple(args, "ss"&device, &filename)) {
        return NULL;
    }



    Функции объявленные как METH_VARARGS получают аргументы в виде кортежа. PyArg_ParseTuple() проверяет что аргументы соответствуют указанному шаблону (в данном случае «ss» — две строки), и получает данные, либо, в случае если аргумент не соответствуют шаблону устанавливает ошибку, и возвращает false. Детали о том как это работает можно прочитать в Extracting Parameters in Extension Functions

    С точки зрения питона это выглядит так:
    >>> import _losetup
    >>> _losetup.mount("aaa")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: function takes exactly 2 arguments (1 given)
    >>> _losetup.mount(1,2)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: argument 1 must be string, not int
    >>> 
    


    Идём дальше
    return PyErr_SetFromErrno(LosetupError);


    PyErr_SetFromErrno создаём исключение с указаным типом, получает код ошибки из глобальной переменной errno, и возвращает NULL — что означает что произошло исключение. Ссылки на документацию: Intermezzo: Errors and Exceptions , Exception Handling

    Для питона это выглядит так:
    >>> _losetup.mount('/dev/loop0', '/tmp/somefile')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    _losetup.error: (2, 'No such file or directory')
    >>> 
    


    return Py_BuildValue("");


    Нашей функции не нужно возвращать никаких особых данных, поэтому мы возвращаем None. Подробнее можно прочитать в Building Arbitrary Values

    Остальные функции реализуются аналогично.

    Публикация на PyPI


    Итак модуль написан. Нужно дать человечеству шанс им воспользоваться. Самый простой способ это сделать — опубликовать модуль на Python Package Index.

    Регистрируемся на PyPI.

    После регистрации пишем в консоли
    python setup.py register

    вводим данные своего аккаунта, и setuptools создёт пакет на PyPI.

    python setup.py sdist upload

    делает source destribution (tgz архив с кодом и метаданными), и заливает его на PyPI.

    Результат можно увидеть тут http://pypi.python.org/pypi/losetup/

    Идём шелом на ненавистный RHEL, пишем easy_install -U losetup, и, пока мы говорим волшебные слова «крибле-крабле-бумц», setuptools скачает наш пакет, сбилдит его и установит в систему.

    Добавляем losetup как зависимость в setup.py основного приложения. Теперь при его инсталляции setuptools поставит и наш модуль.

    Завершение


    Вот так, неожиданно легко оказалось опуститься с Python на уровень абстракции ниже, и написать модуль для low-level взаимодействия с системой.

    Так-же получили хороший пример того что нужно больше думать и меньше делать. Наш Зелёный Друг могуч, и даже такие экзотические задачи можно решать не расставаясь с ним.

    Чего и вам желаю.

    Использованая литература


    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 41
    • –5
      ну ниче так, интересно.
      • +1
        кто знает сколько кармы нужно чтобы можно было перенести в «Язык программирования Python»?
        • +2
          Перенёс. Спасибо всем кто поделился кармой.
        • +2
          Любопытно что народ плюсует, но не комментирует. Нечего добавить? ;)
          • +5
            все понимают, что статья нужная и хорошая, но для хороших комментариев нехватает знаний в предметной области.
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                Вполне закономерный вопрос. Хотел написать этом в топике и забыл. SWIG это хорошо и удобно, но для такого маленького модуля, imho, это оверкилл. У меня в сумме 3 часа где-то на всё вместе ушло. Да и интересно было написать без обёрток.

                Ну а если нужен интерфейс к чему-то готовому, то SWIG конечно лучше.
              • +1
                А всем исходный код нравится и вызывает симпатию, поэтому когда первый раз прокручивают бегло статью — плюсуют) а чтобы что-то написать, надо время пока народ осилит много букв)
              • +2
                Язык С позволяет достичь огромного прироста производительности по сравнению с некоторыми родными модулями Питона. Например, есть великолепный модуль Pickle, который запаковывает объект в строку или файл в бинарном режиме. Он хорош и быстр, но есть модуль cPickle, который работает в ТЫСЯЧИ раз быстрее своего питоновского аналога.

                А вообще если дадите кармы, напишу статью о консервации и последующего использования объектов. Я много интересного знаю про Питон.
                • 0
                  А ты напиши, а потом дадут. Я впервые что-то на хабр пишу, сегодня утром 0 была.
                  • 0
                    Всё дело в том, что некая группа товарищей в свое время меня основательно минуснула, так что никуда написать не могу, кроме комментов, а в комменты писать как-то не сильно хочется, слишком тексту много.
              • –6
                >>> OMFG! — может воскликнуть читатель. Зачем писать что-то на С когда есть Python, и будет во многом прав.

                ха-ха, вы что действительно так считаете? это типа — зачем писать ОС на си когда есть Python да? ха-ха!
                • –1
                  а вот и быдлопитонщики подоспели со совими минусами :)
                  • 0
                    А вот и подоспел дежурный говнотролль. Гори в аду, тебя не слышат.
                  • 0
                    я где-то говорил что я пишу ОС? :)
                    • 0
                      А если серьёзно — то лёгкость которую даёт питон подкупает, к ней привыкаешь, и менять её на что-то другое уже не очень хочется. Думаю я не один такой.

                      PS: ОС на C у нас уже полно. Почему бы не написать ещё одну на Python? :P
                      • 0
                        ОС — пока нет, но программы для каких-то муниципальных компьютеров было однажды написано именно на Питоне.
                        • –1
                          ну точто на питоне можно быстро реализовать сносно рабочую поделку — это я и сам знаю… только в отличие от вас я трезво оцениваю возможности питона и его место…

                          нащет ОС, ага, пишите :)…
                          • 0
                            спасибо что вернули нас на землю
                      • +1
                        Зелёный друг? O_o
                      • +1
                        В коде, в условиях после < ничего не видно (смотрю в Opera/Windows).
                        • 0
                          Есть и другие ошибки — такое же «повисшее» присваивание, пропавшие параметры.
                          • 0
                            да, спасибо что сказал. хайлайтер или хабр съел код. сейчас исправлю.
                            • 0
                              фух, вроде-бы поправил. оказалось что хабр безжалостно жрёт конструкции <font color="#444444">0</font>, а нулей там в коде было предостаточно.

                              написал свой форматтер к Pygments который в в том числе выдаёт <font>0</font> как <font>0& shy;</font>. помогло
                          • 0
                            Whoohoo! Спасибо. Мне это супер полезно. Я как раз пару узких мест в pys60 должен прооптимизировать.
                            • 0
                              Программист должен быть ленив. Гораздо проще и приятнее писать экстеншены на boost.python. В крайнем случае, если очень не хочется C++, существует SWIG

                              Впрочем, если надо написать простой модуль-прослойку из пары функций, нативный питоновский интерфейс достаточно удобен.
                              • 0
                                ленивые програмисты и С++ лол… pyrex откройте для себя чтоли.
                                • 0
                                  О ужас, pyrex-ом кто-то пользуется??? Мне он показался невообразимым костылем, поскольку не решает главной проблемы — простого и быстрого построения интерфейса к готовым библиотекам из мира C/C++.

                                  А если просто надо быстро что-то сосчитать, в 90% случаев хватает NumPy и Weave.
                              • 0
                                rushman, а можно вас попросить написать статейку про то как вы с libvirt из питона работаете?
                                я помнится пытался в ней покопаться, но что-то не осилил.

                                кстати, можно на вашу систему управления машинками посмотреть?:)
                                • 0
                                  можно.

                                  интересует что-то конкретное, или в целом обзор того как это делается?
                                  • 0
                                    скорее обзор.
                                    • 0
                                      ок, разгребусь немного с работой и напишу. у нас сейчас релиз ;)
                                • 0
                                  Лого у вашего проэкта супер! =)
                                  • 0
                                    какого из них?
                                    • 0
                                      Libvirt, или это я чего-то перепутал? всё равно супер! )
                                      • 0
                                        Не, Libvirt под крылом RedHat'а делается. Мы его только используем.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.