Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров26K
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 взаимодействия с системой.

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

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

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


Теги:
Хабы:
+90
Комментарии41

Публикации

Истории

Работа

Python разработчик
141 вакансия
Data Scientist
63 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн