Выпуск 3. Май 2013

Pinto — собственный CPAN из коробки | Содержание | Введение в разработку web-приложений на PSGI/Plack. Часть 2.

Введение в Perl XS

Вероятно, многим Perl-программистам никогда не приходилось применять язык XS в разработке. Поэтому, если для вас акроним XS ассоциируется больше с размером одежды, то это нормально и, кстати, удачно описывает узость ниши его применения, но, ни в коем случае, не объём предоставляемых возможностей. Знание о том, какие возможности открывает создание расширений для Perl с использованием XS и какие требования налагает его использование в программах, безусловно будет полезно для всех.

Что такое XS?

XS — акроним от eXternal Subroutine (внешняя подпрограмма), представляет собой макроязык, предназначенный для стыковки кода функций, написанных на языке C (или C++) для использования в Perl-программах. Макроязык XS описывает интерфейс функций и служит для согласования модели вызова Perl-функций с моделью вызова C-функций, что включает в себя преобразование типов и манипуляции с размещением аргументов функций и возвращаемых значений. Каждую отдельно описанную функцию в интерфейсе принято называть XSUB.

XS используется в тех случаях, когда требуется сделать обвязки (bindings) или интерфейс к существующим C-библиотекам для использования в Perl. Например, модуль Gtk3 — это интерфейс к C-библиотеке libgtk3.

XS может использоваться для написания части функций критичных к скорости выполнения или объёму потребления памяти, реализация которых на языке Perl может быть значительно медленнее или требовать больше ресурсов, чем написанная на языке C. Примером могут служить различные вычислительные задачи с большим объёмом вычислений и количеством операций с памятью, как например, Math::FFT — модуль с реализацией алгоритмов для выполнения быстрого преобразования Фурье.

XS может потребоваться в системном программировании для низкоуровневого взаимодействия с системой. Например, модуль Socket::Netlink — это интерфейс для работы с сокетом семейства PF_NETLINK в Linux.

Часто под XS также понимают вообще весь код модуля написанный не на Perl (XS-часть модуля) или в целом аппаратно-зависимые модули, которые требуют для сборки наличие компилятора языка C/C++ (XS-модули). Хотя это и вполне допустимо, но следует знать, что написать модуль для языка Perl на языке программирования С/С++ можно не только с помощью XS, но и, например, с помощью проекта Swig или вообще без использования XS, а только используя Perl API.

Как было сказано, XS — это макроязык и представляет собой набор макросов. Существует компилятор для языка XS, называемый xsubpp, который раскрывает код макросов в конструкции C-кода, использующих Perl API. Компилятор использует карту типов (typemaps) для преобразования типов аргументов и возвращаемых значений функций к типам используемым в Perl. Таким образом, на выходе компилятора XS мы получаем обычный C-код, который затем компилируется C-компилятором и линкуется в бинарный модуль.

Задача языка XS — упростить написание модулей, заменяя типовой код обвязки короткими макросами. Но сколько бы XS не упрощал жизнь разработчика, он не отменяет необходимости изучения внутреннего строения Perl и API Perl, без чего попытаться объяснить, как писать XS-расширения для Perl невозможно.

Краткий экскурс во внутренний мир Perl

Сам Perl написан на языке C, поэтому работа с API Perl достаточна естественна при программировании расширений с использованием языка C. В первую очередь, необходимо узнать какие основные типы данных определены в Perl, поскольку многие API-функции оперируют со специфическими типами данных, присутствующих только в Perl.

SV — это название типа данных, описывающий скаляр в Perl. Он представляет собой структуру, состоящую из заголовка (с полями: флаг обозначающий тип, количество ссылок на скаляр, ссылка на тело) и объединения, которое собственно и содержит данные, соответствующие типу скаляра. Из структуры видно, что для переменных ведётся подсчёт ссылок и когда он становится равным нулю, это означает, что переменная будет уничтожена (структура освобождена).

Все прочие типы данных AV — массив, HV — хеш, CV — код, GV — глоб, по сути являются той же структурой SV, выбирается только соответствующий тип в объединении. Такая организация данных позволяет свободно конвертировать один тип в другой. Кроме того, в отличии от простых типов C, это позволяет переменным в Perl самим себя описывать — какой тип данных содержится в них.

