Механика XS: Инструменты

Механика XS

Эта статья про XS. Она объясняет что это, зачем это, как это работает, и как это использовать. Она включает полный, работающий пример XS модуля, а также базовую основу модуля, который вы можете использовать как отправную точку для своего кода. Вкратце, основной целью данной статьи является предоставление необходимых основ и информации для того, чтобы вы создавали свои XS модули.

Данная статья состоит из пяти частей:

Ноябрь Введение мотивация, определения, примеры
Декабрь Архитектура интерпретатор Perl, соглашения вызывов, представление данных
Январь Инструменты h2xs, xsubpp, DynaLoader
Февраль Модули Math::Ackermann, Set::Bit
Март Align::NW глобальное оптимальное выравнивание последовательностей Нидлмана-Вунша

Два месяца назад, мы рассказали о задаче, которая могла бы получить выигрыш от решения на XS. В последнем месяце, мы обсудили архитектуру XS. В этом месяце мы обсудим инструменты, которые используются для написания XS.

Инструменты

На кодерском уровне, XS вращается вокруг двух инструментов: h2xs и xsubpp. Как и другие инструменты, они легки в обращении, если вы понимаете как они были задуманы использоваться. Долото режет по направлению лезвия, а не поперек него. Чтобы понять как инструменты XS планировались использоваться, нам нужно совершить экскурс в истории.

Perl часто использовался для задач, которые до него решались с помощью shell-скриптов, программ на C, и известных инструментов Unix, таких как find(1), awk(1), sed(1), и sort(1). Чтобы помочь программистам портировать существующее ПО на Perl, дистрибутив Perl включает некоторые утилиты-трансляторы, такие как find2perl, a2p (awk to Perl) и s2p (sed to Perl). Результаты работы этих утилит может потребовать небольшого редактирования, но они генерируют достаточно полные и корректные трансляции.

Не существует c2p. Подобную программу было бы сложно написать; между тем, прямая трансляция с C на Perl редко требуется. В большинстве случае, мы хотим вызывать существующий код на C из новых программ на Perl.

h2xs

Интерфейс кода на C – типы данных, прототипы функций – которые обычно описаны в .h файле. Чтобы сгенерировать интерфейсы к коду на C, Perl предоставляет h2xs. h2xs это утилита, которая читает .h файл и генерирует набросок XS-интерфейса к коду на C. Набросок включает в себя

  • директорию с модулем
  • файл Makefile.PL
  • файл .xs
  • файл .pm

Однако, результат работы h2xs не полный, или даже не близкий к завершенности, интерфейс XS. Это просто начало. Ценное начало: он включает некоторый шаблон, который трудно сгенерировать вручную. Но, все же, это только начало.

Также результат работы h2xs не обязательно корректный. Сопряжение Perl с C это сложная задача. h2xs предполагает как ее решить; иногда предполагает неверно.

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

Похожие проблемы связаны и с входными данными для h2xs. h2xs принимает множество опций командной строки. Однако, они не дают полностью завершенной системы, которая используется h2xs, чтобы выполнить то, что вам нужно. Скорее, эти опции образовались с течением времени, каждая из которых предназначалась для решения определенной задачи, в определенных условиях. Многие из них полезны, но не существует такой их комбинации, которая бы заставила h2xs делать Правильную Вещь специально для вас. Вам придется использовать то, что имеется и двигаться дальше.

xsubpp

xsubpp это утилита, которая преобразует XS код в код на C. Иногда XS называют языком программирования, но все же лучше думать о нем как о коллекции макросов; xsubpp это раскрыватель макросов (macros expander). Опять же, макросы XS не претендуют на завершенный и полный язык для сопряжения Perl с C. Они образовывались с течением времени, каждый для решения определенной задачи.

Написание XS не требует глубоких познаний структуры макросов – её просто нет. Скорее, они требуют от вас поисков по perlxs, чтобы найти макрос, который вам нужен, и затем использовать его.

h2xs

Первый шаг в создании любого модуля Perl, это запуск h2xs из командной строки.

Директории

До момента, когда вы точно знаете, что делаете, запуск h2xs это процесс с последовательным уточнением. Вам нужно создать директорию для разработки в этом случае. В примерах далее, мы будем ссылаться на директорию для разработки как

.../development/

Когда вы запускаете h2xs, она создает новую директорию внутри директории для разработки для хранения исходников модуля; мы зовем ее директорией модуля. Директория модуля создается с путем, который согласуется с именем модуля. Например, директория модуля для Align::NW это

.../development/Align/NW/

Существующие библиотеки

h2xs была изначально написана для генерации XS-интерфейсов для существующих библиотек на C. В самом простейшем виде, вы указываете заголовочный файл для библиотеки, а она создает и заполняет директорию модуля. Если заголовочный файл это /usr/include/rpcsvc/rusers.h, мы можем сделать следующее

.../development>h2xs rpcsvc/rusers
Writing Rusers/Rusers.pm
Writing Rusers/Rusers.xs
Writing Rusers/Makefile.PL
Writing Rusers/test.pl
Writing Rusers/Changes
Writing Rusers/MANIFEST

h2xs ищет заголовочный файл в текущей директории и в стандартных include-путях, и жалуется, если не может найти его.

.../development>h2xs foo
Can't find foo.h

[Стандартные include-пути в Unix-подобных системах это директория /usr/include. Однако, от системы к системе эти пути могут отличаться. – прим. пер.]

