13 November 2008

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

Python
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."},
{NULL, NULL, , NULL} /* Sentinel */
};

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

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

// Создаём исключение
LosetupError = PyErr_NewException("_losetup.error", NULL, NULL);
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 взаимодействия с системой.

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

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

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


Tags:pythonextensionmodulelinuxloopioctlpypipython package index
Hubs: Python
+90
20.8k 124
Comments 41
Popular right now
Python для работы с данными
December 7, 202031,500 ₽Нетология
Python QA Engineer
December 21, 202060,000 ₽OTUS
Administrator Linux. Professional
January 18, 202180,000 ₽OTUS
Безопасность Linux
February 12, 202130,000 ₽OTUS
Top of the last 24 hours