Непосредственно значением скаляра могут являться типы: IVlong, UVunsigned long, NVdouble, PVchar * и некоторые другие типы. Здесь важно лишь то, что нам никогда не потребуется использовать внутреннее представление скаляра, для получения соответствующих полей в его структуре, поскольку для этих целей в API предусмотрены соответствующие макросы/функции.

Perl API

Документация perlapi содержит список и описание всех публичных (доступных для использования в расширениях) функций. Perl API не является каким-то фиксированным набором функций, как и сам язык Perl, его C API претерпевает изменение: появляются новые функции, исчезают устаревшие. С каждым новым мажорным релизом Perl происходит закрепление нового API и в минорных выпусках стабильных версий API не изменяется. Именно по этой причине при установке новой мажорной версии Perl всегда требуется пересборка всех аппаратно-зависимых модулей. Как правило, поддерживается обратная совместимость, поэтому кроме пересборки никаких других манипуляций может не потребоваться. Но для надёжности существуют механизмы, о которых будет сказано чуть позже, которые позволяют без особых усилий поддерживать возможность сборки и работы ваших расширений на старых версиях Perl.

Перечислять все функции не имеет смысла, но важно обратить внимание на то, какие принципы используются при именовании функций, которые позволяют понять назначение функции и те типы данных, над которыми функция оперирует и какие возвращает.

Например, создание новых переменных:

  • SV* newSV(const STRLEN len) — создаёт новый скаляр и резервирует под него len+1 байт (дополнительный 1 байт зарезервирован под нуль-символ);
  • AV* newAV() — создание нового массива;
  • HV* newHV() — создание нового хеша.

Как видно, функции создания имеют общий вид: суффикс new + возвращаемый тип данных (в верхнем регистре). Тип данных, который получит создаваемый скаляр может конкретизироваться суффиксом (в нижнем регистре), например функции:

SV* newSViv(IV);
SV* newSVuv(UV);
SV* newSVnv(double);

Как видно, создавая скаляр, можно сразу уточнить, какой тип данных там будет содержаться изначально: iv — целое число, uv — неотрицательное целое, nv — число с плавающей запятой и т.п.

Для доступа к значению (с выполнением преобразования), содержащимся в переменной, используются функции вида:

IV    SvIV(SV* sv)
UV    SvUV(SV* sv)
NV    SvNV(SV* sv)
char* SvPV(SV* sv, STRLEN len)
char* SvPV_nolen(SV* sv)

Первая часть названия указывает на тип входных данных (Sv, Av…) далее может быть суффикс описывающий тип возвращаемых данных (IV, NV…) и завершать название функции может имя, поясняющая смысл функции. Ещё примеры функций, которые подходят под данное правило:

  • int AvFILL(AV* av) — получить длину массива;
  • HV* CvSTASH(CV* cv) — получить стеш (хеш-таблица символов переменных пакета).

Также присутствует набор макросов, название которых пишется полностью в нижнем регистре:

  • void av_clear(AV *av) — очистить массив;
  • AV* get_av(const char *name, I32 flags) — получить массив по имени глобальной переменной;
  • CV* get_cvn_flags(const char* name, STRLEN len, I32 flags) — получить заданную Perl-подпрограмму.

Принцип именования также интуитивно понятен: av, cv и подобное — указывают на тип данных, а дополнительные суффиксы и префиксы раскрывают суть выполняемой операции.

Также можно обратить внимание, что в названиях разных функций в названии типа можно увидеть дополняющие символ, например, cvn, pvf, pvs. В этом случае, под n — понимается длина (т.е. в функции есть параметр, задающий длину STRLEN), f — использование в функции параметра формата (sprintf), s — статичной строки (const char*). Например:

  • void sv_catpv(SV *const sv, const char* ptr) — копирует одну строку в конец строки в sv;
  • void sv_catpvn(SV *dsv, const char *sstr, STRLEN len) — копирует указанное число байтов из одной строки в конец другой;
  • void sv_catpvs(SV* sv, const char* s) — тоже самое, что и sv_catpvn только копирует литеральную строку (нуль-терминированную);
  • void sv_catpvf(SV *const sv, const char *const pat, ...) — обрабатывает аргумента также как sprintf(), добавляя отформатированный вывод в sv.