Наименование модуля

h2xs называет модуль и директорию модуля по заголовочному файлу. Он делает заглавной первую букву в имени, в соответствии с соглашением в Perl об именах модулей, которые начинаются с прописных букв.

Если вам не нравится имя модуля, которое генерирует h2xs, вы можете указать другое, используя флаг -n.

.../development>h2xs -n RPC::Rusers rpcsvc/rusers
Writing RPC/Rusers/Rusers.pm
Writing RPC/Rusers/Rusers.xs
Writing RPC/Rusers/Makefile.PL
Writing RPC/Rusers/test.pl
Writing RPC/Rusers/Changes
Writing RPC/Rusers/MANIFEST

Флаг -n отвечает и за имя модуля, и за имя директории; в данном случае модуль Perl будет RPC::Rusers. Флаг -n не влияет на поиски заголовочного файла: h2xs все равно найдет заголовок в /usr/include/rpcsvc/rusers.h.

Новый код

В следующем месяце мы покажем реализацию XS модуля Align::NW. Align::NW не является интерфейсом к существующей библиотеке, и его заголовочные файлы не расположены в /usr/include. Это новый Perl-модуль, часть которого реализована на C.

Поскольку код на C является частью модуля Align::NW, он обязан находиться в директории модуля. Код на Perl будет в Align/NW/NW.pm, и заманчиво называть исходники на C в соответствии с этим именем

.../development/Align/NW/NW.pm
.../development/Align/NW/NW.c
.../development/Align/NW/NW.h

Однако, это не сработает. Проблема в том, что h2xs собирается создать

.../development/Align/NW/NW.xs

а xsubpp преобразует NW.xs в

.../development/Align/NW/NW.c

что приводит к коллизии с нашим файлом NW.c.

Другой вариант заключается в интеграции кода на C внутри файла .xs

.../development/Align/NW/NW.pm
.../development/Align/NW/NW.xs    # содержит наш код на C
.../development/Align/NW/NW.h

Этот вариант работает, потому что, всё, что находится в .xs файле и не является макросом XS пропускается в неизмененном виде сквозь xsubpp в файл .c. Некоторые XS-модули содержат много кода на C прямо в файле .xs; в конечном счете, разница между кодом на XS и кодом на C становится произвольной.

Однако, я предпочитаю хранить куски моего кода на C в .c файлах, и зарезервировать файл .xs для клей-подпрограмм. Причины на это следующие

  • Я могу скомпилировать файлы .c вместе с main.c и протестировать их в отдельной программе на C.
  • Написание XS сложно; я хочу, чтобы в файле .xs было как можно меньше кода.

Код на C в Align::NW реализует один Perl-метод, названный score. Мы назовем наши исходники на C score.c и score.h. После этого, мы можем создать и заполнить директорию модуля следующим образом

.../development>ls
score.c   score.h
.../development>h2xs -n Align::NW score
Writing Align/NW/NW.pm
Writing Align/NW/NW.xs
Writing Align/NW/Makefile.PL
Writing Align/NW/test.pl
Writing Align/NW/Changes
Writing Align/NW/MANIFEST
.../development>cp score.c score.h Align/NW/

Константы