Глобальные переменные Perl API начинаются с префикса PL_, например: PL_sv_undef — это undef, PL_na — длина строки, которая используется по умолчанию в операциях без указания длины строки.

Создание XS-модуля

В дополнение к привычной структуре файлов в составе модуля создаётся файл с названием модуля и с расширением xs, который помещается в корневом каталоге проекта. Простейший xs-файл выглядит следующим образом:

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

MODULE = MyModule    PACKAGE = MyModule

По структуре файл состоит из двух частей. Первая часть до строки, начинающейся с MODULE =, рассматривается как C-код. Вторая часть, начиная с MODULE =, рассматривается как, собственно, xs-код. Парсер xsubpp также способен распознать POD-документацию, как в 1-ой, так и 2-ой части файла и при компиляции удаляет её, что позволяет безопасно снабжать модуль документацией, не опасаясь ошибок компиляции.

Первые три строки — это стандартные подключаемые заголовочные файлы Perl, минимально необходимые для создания расширения. Далее могут располагаться функции, написанные на языке C, реализующие функционал вашего модуля. Директива MODULE во второй части файла задаёт имя файла, в котором содержится Perl-код модуля, из которого будет происходить начальная загрузка (bootstrap) двоичного кода, а директива PACKAGE определяет пространство имён, в котором будут определены последующие функции. В теле xs-директивы MODULE и PACKAGE могут встречаться несколько раз, соответственно меняя имя модуля и пространство имён для следующих после них функций.

Данный пример — пустой, в нём не содержится никаких функций. В соответствии с традицией, создадим hello,world! с использованием XS. Для удобства, воспользуемся утилитой h2xs, которая позволяет автоматически создавать первоначальные шаблоны всех необходимых файлов.

$ h2xs -b 5.8.9 -A -n Hello::World --use-xsloader --skip-ppport
Writing Hello-World/lib/Hello/World.pm
Writing Hello-World/World.xs
Writing Hello-World/Makefile.PL
Writing Hello-World/README
Writing Hello-World/t/Hello-World.t
Writing Hello-World/Changes
Writing Hello-World/MANIFEST

Пояснение по опциям:

  • -b задаёт минимальную версию Perl, на которой планируется работа модуля;
  • -A — в модуле не требуется использование AUTOLOAD;
  • --use-xsloader — вместо DynaLoader используем упрощённый XSLoader;
  • --skip-ppport — пока нам не требуется совместимость со старыми версиями Perl API.

Заглянем в lib/Hello/World.pm:

package Hello::World;

use 5.008009;
use strict;
use warnings;

require Exporter;

our @ISA = qw(Exporter);

our %EXPORT_TAGS = ( 'all' => [ qw(
) ] );

our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

our @EXPORT = qw(
);

our $VERSION = '0.01';

require XSLoader;
XSLoader::load('Hello::World', $VERSION);

1;

Так выглядит модуль Hello::World. Условно тут можно выделить две секции: секция Exporter, в которой задаются какие функции будет экспортировать модуль, и секция XSLoader. При добавлении названий функций в массив @EXPORT будет обеспечиваться экспорт функций в адресное пространство программы, которая будет загружать данный модуль. При добавлении в массив @EXPORT_OK функции не будут экспортироваться автоматически, но их можно будет экспортировать, указав их в параметре к загрузке модуля. Во второй секции, состоящей всего из двух строк и происходит вся магия загрузки XS-составляющей модуля. Далее в этом модуле можно создавать методы написанные на Perl, которые могут совместно работать с методами и функциями определёнными в XS.

Рассмотрим Makefile.PL:

use 5.008009;
use ExtUtils::MakeMaker;
WriteMakefile(
    NAME              => 'Hello::World',
    VERSION_FROM      => 'lib/Hello/World.pm',
    PREREQ_PM         => {},
    ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
      (ABSTRACT_FROM  => 'lib/Hello/World.pm', # retrieve abstract from module
       AUTHOR         => 'Vladimir Lettiev <crux@cpan.org>') : ()),
    LIBS              => [''], # e.g., '-lm'
    DEFINE            => '', # e.g., '-DHAVE_SOMETHING'
    # Insert -I. if you add *.h files later:
    INC               => '', # e.g., '-I/usr/include/other'
    # Un-comment this if you add C files to link with later:
    # OBJECT            => '$(O_FILES)', # link all the C files too
);

Никаких специфических описаний для XS-файла не требуется, если он расположен в стандартном месте для поиска и имеет расширение xs. Параметр INC, позволяет задавать каталоги с заголовочными файлами, LIBS может позволить нам указать, какие библиотеки требуется подключать при линковке, OBJECT позволяет задать какие дополнительно требуется собрать C-файлы в проекте, с которыми будет линковаться код модуля.

В случае использования системы сборки Module::Install, Makefile.PL выглядит ещё проще:

use inc::Module::Install;

perl_version '5.008009';
name         'Hello-World';
all_from     'lib/Hello/World.pm';

WriteAll;

Для задания опций сборки XS-модуля в данной системе сборки, может быть удобен модуль Module::Install::XSUtil.

Рассмотрим файл World.xs. Он содержит минимальное содержимое, дополним его функцией печати сообщения.

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

MODULE = Hello::World       PACKAGE = Hello::World

void
greeting()
    CODE:
        printf("Hello World!\n");

Мы описываем функцию greeting(), которая печатает сообщение Hello World! на экран. Описание XSUB производится в следующем порядке: первая строка содержит тип возвращаемый функцией — void. Вторая строка соответствует прототипу функции (имя и, возможно, аргументы). В последующих строках описываются директивы XS. В данном случае директива CODE (для лучшей читаемости делается отступ) — указывает, что далее идёт С-код функции.

Для сборки выполним команды:

$ perl Makefile.PL
...
$ make
cp lib/Hello/World.pm blib/lib/Hello/World.pm
/usr/bin/perl5.16.3 "-Iinc" /usr/share/perl5/ExtUtils/xsubpp  -typemap
/usr/share/perl5/ExtUtils/typemap  World.xs > World.xsc && mv World.xsc
World.c
Please specify prototyping behavior for World.xs (see perlxs manual)
gcc -c   -D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe
-fstack-protector -I/usr/local/include -D_LARGEFILE_SOURCE
-D_FILE_OFFSET_BITS=64 -pipe -Wall -g -O2   -DVERSION=\"0.01\"
-DXS_VERSION=\"0.01\" -fPIC "-I/usr/lib64/perl5/CORE"   World.c
Running Mkbootstrap for Hello::World ()
chmod 644 World.bs
rm -f blib/arch/auto/Hello/World/World.so
gcc  -shared -pipe -Wall -g -O2 -L/usr/local/lib -fstack-protector World.o
-o blib/arch/auto/Hello/World/World.so  \
    \
-L/usr/lib64/perl5/CORE -lperl -lpthread
chmod 755 blib/arch/auto/Hello/World/World.so
cp World.bs blib/arch/auto/Hello/World/World.bs
chmod 644 blib/arch/auto/Hello/World/World.bs

Рассмотрим вывод make. Первая строка копирует Perl модуль в каталог blib. Затем запускается xsubpp, который компилирует World.xs в файл World.xsc и затем переименовывает его в World.c. Строку с предупреждением о прототипах можно проигнорировать, поскольку xsubpp по умолчанию ожидает, что мы опишем прототип функции (в терминах Perl), но это необязательно. Далее идёт компиляция полученного C-файла с помощью gcc, а затем и линковка. По умолчанию используются те опции, с которыми собирался и линковался сам perl (посмотреть можно по команде в perl -V). Собранная разделяемая библиотека World.so и пустой бутстрап-файл World.bs помещается каталог blib/arch/auto/Hello/World. Бустрап-файл по сути — это Perl-программа, которая выполняется перед загрузкой разделяемого модуля. В обычных ситуациях он не нужен и, как правило, пустой, но на определённых платформах может использоваться для корректной загрузки разделяемой библиотеки.