Многие заголовочные файлы на C определяют (#define) константы, которые появляются в их интерфейсах. h2xs распознает эти константы и делает их доступными в модуле на Perl в виде методов. Например, если файл score.h содержит строки

#define FOO 17
#define BAR 42

то значения 17 и 42 будут доступны коду на Perl в виде значений результатов [выполнения] Align::NW::FOO() и Align::NW::BAR(), соответственно.

h2xs не делает этого через создание методов FOO() и BAR(). Вместо этого, она создает Align::NW::AUTOLOAD() в Align/NW.pm, и функцию на C с именем constant() в Align/NW.xs.

Вызовы к FOO() и BAR() управляются с помощью Align::NW::AUTOLOAD(). AUTOLOAD() вызывает constant(), а constant() возвращает значение определенное (#define’d) в файле .h.

Align::NW::AUTOLOAD() использует прототип функции Perl для константных методов. Чтобы удовлетворить этот прототип, вы должны предопределить все константные методы, которые вы используете подобным образом

sub FOO ();
sub BAR ();

Без констант

Если вам не нужны никакие константы из ваших заголовочных файлов, вы можете запустить h2xs с переключателем -c. Это выключает использование процедуры AUTOLOAD в файле .pm, и функции constant в файле .xs.

Если вам не нужен DynaLoader для чего-либо еще, вы можете запустить h2xs с переключателем -A. -A включает в себя -c, и дополнительно выключает наследование от AutoLoader.

Клей-подпрограммы

Заголовочные файлы на C обычно содержат прототипы функций. h2xs может распознавать прототипы функций и генерировать клей-подпрограммы для них. Она не всегда верно определяет как конвертировать параметры, поэтому нам может потребоваться вручную их отредактировать. Даже в этом случае, это спасает нас от излишнего написания кода. Чтобы cгенерировать клей-подпрограммы автоматически, выполните

.../development>h2xs -n Align::NW -A -O -x -F '-I ../..' score.h

Флаги -n и -A такие как прежде. Если вы ранее запускали h2xs, вам потребуется использовать флаг -O, чтобы насильно заставить переписать существующие файлы Align/NW/*. Флаг -x указывает h2xs генерировать клей-подпрограммы, основываясь на прототипах функций в score.h.

Флаг -x использует модуль C::Scan для нахождения заголовочных файлов. Вам понадобится установить данный модуль в вашу систему, чтобы использовать флаг -x.

Мы запускаем h2xs из директории разработки, но C::Scan переходит в директорию модуля, когда он ищет заголовочные файлы. Флаг -F указывает на дополнительные переключатели для C::Scan для передачи их препроцессору C. Мы передаем переключатель -I ../.., чтобы сообщить препроцессору искать заголовочные файлы на два уровня выше, в директории разработки. Это позволит ему найти score.h.

Анатомия XS

Теперь, когда мы знаем как запускать h2xs, мы можем посмотреть внутрь файла .xs, чтобы узнать из чего он состоит.

Термины

Вот некоторые термины, которые мы будем использовать в обсуждении далее:

  • целевая подпрограмма (target routine): подпрограмма (routine) на C, которую мы хотим вызвать из Perl; причина, из-за которой мы делаем все это
  • подпрограмма Perl (Perl routine): подпрограмма Perl, которая вызывает целевую подпрограмму
  • xsub: подпрограмма на C, которая установлена как xsub для подпрограммы Perl
  • подпрограмма XS (XS routine): описание xsub, которое мы пишем в файле .xs. xsubpp транслирует подпрограммы XS в xsub’ы за нас.
  • XS: язык, на котором мы пишем подпрограммы XS. Также зовется XS-код. XS-код это комбинация директив XS и кода на C.
  • клей-подпрограмма: в общем случае относится к подпрограмме XS и xsub. Акцентирует роль xsub в соединение подпрограммы Perl с целевой подпрограммой.
  • директивы XS: элементы языка XS, документированы в perlxs
  • макрос XS: макрос на языке C, который xsubpp извлекает, когда генерирует xsub.

Гипотенуза

Вот целевая подпрограмма в файле под именем hypotenuse.c

double hypotenuse(double x, double y)
{
    return sqrt(x*x + y*y);
}

и ее прототип в файле hypotenuse.h

double hypotenuse(double x, double y);

Подпрограмма Perl это Geometry::hypotenuse(). Мы хотим, чтобы вызовы Geometry::hypotenuse() вызывали целевую подпрограмму.

h2xs

Мы запускаем h2xs

.../development>h2xs -n Geometry -A hypotenuse.h

и она генерирует Geometry/Geometry.xs как

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

#include <hypotenuse.h>

MODULE = Geometry               PACKAGE = Geometry

Я опустил флаг -x. Это пример, где она [h2xs] делает неверное предположение о преобразовании параметров. Довольно просто это исправить, но вы должны понять typemap (карта типов), которую мы еще не обсуждали.

Geometry.xs

Давайте рассмотрим Geometry.xs подробно.

Perl C API

Первые три вложения (#include) дают нашему XS-коду доступ к Perl C API. С помощью них вы можете найти все точки входа и типы данных, упомянутых в perlguts.

hypotenuse.h

Следующее вложение (#include) дает нашему XS-коду доступ к нашему заголовочному файлу на C: hypotenuse.h. h2xs ищет заголовочные файлы в текущей рабочей директории и в стандартном include-пути; однако, директива #include, которая извлекает его использует угольные скобки вместо кавычек. Угольные скобки инструктируют компилятор C искать заголовочные файлы только в стандартном include-пути. Мы собираемся поместить hypotenuse.h в директорию модуля, которая не является стандартным include-путем, поэтому мы должны отредактировать директиву #include, используя кавычки.

#include "hypotenuse.h"

После этого компилятор C найдет hypotenuse.h в директории модуля.

MODULE и PACKAGE

MODULE и PACKAGE являются директивами XS. Они определяют модуль и пакет для наших xsub’ов. Это легко понять, если мы запомним нижележащие определения

  • module: файл содержащий код Perl
  • package: пространство имен (namespace) Perl

Мы записываем xsub’ы в XS; xsubpp транслирует XS-код в чистый C; компилятор C компилирует код C в библиотеки для линковки; makefile устанавливает эти библиотеки, а DynaLoader загружает эти библиотеки во время выполнения. Для того, чтобы загрузить библиотеку, DynaLoader должен знать две вещи:

  • файл, который содержит библиотеку
  • пространство имен Perl, для которого нужно установить xsub’ы

Директива MODULE указывает на файл библиотеки, а директива PACKAGE указывает на её пространство имен.

Директива PACKAGE даёт название пакету Perl, такое как Geometry или Align::NW. xsubpp, зная это, генерирует код для установки xsub’ов в указанный пакет.

Директива MODULE не указывает на настоящее имя файла: она дает имя пакету Perl, также как директива PACKAGE. xsubpp сопоставляет это имя пакета с именем файла, такое как Geometry.so или Align/NW.so. makefile устанавливает этот файл по соответствующему пути в библотеки Perl, и DynaLoader находит его там.

Файл .xs может содержать несколько директив MODULE и PACKAGE. Директивы MODULE и PACKAGE всегда должны объявляться вместе, как показано выше. Все директивы MODULE в файле .xs должны именовать тот же самый модуль. Директива PACKAGE может именовать разные пакеты, если необходимо установить различные xsub’ы в различные пакеты Perl; это почти аналогично использованию повторяющихся выражений package в обычном коде на Perl.

Теперь мы начнем добавлять вещи в Geometry.xs.

PROTOTYPES

Как и обычные подпрограммы в Perl [функции, процедуры – прим. пер.], так и xsub’ы могут иметь прототипы. Директива PROTOTYPES указывает xsubpp нужно или не нужно устанавливать наши xsub’ы с прототипами.

Напишите

PROTOTYPES: ENABLE

или

PROTOTYPES: DISABLE

для включения или выключения прототипов.

Директива PROTOTYPES указывается после директивы MODULE. Если вы укажите ее выше директивы MODULE, она будет передана компилятору C, что вызовет ошибки компиляции.

h2xs предшествует прототипам в Perl, и не раскрывает директиву PROTOTYPES для вас. xsubpp жалуется, если вы забыли ее определить. Я обычно включаю прототипы, до тех пока у меня нет причин не делать этого.

[Как было сказано ранее, h2xs создает лишь заготовку новоиспеченного модуля на XS. Она не знает как будут использоваться прототипы функций (xsub). На этапе компиляции, xsubpp прочитает файл .xs, который был сгенерирован утилитой h2xs, чтобы создать конечный .c файл. Начиная с этого момента играет роль включены прототипы или нет; в результирующем коде на C будут или не будут присутствовать соответствующие проверки прототипа функции. – прим. пер.]

Подпрограммы XS

После директивы PROTOTYPES следуют подпрограммы XS.

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

  • тип возвращаемого значения целевой подпрограммы
  • имя целевой подпрограммы
  • имя и тип каждого параметра для целевой подпрограммы

Вот подпрограмма XS, которая описывает нашу целевую подпрограмму

double
hypotenuse(x, y)
        double  x
        double  y

Переводы строк играют роль; отступы – нет. Однако, это стиль, который используется h2xs, и я обычно следую ему.

Имя подпрограммы XS hypotenuse. xsubpp наследует имя подпрограммы Perl из имени подпрограммы XS. В данном примере, xsubpp также определяет имя целевой подпрограммы из имени подпрограммы XS. В дальнейшем, мы увидим примеры, где целевая подпрограмма имеет имя отличное от имени подпрограммы XS.

make

Теперь переместимся в директорию модуля. Отредактируйте Makefile.PL, добавив пару имя/значение

'OBJECT'    => 'Geometry.o hypotenuse.o'

в список аргументов WriteMakefile. После этого выполните

.../development/Geometry>cp ../hypotenuse.c .
.../development/Geometry>cp ../hypotenuse.h .
.../development/Geometry>perl Makefile.PL
.../development/Geometry>make

Makefile.PL запишет makefile. makefile запускает

  • xsubpp, чтобы транслировать Geometry.xs в Geometry.c
  • компилятор C, чтобы скомпилировать Geometry.c в Geometry.o
  • линковщик, чтобы слинковать Geometry.o в библиотеку для линковки

Geometry.c

Вот Geometry.c, немножко отредактированный для ясности

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

#include "hypotenuse.h"

XS(XS_Geometry_hypotenuse)
{
    dXSARGS;
    if (items != 2)
        croak("Usage: Geometry::hypotenuse(x, y)");
    {
        double  x = (double)SvNV(ST(0));
        double  y = (double)SvNV(ST(1));
        double  RETVAL;

        RETVAL = hypotenuse(x, y);
        ST(0) = sv_newmortal();
        sv_setnv(ST(0), (double)RETVAL);
    }
    XSRETURN(1);
}

XS(boot_Geometry)
{
    dXSARGS;
    char* file = __FILE__;

    XS_VERSION_BOOTCHECK ;

    newXSproto("Geometry::hypotenuse", XS_Geometry_hypotenuse, file, "$$");
    XSRETURN_YES;
}

Geometry.с это типичный исходный файл на C, пригодный для компиляции. Он выглядит странным, потому что он написан с макросами XS. Давайте расшифруем макросы и посмотрим как они работают.

Вложения

Вложения (#include) передаются неизменными из файла .xs. Они необходимы компилятору C.

XSub

XS_Geometry_hypotenuse это настоящая xsub, сгенерированная xsubpp. Имя xsub сформировано из

  • токена XS
  • имени, указанного в директиве PACKAGE
  • имени подпрограммы XS

Макрос XS() объявляет XS_Geometry_hypotenuse с типом возращаемого значения и параметрами, которые Perl ожидает увидеть у xsub. Они не являются параметрами для hypotenuse(); мы получим их из стека Perl.

dXSARGS это другой макрос XS; он объявляет некоторые локальные переменные, которые нужны xsub.

Одна из локальных [переменных] объявленная dXSARGS это items; она указывает на количество аргументов, которые мы передаем xsub на стеке Perl. Как видно из объявления, hypotenuse() требует 2 аргумента; xsub генерирует сообщение об использовании [обратите внимание на croak() – прим. пер.], если hypotenuse() вызвана из Perl с неверным количеством аргументов.

Следующим идет код, который извлекает аргументы из стека Perl

double  x = (double)SvNV(ST(0));
double  y = (double)SvNV(ST(1));

ST() это макрос XS, который получает доступ к аргументу на стеке Perl: ST(0) это первый аргумент, ST(1) это второй, и так далее.

Perl передает параметры по ссылке, поэтому вещи на стеке это указатели к нижележащим скалярам. SvNV это точка входа в Perl C API. Она принимает указатель на скаляр и возвращает значение этого скаляра в виде числа. xsubpp добавляет приведение типа (double), чтобы успокоить компилятор C, и присваивает это значение к локальной переменной: x для ST(0) и y для ST(1).

xsubpp также объявляет локальную переменную для хранения значения результата выполнения подпрограммы.

double  RETVAL;

Эта переменная всегда называется RETVAL, но она объявлена с типом, который возвращает подпрограмма.

Определившись с x, y, и RETVAL, xsubpp может сгенерировать вызов для целевой подпрограммы. xsubpp использует имя подпрограммы XS такое же как имя целевой подпрограммы.

RETVAL = hypotenuse(x, y);

Здесь нет волшебства. Это очень типичный вызов подпрограммы C. Не привыкайте к нему.

Следующие две строки возвращают значение в Perl.

ST(0) = sv_newmortal();
sv_setnv(ST(0), (double)RETVAL);

Возвращаемые значения идут в стек Perl, начиная с ST(0). sv_newmortal и sv_setnv являются точками входа в Perl C API. sv_newmortal создает новое скалярное значение. Как и любой скаляр, оно имеет начальное значение равное undef. sv_setnv устанавливает значение скаляра равным значению, которое было возвращено из hypotenuse.

Наконец, макрос XSRETURN(1) указывает интерпретатору сколько значений мы возвращаем в стек Perl: в данном случае, один.

boot

boot_Geometry это подпрограмма, которую DynaLoader вызывает, чтобы установить xsub в модуле Geometry. Имя подпрограммы сформировано из

  • токена boot
  • имени, взятому из директивы MODULE

Чтобы установить xsub, boot_Geometry вызывает

newXSproto("Geometry::hypotenuse", XS_Geometry_hypotenuse, file, "$$");

newXSproto это точка входа в Perl C API. Ее аргументы это

  • имя подпрограммы Perl
  • указатель на подпрограмму C
  • имя исходного файла C
  • прототип подпрограммы Perl

newXSproto устанавливает подпрограмму C XS_Geometry_hypotenuse в качестве xsub для подпрограммы Perl Geometry::hypotenuse. Он содержит прототип, потому что мы указали в файле .xs PROTOTYPES: ENABLE. Имя исходного файла указывается для того, чтобы Perl мог сообщить его в сообщениях об ошибках.

Имя подпрограммы Perl сконструированно из

  • имени, заданному в директиве PACKAGE
  • имени подпрограммы XS

xsubpp генерирует только одну подпрограмму boot на модуль. Подпрограмма boot делает один вызов newXSproto для каждого xsub в модуле.

Тест

Чтобы протестировать нашу работу, отредактируйте Geometry/test.pl и добавьте следующую строчку

print Geometry::hypotenuse(3, 4), "\n";

в конец. Затем выполните

.../development/Geometry>make test

Результат должен быть следующим

1..1
ok 1
5

r2p

hypotenuse() имеет простую сигнатуру; давая эту сигнатуру, xsubpp может сгенерировать код для ее вызова. В более сложных случаях, нам придется написать немного кода самим. XS предоставляет директивы, которые дают нам возможность прямо описывать код на C, не полагаясь на xsubpp. В примерах ниже, мы будем использовать их, чтобы обеспечить себе больше контроля над xsubpp.

Вот другая целевая подпрограмма, в файле с именем r2p.c

double r2p(double x, double y, double *theta)
{
    *theta = atan2(y, x);
    return sqrt(x*x + y*y);
}

и её прототип в файле r2p.h

double r2p(double x, double y, double *theta);

r2p конвертирует прямоугольные координаты в полярные, таким образом она возвращает 2 значения: величину и угол. Величина это значение, возвращаемое подпрограммой; угол возвращается через третий параметр, переданный по адресу. Если мы напишем подпрограмму XS как

double
r2p(x, y, theta)
        double  x
        double  y
        double  theta

то xsubpp будет считать theta как входной параметр. Он инициализирует его из стека Perl, и не вернет значения в него. Вместо этого, мы напишем подпрограмму XS как

double
r2p(x, y, theta)
        double  x
        double  y
        double  theta = NO_INIT
        CODE:
                RETVAL = r2p(x, y, &theta);
        OUTPUT:
        RETVAL
        theta

Директива NO_INIT выключает инициализацию из стека Perl.

Директива CODE указывает xsubpp, что мы предоставим код на C, чтобы вызвать целевую подпрограмму. xsubpp все еще объявляет RETVAL для нас, но мы должны присвоить ему значение. Вызов r2p это

RETVAL = r2p(x, y, &theta);

Это не директива XS; это выражение на C, и будет передано напрямую компилятору C. Следовательно, оно оканчивается точкой с запятой.

Директива OUTPUT перечисляет значения, которые будут скопированы обратно в скаляры Perl. Последовательность, в которой мы их перечисляем не имеет роли; xsubpp знает куда каждое значение предназначено. Мы должны вернуть оба значения: RETVAL и theta.

Вот xsub, которую xsubpp генерирует для данной подпрограммы XS.

XS(XS_Geometry_r2p)
{
    dXSARGS;
    if (items != 3)
        croak("Usage: Geometry::r2p(x, y, theta)");
    {
        double  x = (double)SvNV(ST(0));
        double  y = (double)SvNV(ST(1));
        double  theta;
        double  RETVAL;
                RETVAL = r2p(x, y, &theta);
        sv_setnv(ST(2), (double)theta);
        SvSETMAGIC(ST(2));
        ST(0) = sv_newmortal();
        sv_setnv(ST(0), (double)RETVAL);
    }
    XSRETURN(1);
}

Она выглядит очень похожей на xsub для hypotenuse. xsubpp объявляет theta для нас, таким образом, чтобы мы могли передать её адрес в r2p. Она также генерирует следующие строки, чтобы вернуть theta в Perl

sv_setnv(ST(2), (double)theta);
SvSETMAGIC(ST(2));

Он знает, что нужно присвоить theta для ST(2), потому что мы объявили theta как третий параметр для r2p. SvSETMAGIC проверяет, чтобы скаляр для ST(2) был создан, если необходимо. Он должен быть создан, например, если его значение равно несуществующему массиву или хэшу.

Тест

Мы можем добавить r2p в модуль Geometry. Скопируйте r2p.c и r2p.h в директорию модуля и добавьте r2p.o в список OBJECT в файле Makefile.PL. Добавьте строчку

#include "r2p.h"

в код XS, показанный выше для Geometry.xs. Добавьте

my $theta;
my $r = Geometry::r2p(3, 4, $theta);
print "$r, $theta\n";

в test.pl. Теперь выполните

.../development/Geometry>perl Makefile.PL
.../development/Geometry>make
.../development/Geometry>make test

Результат должен быть следующим

1..1
ok 1
5
5, 0.927295218001612

[Внимательный читатель заметит, что в коде на Perl $theta передается по значению, хотя через неё же мы получаем результат. Это работает так, потому что используются прототипы функций; по аналогии с типичными функциями Perl, такими как sub fnc(\$) { ... }, где \$ как раз и говорит Perl, что передается ссылка на скаляр, а не его значение. – прим. пер. ]

r2p_list

В примерах выше, подпрограмма Perl и целевая подпрограмма имеют по существу одинаковые имя и сигнатуру. Однако, это необязательно. Например, в Perl, было бы более правдоподобно вызывать подпрограмму такую как r2p следующим образом

($r, $theta) = r2p_list($x, $y);

Мы можем получить подобную последовательность вызова в такой подпрограмме XS

void
r2p_list(x, y)
        double  x
        double  y
        PREINIT:
        double  r;
        double  theta;
        PPCODE:
                r = r2p(x, y, &theta);
        EXTEND(SP, 2);
        PUSHs(sv_2mortal(newSVnv(r    )));
        PUSHs(sv_2mortal(newSVnv(theta)));

Здесь имеется несколько различий между этой подпрограммой XS и той, что мы написали выше для r2p.

Имя подпрограммы XS не совпадает с именем целевой подпрограммы, xsubpp не нужно имя целевой подпрограммы, потому что мы предоставляем код для вызова целевой подпрограммы. xsubpp все еще использует имя подпрограммы XS, чтобы дать имя подпрограмме Perl.

Тип возращаемого результата r2p_list равен void. Это не означает, что r2p_list ничего не возвращает. Скорее, это сообщает xsubpp, что мы предоставляем код с возвращаемыми значениями в Perl. Следовательно, xsubpp не объявляет для нас RETVAL.

Директива PREINIT предоставляет нам место для объявления переменных C. Без неё, xsubpp мог создать исполняемый код C прежде наших объявлений переменных, что является синтаксической ошибкой в C. Мы объявляем две переменные C: r и theta.

Директива PPCODE похожа на директиву CODE. Она сообщает xsubpp, что мы предоставим и код на C для вызова r2p и код PP для значений возврата в Perl. Код PP это Perl Pseudocode; это внутренний язык, который интерпретатор Perl выполняет.

Кодом на C для вызова r2p является

r = r2p(x, y, &theta);

а PP-код для возвращаемых значений в Perl это

EXTEND(SP, 2);
PUSHs(sv_2mortal(newSVnv(r    )));
PUSHs(sv_2mortal(newSVnv(theta)));

Макрос EXTEND выделяет место на стеке для 2 скаляров, а макросы PUSHs кладут эти скаляры на стек. Код PP идет прямо к компилятору C, поэтому строчки заканчиваются на точку с запятой, как и любые строчки кода на C.

xsub, которую генерирует xsubpp выглядит так

XS(XS_Geometry_r2p_list)
{
    dXSARGS;
    if (items != 2)
        croak("Usage: Geometry::r2p_list(x, y)");
    SP -= items;
    {
        double  x = (double)SvNV(ST(0));
        double  y = (double)SvNV(ST(1));
        double  r;
        double  theta;
                r = r2p(x, y, &theta);
        EXTEND(SP, 2);
        PUSHs(sv_2mortal(newSVnv(r    )));
        PUSHs(sv_2mortal(newSVnv(theta)));
        PUTBACK;
        return;
    }
}

xsubpp раскрывает код, чтобы извлечь наши аргументы из стека Perl, как и ранее. Он передает наши объявления переменных на C и наш вызов подпрограммы без изменений. Кстати, он также передает наш PP-код.

Самое больше различие между XS_Geometry_r2p и XS_Geometry_r2p_list заключается в управлении стеком. XS_Geometry_r2p использует вызов макроса XSRETURN(1), чтобы вернуть одно значение на стеке. XS_Geometry_r2p_list уменьшает SP на количество входных параметров, и затем использует макрос PUTBACK перед возвратом.

Я не совсем точно понимаю, что именно делает каждый макрос со стеком. Я написал клей-подпрограммы, показанные выше, следуя примерам в perlxs. Макросы определены в /usr/local/lib/perl5/*version*/*architecture*/CORE/*.h, но когда я попытался прочесть их, я быстро потерялся в лабиринте определений #define, #ifdef, typedef, и внутренних структурах данных Perl.

Не имея понимания принципов работы стека Perl, вы на самом деле не сможете написать PP-код: все что вы можете, это следовать рабочим примерам, как и я. Примеры в perlxs кажутся адекватными для большинства xsub’ов.

r2p_open

Мы увидели выше, что целевая подпрограмма не нуждается той же последовательности как у подпрограммы Perl. Фактически, нам вообще не нужна целевая подпрограмма. Как только у нас имеется директива CODE или PPCODE в нашем XS-коде, мы может запихнуть любой код на C внутрь XS подпрограммы.

В r2p_open, мы раскрываем подпрограмму r2p, и считаем r и theta в открытом коде.

void
r2p_open(x, y)
        double  x
        double  y
        PREINIT:
        double  r;
        double  theta;
        PPCODE:
                r     = sqrt(x*x + y*y);
                theta = atan2(y, x);
        EXTEND(SP, 2);
        PUSHs(sv_2mortal(newSVnv(r    )));
        PUSHs(sv_2mortal(newSVnv(theta)));

Вот xsub, который раскрывает xsubpp. Он выглядит таким же как для r2p_list, за исключением тех строчек, которые считают r и theta.

XS(XS_Geometry_r2p_open)
{
    dXSARGS;
    if (items != 2)
        croak("Usage: Geometry::r2p_open(x, y)");
    SP -= items;
    {
        double  x = (double)SvNV(ST(0));
        double  y = (double)SvNV(ST(1));
        double  r;
        double  theta;
                r     = sqrt(x*x + y*y);
                theta = atan2(y, x);
        EXTEND(SP, 2);
        PUSHs(sv_2mortal(newSVnv(r    )));
        PUSHs(sv_2mortal(newSVnv(theta)));
        PUTBACK;
        return;
    }
}

Добавьте эти строчки в Geometry/test.pl, чтобы протестировать наши новые xsub’ы.

($r, $theta) = Geometry::r2p_list(3, 4);
print "$r, $theta\n";

($r, $theta) = Geometry::r2p_open(3, 4);
print "$r, $theta\n";

Когда мы запустим

.../development>make test

мы получим

1..1
ok 1
5
5, 0.927295218001612
5, 0.927295218001612
5, 0.927295218001612

Для справки, вот финальные версии

perlxs

Эти примеры иллюстрируют только базовое программирование XS, используя всего несколько директив. perlxs описывает более 20 XS директив. Он включает примеры и фрагменты кода, демонстрирующие как их использовать. Вы должны прочитать его, чтобы понимать широту возможностей, которые предоставляет XS.

typemap: карта типов

В примерах выше, мы увидели как писать подпрограммы XS, и как использовать директивы XS, чтобы контролировать C-код, сгенерированный xsubpp. Теперь, мы взглянем на то, как xsubpp конвертирует данные между представлениями на Perl и C.

Задача вот в чем. Когда интерпретатор Perl вызывает подпрограмму, он кладет список скаляров на стек Perl. На входе, xsub должна извлечь эти скаляры из стека и сконвертировать их в данные C. На выходе, xsub должна сконвертировать данные C в скаляры Perl и положить эти скаляры обратно на стек. xsubpp необходимо сгенерировать код на C, чтобы выполнить эти преобразования.

Преобразования между типами данных на Perl и C управляется через макросы и подпрограммы в Perl C API, но необходимые операции варьируются, в зависимости от типов данных C и направления преобразования. Посмотрите:

 Тип данных C |            Вход               |             Выход
------------- | ----------------------------- | ----------------------------------
 int    n     | n   = (int   ) SvIV(ST(0))    | sv_setiv(     ST(0), (IV    )n  )
 double x     | x   = (double) SvNV(ST(0))    | sv_setnv(     ST(0), (double)x  )
 char  *psz   | psz = (char *) SvPV(ST(0),na) | sv_setpv((SV*)ST(0),         psz)

Мы могли бы представить себе большое выражение с переключателем внутри xsubpp, чтобы выбрать нужный фрагмент кода для каждого типа данных C, но такое решение было бы неуклюжим и негибким. Было бы лучше запихнуть фрагменты кода в таблицу, как показано выше.

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

 Тип данных C     |             Вход                |        Выход
----------------- | ------------------------------- | ----------------------
 int            n | n = (int           )SvIV(ST(0)) | sv_setiv(ST(0), (IV)n)
 unsigned       n | n = (unsigned      )SvIV(ST(0)) | sv_setiv(ST(0), (IV)n)
 unsigned int   n | n = (unsigned int  )SvIV(ST(0)) | sv_setiv(ST(0), (IV)n)
 long           n | n = (long          )SvIV(ST(0)) | sv_setiv(ST(0), (IV)n)
 unsigned long  n | n = (unsigned long )SvIV(ST(0)) | sv_setiv(ST(0), (IV)n)
 short          n | n = (short         )SvIV(ST(0)) | sv_setiv(ST(0), (IV)n)
 unsigned short n | n = (unsigned short)SvIV(ST(0)) | sv_setiv(ST(0), (IV)n)

В виду этого, xsubpp использует двух-уровневое соотношение. Во-первых, он соотностносит типы данных C с типами XS, вот так

 Тип данных C |  Тип XS
------------- | --------
 int          | T_IV
 unsigned     | T_IV
 char         | T_CHAR
 char *       | T_PV

Потом он соотносит типы XS с фрагментами кода, в двух таблицах: одна на входе

 Тип XS  |    Фрагмент кода на входе
-------- | -----------------------------
 T_IV    | $var = ($ntype)SvIV($arg)
 T_CHAR  | $var = (char)*SvPV($arg,na)
 T_PV    | $var = ($ntype)SvPV($arg,na)

и другая на выходе

 Тип XS  |    Фрагмент кода на выходе
-------- | -----------------------------------
 T_IV    | sv_setiv ($arg, (IV)$var);
 T_CHAR  | sv_setpvn($arg, (char *)&$var, 1);
 T_PV    | sv_setpv ((SV*)$arg, $var);

Эти таблицы составляют typemap (карта типов).

Типы XS играют роль только для xsubpp, и появляются только в typemap. Они не появляются в коде Perl, коде XS, или C-коде.

$var, $ntype, и $arg

Фрагменты кода в карте типов не чистый C: в их текстах содержатся переменные Perl. Переменные следующие

  • $var: Имя переменной C
  • $ntype: Тип $var
  • arg: Код для доступа к скаляру Perl

xsubpp это программа на Perl. Когда ей нужно сконвертировать аргумент из Perl в C, она объявляет $var, $ntype, и $arg, находит соответствующий фрагмент кода из карты типов, и запускает этот фрагмент с помощью eval для подстановки значений переменных Perl.

Например, рассмотрим данную подпрограмму XS

int
max(a, b)
  int a
  int b

Для генерации кода, чтобы сконвертировать первый параметр из Perl в C, xsubpp устанавливает переменные Perl следующим образом

 Переменная  |  Значение
------------ | ----------
 $var        | a
 $ntype      | int
 $arg        | ST(0)

Потом, она использует eval для фрагмента

$var = ($ntype)SvIV($arg)

чтобы получить C-код

a = (int)SvIV(ST(0))

Важно понять как работают эти переменные, потому что иногда вам понадобится организовать их таким образом, чтобы заставить xsubpp делать то, что вам нужно. Мы рассмотрим пример этого в следующем месяце, когда мы напишем код для модуля Align::NW.

Файлы карт типов

Три таблицы, которые составляют карту типов называются как TYPEMAP, INPUT, и OUTPUT, соответственно. Все три таблицы могут располагаться внутри одного файла, где каждая из таблиц начинается со своего имени. Вот пример, иллюстрирующий формат файла

# A typemap file (файл карты типов)

TYPEMAP
int           T_IV
SV *          T_SV

INPUT
T_SV
  $var = $arg
T_IV
  $var = ($ntype)SvIV($arg)
  
OUTPUT
T_SV
  $arg = $var;
T_IV
  sv_setiv($arg, (IV)$var);   

Первый заголовок TYPEMAP может быть опущен.

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

Perl содержит карту типов по умолчанию в файле

/usr/local/lib/perl5/version/ExtUtils/typemap

Модули XS могут содержать локальную версию файла карты типов в директории модуля. Если модуль объявляет структуры или иные типы данных C, он может соотнести их с типами XS в секции TYPEMAP. Локальные карты типов редко нуждаются в секциях INPUT и OUTPUT; карта типов по-умолчанию почти всегда содержит необходимые фрагменты кода.

В следующем месяце, мы будем использовать эти инструменты, чтобы написать Align::NW в виде реализации на XS.


Заметки

согласуется

Согласование похожее, но не идентичное, тому, что используется в директории установки. NW.pm разрабатывается в

.../development/Align/NW/NW.pm

но устанавливается в

/usr/local/lib/perl5/site_perl/version/Align/NW.pm

Лишняя /NW/ в директории разработки необходима для того, чтобы мы могли получить, например, следующее

.../development/Align/NW/Makefile.PL
.../development/Align/SW/Makefile.PL

без каких-либо проблем.

модуля

Беря начало своих корней в Perl 4, даже после перехода к Perl 5, в документации (POD) h2xs постоянно используется термин extension (расширение) вместо module (модуля).

выключает

Из-за бага в h2xs вы все еще можете обнаружить строчку

require AutoLoader;

в своем .pm файле. Вы можете удалить её, если хотите.

sv_newmortal

Слово mortal (смертный) ссылается на оптимизацию в текущей реализации Perl. Все объекты данных в Perl подвергаются очистке мусора (garbage collection). В большинстве случаев, это выполняется путем подсчета ссылок. Если объект будет существовать в течении короткого промежутка времени, – например, на стеке – обслуживание подсчета ссылок может оказаться излишней нагрузкой. Чтобы это предотвратить, такие объекты можно создать как смертные. Смертные объекты не имеют подсчета ссылок, но они безоговорочно удаляются, когда в них нет больше необходимости – обычно в конце утверждения, в котором они были созданы. Сложность определения, когда смертный [объект] более не нужен – источник бесконечных споров разработчиков интерпретатора Perl.

протестировать нашу работу

Для “боевого” кода, обычно предпочитают помещать эти тестовые программы в файлы t/*.t. Подробности смотрите в Module Mechanics.

SvSETMAGIC

На появление слов подобных magic в API часто имеются свои причины. Сравните с использованием psychic в Страуструповом обсуждении ключевого слова typename из The C++ Programming Language.