Проверим как работает модуль:

$ perl -Iblib/lib -Iblib/arch -MHello::World -e 'Hello::World::greeting()'
Hello World!

Параметр -I указывает, где perl будет искать модуль, -M загружает модуль, -e выполняет код, в данном случае вызов greeting(). Модуль работает как и ожидается.

Описанный выше код имеет один недостаток: весь C-код функции находится непосредственно в описании интерфейса функции. Взглянем на альтернативный вариант записи кода:

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

void greeting() {
    printf("Hello World!\n");
}

MODULE = Hello::World       PACKAGE = Hello::World

void
greeting()

Здесь функция greeting() описывается в 1-ой секции XS-файла, как обычная C-функция. А во-второй секции файла описание интерфейса радикально сократилось! Указывается только возвращаемый тип данных, прототип функции, а секция CODE опущена. xsubpp автоматически привяжет к функции модуля одноимённую C-функцию, определённую выше.

Этот пример даёт понять, что, насколько это возможно, необходимо разделять код функций и код-интерфейса для удобства сопровождения такого кода в дальнейшем. Например, код функции greeting() можно вынести и вне файла xs и подключить его код с помощью дополнительного #include заголовочного файла. В дальнейшем можно менять код функций, при этом никак не затрагивая кода интерфейса.

Примеры XSUB

Список и документацию всех директив XS можно прочитать в perlxs. Изучать же применение директив удобнее на реальных примерах XSUB-функций.

Рассмотрим пример XS-кода модуля Readonly::XS (выборочный фрагмент):

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"

MODULE = Readonly::XS       PACKAGE = Readonly::XS

int
is_sv_readonly(sv)
    SV *sv
PROTOTYPE: $
CODE:
    RETVAL = SvREADONLY(sv);
OUTPUT:
    RETVAL

Что нового видно в этом примере? Во-первых, объявляемая функция is_sv_readonly() возвращает результат типа int. После названия функции следует описание аргумента функции, в данном случае sv*, имеющий тип SV. Каждый аргумент функции описывается на отдельной строке. Далее следует директива PROTOTYPE, которая указывает Perl-прототип функции — в данном случае $ — функция принимает скаляр в качестве аргумента. В секции CODE обнаруживаем, что переменной RETVAL присваивается значение одного из макросов Perl API — SvREADONLY(), который возвращает значение флага SVf_READONLY скаляра sv, т.е. происходит проверка является ли скаляр доступным только для чтения.

Переменная RETVAL — это специальная переменная, которая декларируется автоматически и её тип соответствует типу, который должна вернуть эта функция. В Perl-функциях все аргументы и возвращаемые значения помещаются в стек. В XS существует специальный макрос ST(x), где x — номер позиции в стеке, который адресует каждый элемент помещённый в него. Позиция 0, соответствует ST(0). В простых случаях xsubpp помещает значение RETVAL в ST(0), избавляя от необходимости вручную манипулировать со стеком.

Директива OUTPUT подсказывает xsubpp, какие параметры функции должны быть обновлены, когда XSUB будет завершена и определённые значения будут возвращены в вызывающую Perl-функцию. Без этой директивы xsubpp не поймёт, что надо возвращать именно переменную RETVAL.

В том случае, если требуется самостоятельно управлять стеком, можно переписать интерфейс функции следующим образом:

void
is_sv_readonly(sv)
    SV *sv
PROTOTYPE: $
PPCODE:
    PUSHs(sv_2mortal(newSViv(SvREADONLY(sv))));

Вместо секции CODE используется директива PPCODE, что указывает xsubpp, что все операции со стеком выполняются вручную. Развернём выражение в секции PPCODE в порядке выполнения операций:

  • макрос SvREADONLY(sv) возвращает значение соответствующего флага скаляра sv;
  • макрос newSViv создаёт новый скаляр для целочисленного типа, куда помещает значение флага;
  • макрос sv_2mortal помечает созданный скаляр «смертным», т.е. после завершения XSUB, счётчик ссылок скаляра будет уменьшен, что приведёт к освобождению занимаемой им памяти;
  • PUSHs помещает полученный скаляр в стек (который должен иметь достаточно места).

Ручное управление стеком достаточно сложно и в большинстве случае его можно благополучно избегать. Но это единственный способ возвращать больше одного значения из подпрограммы.

Typemap

Возможно, вы обратили внимание в последнем примере, что описание возвращаемого значения функции s_sv_readonly() изменилось с int на void. Помимо управления стеком xsubpp автоматически выполняет конвертацию типов из Perl в C и обратно. В Perl нет типа int, поэтому существует соответствие, по которому тип int преобразуется в скаляр, содержащий значение целого числа. И, соответственно, аргументы C-функции преобразуются из Perl-типа в соответствующий C-тип. Информация о том, как выполнять преобразования, помещается в файл typemap. По умолчанию xsubpp имеет базовый необходимый набор для преобразования всех простых C-типов в Perl и обратно.

Файл typemap состоит из трёх секций: TYPEMAP, INPUT и OUTPUT. Первая секция начинается сразу с начала файла, в ней в каждой строчке перечисляется какой-либо C-тип и его уникальный идентификатор, называемый XS-типом. С ключевого слова INPUT начинается следующая секция, которая описывает как преобразовывать Perl-типы к C-типам. С ключевого слова OUTPUT следует секция, в которой описывается как выполнить обратное преобразование из C-типов к Perl типам.

Например, в стандартном typemap можно увидеть такие преобразования для типа int:

int         T_IV
INPUT
T_IV
    $var = ($type)SvIV($arg)
OUTPUT
T_IV
    sv_setiv($arg, (IV)$var);

Как видно, в первой строке для типа int задаётся XS-тип T_IV. В секции INPUT для XS-типа T_IV указывается преобразование. xsubpp выполняет подстановку переменных в данном выражении: вместо $var подставляется имя переменной, вместо $type подставляется C-тип, $arg заменяется на имя аргумента. В секции OUTPUT описывается обратное преобразование из C-типа в Perl-скаляр.

Вообще стоит отметить, что каждая строка с выражением преобразования в секциях INPUT и OUTPUT — это Perl-строка, которая будет помещена в двойные кавычки, а затем выполнена через eval, для того, чтобы произошла подстановка переменных. По этой причине любые двойные кавычки должны быть экранированы обратным слешем. С другой стороны, это также позволяет производить внедрение исполняемого Perl-кода. Например, реальный пример из файла typemap модуля GTK:

T_GtkPTROBJOrNULL
    $var = SvTRUE($arg)
        ? Cast$type(SvGtkObjectRef($arg, \"" . ($foo=$ntype,$foo=~s/_OrNULL//,$foo). "\"))
        : 0

В данной строке видно, что после неэкранированной двойной кавычки идёт обычный Perl-код, который возвращает название первоначального типа с удалённым фрагментом _OrNULL в конце. Чтобы не возникло синтаксической ошибки, после кода добавляется ещё одна неэкранированная двойная кавычка. Это даёт большие возможности для манёвра, но с другой стороны усложняет понимание, поэтому используйте эту силу с умом и осторожностью.

Возьмём следующий пример XSUB, который возвращает среднее значение двух целых чисел:

double
avg(x,y)
    int x
    int y
CODE:
    RETVAL = (double) (x+y)/2;
OUTPUT:
    RETVAL

Если выполнить make и взглянуть на сгенерированный xsubpp C-код, можно увидеть такой результирующий код функции:

{
    dVAR; dXSARGS;
    if (items != 2)
       croak_xs_usage(cv,  "x, y");
    {
    int x = (int)SvIV(ST(0));
    int y = (int)SvIV(ST(1));
    double  RETVAL;
    dXSTARG;
    RETVAL = (double) (x+y)/2;
    XSprePUSH; PUSHn((double)RETVAL);
    }
    XSRETURN(1);
}

В 6-ой и 7-ой строках переданные через стек скаляры преобразуется к типу int именно так, как и описано в файле typemap в секции INPUT. Преобразование возвращаемого значения RETVAL из типа double в скаляр, описано в секции OUTPUT typemap так:

T_DOUBLE
    sv_setnv($arg, (double)$var);

Но xsubpp в последствии оптимизирует эту запись, заменяя её на макрос PUSHn.

Если в функциях модуля вам часто требуется выполнять преобразования к какому-то специфическому типу, вероятно удобнее вынести такую операцию в typemap. По умолчанию xsubpp ищет файл с названием typemap в текущем каталоге или выше по иерархии директорий проекта.

Файл ppport.h

Отдельно стоит рассказать о файле ppport.h. В примере модуля Readonly::XS можно увидеть включение заголовочного файла ppport.h:

#include "ppport.h"

Этот файл формируется автоматически с помощью модуля Devel::PPPort для того, чтобы дать создаваемому модулю доступ к некоторым API-функциям новых версий Perl для возможности работы с ними на старых версиях Perl. Тем самым улучшая переносимость программ на старые версии интерпретатора. Поддерживаются версии Perl от 5.003 до 5.11.5.

Файл является не простым заголовочным файлом для C. В тоже время это полноценный Perl-скрипт, со встроенной POD-документацией. Скрипт может проанализировать исходные XS-файлы проекта и сообщить какие проблемы в них могут присутствовать в плане совместимости со старыми версиями интерпретатора Perl и даже подготовить патч для исходного кода с необходимыми изменениями. Для этого достаточно запустить команду в корневом каталоге проекта:

$ perl ppport.h
Scanning XS.xs...
Doesn't seem to need ppport.h.
--- XS.xs       2012-08-31 22:24:26.000000000 +0400
+++ XS.xs.patched       2013-04-16 00:01:57.386431309 +0400
@@ -2,7 +2,6 @@
 #include "perl.h"
 #include "XSUB.h"

-#include "ppport.h"


 MODULE = Readonly::XS          PACKAGE = Readonly::XS

Прочитать все возможные опции можно во встроенной POD-документации файла:

$ perldoc ppport.h

Сформировать файл можно командой:

$ perl -MDevel::PPPort -e 'Devel::PPPort::WriteFile();'

Также он может создаваться автоматически утилитой h2xs, если не указывать опцию --skip-ppport.

Переменное число аргументов функции

На примере функции minstr() модуля List::Util можно рассмотреть каким образом в XS-функции можно организовать обработку переменного числа аргументов:

#define SLU_CMP_LARGER   1
#define SLU_CMP_SMALLER -1

void
minstr(...)
PROTOTYPE: @
ALIAS:
    minstr = SLU_CMP_LARGER
    maxstr = SLU_CMP_SMALLER
CODE:
{
    SV *left;
    int index;
    if(!items) {
        XSRETURN_UNDEF;
    }
    left = ST(0);
    for(index = 1 ; index < items ; index++) {
        SV *right = ST(index);
        if(sv_cmp(left, right) == ix)
        left = right;
    }
    ST(0) = left;
    XSRETURN(1);
}

Функция minstr() ищет наименьшую строку в списке. Как видно в 5-ой строке, в прототипе функции указывается троеточие, что даёт понять xsubpp, что функция имеет переменное число аргументов. Для обработки аргументов автоматически создаётся переменная items, которая содержит количество аргументов, переданных функции. В строке 14 происходит проверка, переданы ли параметры или нет. Если параметров нет, то применяется специальный макрос XSRETURN_UNDEF, который приводит к завершению функции и возврату значения константы &PL_sv_undef (undef). Далее в цикле происходит поиск наименьшего значения среди аргументов, обращаясь к каждому из них с помощью макроса ST(index). Результат помещается в начало стека уже известным макросом ST(0). Макрос XSRETURN(1) производит возврат из XSUB и также сообщает сколько возвращаемых значений находится в стеке. Такой способ возврата значений также может использоваться и является альтернативой уже известного RETVAL.

Также, в данной функции используется директива ALIAS. Данная директива позволяет создать несколько альтернативных имён для функции, которые будут видны в Perl: minstr() и maxstr(). В зависимости от того под каким именем вызвана функция в коде инициализируется специальная переменная ix, которой присваивается значение, заданное в секции ALIAS. Т.е. в случае вызова minstr() в ix будет содержаться значение константы SLU_CMP_LARGER, а в случае maxstr() в ix буде уже SLU_CMP_SMALLER. Это позволяет соответствующим образом менять логику функции в зависимости от использованного имени функции.

Создание обвязок (bindings) к C-библиотекам

Обвязки (bindings) к библиотекам позволяют использовать функции реализованные в этих библиотеках внутри Perl-программ. На сегодняшний день существует огромное множество модулей на CPAN, которые реализуют интерфейс к различным библиотекам. Например, XML::LibXML, Gtk3, SDL и т.д.

Рассмотрим создание такой обвязки на примере создания модуля My::Libm, который предоставляет интерфейс к некоторым функциям стандартной математической библиотеки C — libm.

Автоматически создадим проект:

$ h2xs -b 5.8.9 -A -n My::Libm --use-xsloader --skip-ppport

Дополним файл Libm.xs интерфейсами нескольких выбранных нами функций: ceil(), floor() и pow(). Кроме того, подключим заголовочный файл <math.h> откуда будут извлечены реальные прототипы этих функций. В итоге получится такой код:

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include <math.h>

MODULE = My::Libm       PACKAGE = My::Libm

double
ceil(x)
    double  x
    PROTOTYPE: $

double
floor(x)
    double  x
    PROTOTYPE: $

double
pow(x, y)
    double  x
    double  y
    PROTOTYPE: $$

Чтобы собираемый модуль слинковался с библиотекой libm, необходимо в Makefile.PL указать в параметре LIBS ключ для линковки с библиотекой -lm:

use 5.008009;
use ExtUtils::MakeMaker;
WriteMakefile(
    NAME              => 'My::Libm',
    VERSION_FROM      => 'lib/My/Libm.pm',
    PREREQ_PM         => {},
    ($] >= 5.005 ?     ## Add these new keywords supported since 5.00
      (ABSTRACT_FROM  => 'lib/My/Libm.pm',
       AUTHOR         => 'Vladimir Lettiev <crux@cpan.org>') : ()),
    LIBS              => ['-lm'],
    INC               => '',
);

Поскольку заголовочный файл math.h находится в стандартном каталоге (/usr/include) включать этот каталог в INС не нужно.

В файле lib/My/Libm.pm указывается список функций, которые мы собираемся импортировать:

...
our @EXPORT = qw(
    ceil floor pow
);
...

Необходимо убедиться, что есть заголовочный файл math.h (идёт в составе glibc) и попробовать собрать проект:

$ perl Makefile.PL
$ make

Теперь можно проверить (без установки), что модуль и функции работают правильно с помощью простого теста в t/My-Libm.t:

use strict;
use warnings;

use Test::More;
BEGIN { use_ok('My::Libm') };

is floor(1.9), 1, "floor()";
is ceil(1.9), 2, "ceil()";
is pow(2,3), 8, "pow()";

done_testing();

Запустим prove:

$ prove -b -v
t/My-Libm.t ..
ok 1 - use My::Libm;
ok 2 - floor()
ok 3 - ceil()
ok 4 - pow()
1..4
ok
All tests successful.
Files=1, Tests=4,  0 wallclock secs ( 0.05 usr  0.00 sys +  0.05 cusr  0.00 csys =  0.10 CPU)
Result: PASS

Модуль работает. Теперь можно выполнить команду make dist и смело отправлять это творение на CPAN. Конечно, данный пример очень простой, но он демонстрирует общий подход к задаче.

Справочные материалы

Для дальнейшего изучения темы рекомендуется чтение следующей документации:

  • perlguts — введение во внутренности Perl;
  • perlapi — документация по API функциям Perl;
  • perlxs — документация по XS;
  • perlxstut — вводное руководство с хорошими примерами по созданию XS;
  • perlapio — документация по работе с интерфейсом PerlIO в XS.

Google также выдаёт много интересных ссылок, но надо быть внимательным, поскольку очень много материалов уже достаточно сильно устарели. Лучше ориентироваться на последнюю актуальную документацию в составе дистрибутива Perl. Успехов в изучении!

Владимир Леттиев


Pinto — собственный CPAN из коробки | Содержание | Введение в разработку web-приложений на PSGI/Plack. Часть 2.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье