Перейти к содержанию

Некоторые сведения о Perl 5/Perl XS

Материал из Викиучебника — открытых книг для открытого мира
← Работа со строками и регулярные выражения Глава Приложения →
Perl XS


Perl XS (акроним от eXternal Subroutineрусск. внешняя подпрограмма) — это вспомогательный макроязык, предназначенный для согласования вызовов Perl API с моделью вызова функций на языке Си, что позволяет вам внедрять в программы, написанные на языке Perl, вызовы программ, написанных на C/C++ (или совместимые с ними на этапе компоновки). Далее для краткости мы будем говорить просто XS.

Основной целью XS является написание расширений интерпретатора с различной мотивацией:

  • обвязки функций C/C++ библиотек (англ. bindings) обычно с целью использования их функционала за интерпретатором Perl;
  • когда из программы Perl требуется выполнить платформозависимый вызов (драйверы);
  • когда возможностей языка Perl не хватает, чтобы реализовать максимально быстрый алгоритм: обычно он реализуется на более подходящем языке, а его вызов транслируется из Perl-программы.

Для написания расширений требуется знать (или хотя бы понимать) язык Си. Если это требование не выполняется, то вы не сможете до конца понять следующий материал.

Для написания расширения Perl язык XS в общем то не нужен, так как вы можете компоновать свою программу с Perl API напрямую, однако, в этой затее кроется много недостатков:

  • Perl API довольно большой, а желательно его понимать полностью (см. Perlapi);
  • Perl API — это «живой организм»: от релиза к релизу появляются новые функции, исчезают старые (в процессе доработки). После смены мажорного номера, ранее написанный код требуется повторно компилировать и компоновать, что ведет к рискам, что какая-то часть исходного кода перестанет быть совместимой c новой версией Perl API. Это потребует от вас изучения изменений в API, а также выработки решений, как переписать некомпилирующуюся часть кода.

Большая часть расширений CPAN, имеющая XSUB-процедуры, использует язык Perl XS, однако, вы должны знать, что это не единственный способ компоноваться с Perl API. Существует проект SWIG[1], который делает некоторые вещи проще и не ограничивает вас языком Perl, однако, это решение стороннее и его нужно изучать отдельно, тогда как Perl XS доступен из коробки.

Работа компилятора Perl

[править]

Перед тем как начать, давайте немного отвлечемся и поговорим о том, как Perl исполняет ваш код. Во времена своего активного развития, Perl начал использовать довольно передовую на то время технику компиляции, так называемый гибридный компилятор, который повлиял на популярность этого языка. Perl не единственный язык, который использует данную технику: похожая техника используется во многих других языках программирования, например, Java, Python и другие.

Гибридный компилятор сочетает в себе возможности классической компиляции (когда исходный код переводится компилятором в некоторый другой) и интерпретатора (когда код не переводится, а исполняется интерпретатором строка за строкой). В такой технике сначала исполняется часть компилятора, когда из исходных файлов получается некоторое промежуточное представление. Конечным продуктом фазы компиляции в Perl является дерево синтаксического анализаparse tree. Компилятор получает это дерево путем многократных проходов по коду исходных файлов, в результате чего он получает древовидную структуру, узлами которой являются коды операций (opcodes), определяющие представление операции в терминах Perl, а ребра этого дерева связывают операции и определяют порядок действий. Это дерево визуально обычно представляют корневым узлом сверху, потому что так его легче обходить визуально.

Для примера, давайте возьмем выражение $a = $b + $c, тогда часть дерева для этого выражения можно представить так

         =
       /   \
      +     $a
    /   \
  $b     $c

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

$b -> $c -> + -> $a -> =

Данный пример очень простой, но представьте, какими деревья могут быть в очень больших программах.

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

  • Синтаксический анализ снизу вверх. Здесь базовый лексический анализатор проходится по дереву от крайних его листьев в сторону корня, попутно проверяя семантику в каждом узле (например, число параметров и их типы). Попутно оптимизатор выполняет некоторые начальные оптимизации, например, если в вызов передаётся конкретное количество аргументов, то компилятор может не генерировать код, вычисляющий число переданных аргументов, делая вызов проще. Ещё одна оптимизация, называемая свёрткой констант, в случаях, когда код вычисляющий некоторое значение можно заменять конкретным результатом (потому что он постоянный), просто подставляет значение константы. Во время прохода по узлам, анализатор создает временные связки из уже пройденных узлов. Эти связки со временем укрупняются и перестраиваются, поэтому конечное дерево может немного отличаться от того, что мы представили выше.
  • Проход дерева сверху вниз. Обычно на этом этапе расставляются контексты для операций, в которых они должны исполняться. В частности, ведется поиск void-контекста, потому что этот контекст даёт возможность удалить часть кода без побочного эффекта.
  • Peephole оптимизация. На этом этапе для процедур, блоков eval, пакетов исполняется оптимизация подпрограммой peep, которая реализует долгосрочные оптимизации: определение окончательного порядка действий, путем пропуска null-opcodes; распознавание конкатенаций строк, которые присутствуют в одном выражении. Обычно на этом этапе компилятор генерирует предупреждения о сомнительных конструкциях; определяет мертвый код; проверяет прототипы и другое.

Компилятор может завершать свою работу дополнительным, необязательным шагом, в случаях, когда вызывается один из модулей B::: B::Bytecode, B::C, B::CC. В этом случае полученное дерево сериализуется в байт-код, который затем используется на этапе его реконструкции, когда он вызывается потом.

Исполнение дерева

[править]

На этом этапе вступает в работу вторая часть Perl, которая называется интерпретатором Perl. По сути он представляет собой виртуальное исполняющее окружение (как JVM в Java)[2], которое пытается изолировать программу Perl от транслятора конкретной системы, на которой эта программа исполняется. Это даёт преимущество в том, что мы можем абстрагировать компилятор и не привязывать код, который он производит к конкретной исполняющей компьютерной архитектуре, делая код компилятора мультиплатформенным. Именно проблема портируемости программ остро стояла на начальном рубеже развития компьютерной техники.

Интерпретатор языка Perl реализует озвученную задачу исполняя свой виртуальный стек вызовов из кодов операций дерева, которые в терминах этого стека называются PP-кодами (от Push-Pop codes). По сравнению с компилятором, интерпретатор устроен очень просто: всё что от него требуется, это развернуть дерево в длинную линию[3] и выполнить коды операций в этой линии по порядку. Для этого он задействует большое количество стеков:

  • Стек операндов — хранит значения операндов текущей операции;
  • Временный стек — хранит значения, которые позже нужно восстановить для другой операции;
  • Стек области видимости — упрощенный динамический стек, который решает, когда нужно извлечь тот или иной временный стек;
  • Стек контекста — тяжелый динамический контекст, который по сути является стеком вызовов;
  • Jumpenv-стек — стек, используемый для реализации исключительных ситуаций;
  • Стек возврата — хранит точку откуда мы пришли в данную подпрограмму;
  • Mark-стек — определяет место, с которого начинается список аргументов переменного размера;
  • Стек хранения лексических переменных рекурсии.

Хотя Perl использует множество стеков, игнорировать стек языка Си язык Perl полностью не может, но он старается им пользоваться по минимуму, так как его сложно восстанавливать после longjump вызовов, которые использует интерпретатор.

Введение в Perl API

[править]

Лучший способ изучить интерпретатор Perl это читать его исходные файлы, однако, изучать их без вводных будет сложно. Основной документ, описывающий API более-менее формально, это perlguts[4]. С точки зрения заголовочных файлов, основным заголовком является perl.h, который должен включаться в каждое компонуемое расширение.

Далее мы пробежимся по основным структурам и принятым соглашениям Perl. Основные типы данных, о которых мы говорили в главе Типы данных, вводятся структурами данных c именами SV (Scalar Variable), AV (Array Variable), HV (Hash Variable), CV (Code Variable) и GV (Glob Variable). Все они вводятся заголовком sv.h. В своём строении они похожи друг на друга и состоят из:

  • заголовка
    #define _SV_HEAD(ptrtype) \
        ptrtype	sv_any;		/* pointer to body */	\
        U32		sv_refcnt;	/* how many references to us */	\
        U32		sv_flags	/* what we are */
    
    состоящего из указателя на тело, счетчика созданных ссылок и флагов;
  • тела, которое реализовано через union:
    #define _SV_HEAD_UNION \
        union {				\
            char*   svu_pv;		/* pointer to malloced string */	\
            IV      svu_iv;			\
            UV      svu_uv;			\
            _NV_BODYLESS_UNION		\
            SV*     svu_rv;		/* pointer to another SV */		\
            SV**    svu_array;		\
            HE**	svu_hash;		\
            GP*	svu_gp;			\
            PerlIO *svu_fp;			\
        }	sv_u				\
        _SV_HEAD_DEBUG
    

Управляя полями union, Perl легко конвертирует один тип данных в другой c точки зрения языка Си. Также это позволяет переменным Perl самим себя описывать по большей части одинаковым способом.

Чтобы абстрагироваться от аппаратной платформы, Perl вводит хранимые в скаляре типы данных через макросы IV (Integer Value), UV (Unsigned Value), NV (Numeric Value), PV (Pointer Value) и некоторые другие. Чтобы увидеть, как они соотносятся с типами языка Си, нужно заглянуть в заголовок config.h. Например, на 64-х разрядной архитектуре они будут соотноситься так:

#define IVTYPE		long long		/**/
#define UVTYPE		unsigned long long		/**/
#define I8TYPE		char		/**/
#define U8TYPE		unsigned char		/**/
#define I16TYPE		short	/**/
#define U16TYPE		unsigned short	/**/
#define I32TYPE		long	/**/
#define U32TYPE		unsigned long	/**/
#ifdef HAS_QUAD
#define I64TYPE		long long	/**/
#define U64TYPE		unsigned long long	/**/
#endif
#define NVTYPE		double		/**/
#define IVSIZE		8		/**/
#define UVSIZE		8		/**/
#define I8SIZE		1		/**/
#define U8SIZE		1		/**/
#define I16SIZE		2	/**/
#define U16SIZE		2	/**/
#define I32SIZE		4	/**/
#define U32SIZE		4	/**/
#ifdef HAS_QUAD
#define I64SIZE		8	/**/
#define U64SIZE		8	/**/
#endif
#define NVSIZE		8		/**/

Функции, которые вводит API в каждом релизе, описывает документ Perlapi. Необязательно знать их все, а лишь некоторые, так как, возможно, вам понадобиться обращаться к ним явно из XS-кода (мы поговорим о нём в следующей главе).

Здесь важно знать принципы, по которому строятся имена функций:

  • Порождающие функции обычно строятся по шаблону new<что-создает><доп-суффикс>, например
    SV* newSV(const STRLEN len); // Создать скаляр размером len+1.
    AV* newAV();                // Создать массив.
    HV* newHV();                // Создать хеш.
    SV* newSViv(IV);            // Создать скаляр и инициировать его целым числом.
    SV* newSVuv(UV);            // Создать скаляр и инициировать его целым беззнаковым числом.
    
  • Функции, извлекающие значение с динамическим преобразованием, обычно строятся по шаблону <входящий-тип><исходящий-тип>, например
    IV    SvIV(SV* sv);             // Получить значение скаляра, как целое число.
    UV    SvUV(SV* sv);             // Получить значение скаляра, как целое беззнаковое.
    NV    SvNV(SV* sv);             // Получить значение скаляра, как число с плавающей точкой.
    char* SvPV(SV* sv, STRLEN len); // Получить указатель из скаляра.
    char* SvPV_nolen(SV* sv);       // Здесь _nolen говорит, что нет второго параметра.
    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); // Получить указатель на процедуру.
    
  • Глобальные константы начинаются на префикс PL_, например, PL_sv_undef (тип данных undef).
  • Функции, которые реализуют одинаковую операцию, но с разными параметрами, обычно имеют одинаковое имя, а последний символ конкретизирует последний параметр:
    // Копирует одну строку в конец другой.
    void sv_catpv(SV *const sv, const char* ptr);
    // Копирует указанное число байтов.
    void sv_catpvn(SV *dsv, const char *sstr, STRLEN len);
    // Копирует строку, в которой есть терминальный символ \0.
    void sv_catpvs(SV* sv, const char* s);
    

Perl изнутри

[править]

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

Далее по тексту мы будем говорить просто API, имея в виду Perl API.

Типы данных Perl изнутри

[править]

На самом деле, если вы читали эту книгу внимательно, то многое об API вам уже известно; осталось только привести это в плоскость языка Си.

Внутри API три основных типа данных:

  • SV — скаляр;
  • AV — массив скаляров;
  • HV — хеш-массив.

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

  • IV — представляет целочисленное значение.
  • NV — представляет число с плавающей точкой удвоенной точности.
  • PV — представляет строковый литерал.
  • SV — представляет указатель на указатель, т.е. ссылку на другой скаляр.

Чтобы создать новый скаляр, внутри API используется одна из четырех функций:

  • SV* newSViv(IV) — создает скаляр и инициализирует его целочисленным значением.
  • SV* newSVnv(double) — создает скаляр и инициализирует его числом с плавающей точкой.
  • SV* newSVpv(char*, int) — создает скаляр и инициализирует его указателем на строку.
  • SV* newSVsv(SV*) — создает скаляр и инициализирует его указателем на другой скаляр.

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

void  sv_setiv(SV*, IV);
void  sv_setnv(SV*, double);
void  sv_setpvn(SV*, char*, int); // Инициализирует строкой с указанной длиной.
void  sv_setpv(SV*, char*);       // Длина вычислится strlen() неявно.
void  sv_setsv(SV*, SV*);

Обратите внимание, что изначально API работает с однобайтными строками, поэтому везде, где идет речь о строках, следует помнить, что они должны заканчиваться терминирующим \0-символом. Многие функции API добавляют терминирующий символ неявно, однако всегда нужно проявлять максимальную внимательность в этом вопросе.

Чтобы вернуть значение из скаляра в тип данных Си, нужно использовать макросы, которые автоматически связывают хранимый тип в скаляре с типом языка Си:

  • SvIV(SV*)
  • SvNV(SV*)
  • SvPV(SV*, STRLEN len)

В последнем макросе в переменную STRLEN len будет помещен размер строки. Если у вас нет переменной или вас не интересует размер, можно воспользоваться глобальной переменной PL_na (либо SvPVbyte_nolen, SvPVutf8_nolen, или SvPV_nolen).

Так как скаляр динамический с точки зрения Perl, в API предусмотрены макросы для проверок типа данных внутри него, которые возвращают TRUE, если это так:

SvIOK(SV*)   // Это целое число ?
SvNOK(SV*)   // Это вещественное число ?
SvPOK(SV*)   // Это строковый тип ?

SvTRUE(SV*)  // Хранится ли в скаляре что-нибудь?
Массивы
[править]

Существует два основных метода создания массивов:

  • AV* newAV() — создаёт пустой массив AV;
  • AV* av_make(SSize_t, SV**) — создает массив из N-элементов (первый аргумент), каждый из которых заполнен значением второго аргумента.

После того как массив будет создан, его можно видоизменять следующими функциями:

  • void av_push(AV*, SV*) — добавляет элемент в конец массива;
  • SV* av_pop(AV*) — извлекает последний элемент в массиве с удалением;
  • SV* av_shift(AV*) — извлекает первый элемент в массиве с удалением;
  • void av_unshift(AV*, SSize_t) — добавляет N значений в начало массива со значением undef.

Для заполнения массива в произвольной позиции, следует использовать функцию SV** av_store(AV*, SSize_t index, SV* value), которая по значению индекса заполняет значением. Если присваивание удалось, то возвращается предыдущее значение, иначе NULL.

Чтобы извлечь значение из произвольной позиции, следует использовать функцию SV** av_fetch(AV*, SSize_t index, I32 index1). Если index1 не равно нулю, то извлечение значения сопровождается присваиванием значения undef в позиции index.

Чтобы работать с размером уже существующего массива, следует использовать функции:

  • void av_clear(AV*) — очищает массив от элементов, но не удаляет сам массив;
  • void av_undef(AV*) — удаляет массив вместе со всеми элементами;
  • void av_extend(AV*, SSize_t) — расширяет массив таким образом, чтобы он состоял из того числа элементов, которое указано во втором аргументе. Если передано число меньшее фактического размера массива, то ничего не делает.

Если вам известна имя переменной и пакет, где находится массив, ссылку на него можно запросить по имени переменной через функцию AV* get_av((const char *)"packagename::varname", I32 flags). Если значение флагов равно NULL и массива в пакете не существует, вернется NULL.

Хэш-массивы
[править]

Для создания хэш-массива можно использовать функцию HV* newHV(). После того как пустой хэш-массив будет создан, с его элементами можно работать с помощью функций

SV**  hv_store (HV *hv, const char *key, I32 klen, SV *val, U32 hash);  // Записать значение в хэш-массив
SV**  hv_fetch (HV *hv, const char *key, I32 klen, I32 lval); // Извлечь значение из хэш-массива

Переменная klen определяет длину передаваемого ключа; указатель val представляет значение, которое нужно сохранить по ключу key, а hash — это хэш-значение, по которому данное значение будет искаться внутри хэш-массива. Значение hash может быть равно 0, тогда он будет вычислен алгоритмами API.

Чтобы убедиться, что значение существует в массиве, можно воспользоваться функцией bool hv_exists(HV*, const char* key, U32 klen).

Функцией SV* hv_delete(HV*, const char* key, U32 klen, I32 flags) можно извлекать значения из хэш-массива, причём если использовать флаг G_DISCARD, значение извлекается в виде удаляемой в ближайший момент ссылки (т.н. mortal copy).

Для работы с размером хэш-массива, можно использовать функции

void   hv_clear(HV*);    // Очищает, но не удаляет хэш-массив
void   hv_undef(HV*);    // Удаляет все элементы хэш-массив и сам хэш-массив

Внутренне к элементам хэш-массива можно обращаться в форме двусвязного списка, элемент которого реализуется макросом HE (от Hash Element). Для работы с этим типом данных Perl предлагает множество сопутствующих функций:

/* Подготавливает итератор для прохода по двусвязному списку */
I32    hv_iterinit(HV*);

/* Сдвигает итератор и возвращает очередной элемент */
HE*    hv_iternext(HV*);

/* Возвращает ключ из элемента HE */
char*  hv_iterkey(HE* entry, I32* retlen);
          
/* Возвращает значение из элемента HE */
SV*    hv_iterval(HV*, HE* entry);

/* Объединяет возможности hv_iternext, hv_iterkey и hv_iterval. В retlen возвращается
 размер ключа, а само значение возвращается функцией в форме SV */
SV*    hv_iternextsv(HV*, char** key, I32* retlen);

Если известно имя переменной, в которой хэш-массив сохранён, можно получить ссылку на него функцией

HV*  get_hv("package::varname", 0);

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

#  define PERL_HASH(hash,str,len)        \
     STMT_START { \
        const char *s_PeRlHaSh = str; \
        I32 i_PeRlHaSh = len; \
        U32 hash_PeRlHaSh = 0; \
        while (i_PeRlHaSh--) \
            hash_PeRlHaSh = hash_PeRlHaSh * 33 + *s_PeRlHaSh++; \
        (hash) = hash_PeRlHaSh; \
    } STMT_END

Переменные изнутри

[править]

До этого мы представляли функции, работающие с типами данных, как с анонимными программными объектами. Чтобы создать скаляр, массив или хэш-массив, прикрепленный к именованной переменной пакета, нужно использовать одну из следующих функций:

  • SV* get_sv(const char *name, I32 flags) — работает с переменными на скаляры;
  • AV* get_av(const char *name, I32 flags) — работает с переменными на массивы;
  • HV* get_hv(const char *name, I32 flags) — работает с переменными на хэш-массивы.

Для всех трёх функций первый аргумент определяет имя (возможно специфицированное именем пакета), а второй аргумент определяет действие. Если флаг равен нулю, то функция будет предпринимать попытку извлечь тип данных по этому имени, и если переменной не существует, то возвращает NULL. Если установлен флаг GV_ADD, то переменная будет создана.

Ссылки

[править]

Ссылки Perl с точки зрения языка Си не имеют специального представления, а являются особенными скалярами SV[5]. Тем не менее, API функции, которые как то работают с ссылками, обычно имеют строку RV (от Reference Variable) в имени, например

SV *  newRV(SV * const sv)
SV *  newRV_noinc(SV * const tmpRef)

В приведенных выше функциях в первом аргументе допускается указатель на любой разрешенный тип данных, для которого нужно построить ссылку. Основным отличием ссылки является то, что она хранит счетчик использования ссылки, который используется как сигнал для удаления субъекта ссылки из памяти. Некоторые функции имеют суффикс _noinc, который говорит, что ссылка конструируется без увеличения счетчика использования, тогда как другие повышают его на единицу. Ситуация, когда счётчик не повышается, называется передачей правом владения ссылкой: ожидается, что код, который конструирует ссылку без наращивания счётчика, отказывается от её владения (обязуется её не пользоваться) и передаёт её другому участку кода, который теперь отвечает за счетчик использования. Правильное управление счётчиком очень важно, чтобы механизм очистки памяти отрабатывал правильно (ниже мы вернёмся к этому более подробно).

Чтобы определить, является ли скаляр ссылкой, нужно использовать макрос SvROK(SV*), а чтобы понять, на какой тип ссылается ссылка — SvTYPE(SvRV(SV*)), который может возвращать:

  • SVt_IV — скаляр со значением на целое число;
  • SVt_NV — скаляр со значением на вещественное число;
  • SVt_PV — скаляр со строковым значением;
  • SVt_RV — другая ссылка;
  • SVt_PVAV — массив;
  • SVt_PVHV — хэш-массив;
  • SVt_PVCV — процедура;
  • SVt_PVGV — GLOB-ссылка;
  • SVt_PVMG — объект.

Stash-массивы

[править]

Stash-массив — это хэш-массив, который представляет таблицу символов пакета с точки зрения языка Си. Как мы ранее говорили, ключ этого массива является идентификатором гнезда, а значение является глобальной переменной GV. Эта переменная хранит ссылки на различные переменные с именем гнезда идентификатора:

  • скаляры;
  • массивы;
  • хэш-массивы;
  • файловый указатель;
  • указатель каталога;
  • описание формата;
  • указатель на процедуру.

Для пакета main создаётся отдельный Stash-массив, на который можно сослаться через PL_defstash.

Для получения/создания stash-массива используются функции:

HV *  gv_stashpv(const char *name, I32 flags)
HV *  gv_stashsv(SV *sv, I32 flags)

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

Процесс вызова процедур изнутри

[править]

Процедуры Perl вызываются с точки зрения языка Си по-особенному, а именно они требуют работы с собственным стеком вызовов языка Perl. Отчасти по этой причине были придуманы различные конструкции языка XSUB, чтобы завуалировать и упростить этот процесс.

При вызове процедуры Perl, значения параметров процедуры размещаются в стеке, доступ к которому можно получить через макрос ST(n), где в параметре макроса указывается номер элемента стека, который нужно вернуть, при этом нулевой элемент стека — это первый аргумент, переданный подпрограмме Perl. Все аргументы стека имеют тип SV*.

В большинстве ситуаций результат, возвращаемый процедурой из кода Си, может быть обработан макросами RETVAL и OUTPUT, но иногда случается, что стек недостаточно большой, чтобы с его помощью вернуть результат. В этих случаях, перед возвратом результата, стек должен быть расширен макросом EXTEND(stack_ptr, add_number), где add_number — это число, на которое должен быть увеличен стек. После того как стек станет достаточно большим, вы можете втолкнуть в него значения с помощью макросов:

  • PUSHi(IV)
  • PUSHn(double)
  • PUSHp(char*, I32)
  • PUSHs(SV*)

Есть также макросы, автоматически контролирующие размер стека:

  • XPUSHi(IV)
  • XPUSHn(double)
  • XPUSHp(char*, I32)
  • XPUSHs(SV*)

Конкретнее этот процесс следует рассматривать вместе с XS-нотацией.

Так как код языка Perl внутри представлен промежуточным кодом, часть этого кода (в виде процедур) может быть вызвана из XSUB или другого кода языка Си. Обычно для этого используется одна из функций:

I32  call_sv(SV*, I32);
I32  call_pv(const char*, I32);
I32  call_method(const char*, I32);
I32  call_argv(const char*, I32, char**);

Обычно Perl процедуры вызывают через call_sv(SV*, I32), где в первом аргументе передается либо имя вызываемой процедуры, либо ссылка на неё, а флагами во втором аргументе управляется контекст вызова. Остальные функции представляют различные вариации передачи функции для вызова. Все эти функции в качестве результата возвращают размер стека возвращаемых значений.

Некоторые элементы API

[править]

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

Повторное использование стека

[править]

Большая часть opcode-операций Perl занимаются тем, что размещает указатели SV* в некотором стеке интерпретатора. Однако, создавать указатели каждый раз, когда это нужно, было бы накладно, потому что интерпретатор должен работать быстро. В интерпретаторе существует оптимизация, которая призвана повторно использовать ранее созданные указатели. Таким образом, исполняющему коду достаточно занять свободную структуру, установить в ней поля и сдвинуть на вершину стека через один из вызовов (X)PUSH[iunp].

Чтобы иметь возможность занимать эти указатели, компилятор для модулей и подпрограмм генерирует сущности (в виде массивов), которые называются scratchpads. Обычно операции нацеливаются на некоторый scratchpad, а каждый scratchpad ориентируется на лексическую область видимости, а также новый scratchpad заводится для нового уровня рекурсии.

Автоматическое управление памятью в Perl

[править]

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

При работе с объектами из Perl API вам рано или поздно потребуется помогать системе очистки памяти, помечая в нужные моменты используется или не используется сейчас объект. Обычно это происходит, когда вы что-то пытаетесь вернуть из XSUB-процедуры. Для этих целей предусмотрены макросы

int SvREFCNT(SV* sv);
SV* SvREFCNT_inc(SV* sv);
void SvREFCNT_dec(SV* sv);

Чтобы упростить работу с счетчиком, дополнительно вводятся функции, которые оперируют концепцией смертности объекта. В таких функциях обычно фигурирует строка mortal. Смертность объекта — это способ вызывать функцию SvREFCNT_dec() отложено, в ближайшее время. Под ближайшим временем чаще всего подразумевается завершение выполнения текущего оператора. Например, выход из XSUB относится к «ближайшему времени». Момент уменьшения счетчика управляется макросами SAVETMPS и FREETMPS.

Сделать объект смертным можно одной из следующих функций:

SV*  sv_newmortal();     // Создать смертный скаляр.
SV*  sv_mortalcopy(SV*); // Создать смертную копию скаляра.
SV*  sv_2mortal(SV*);    // Отметить скаляр смертным.

Эти функции используются для всех типов объектов, для чего нужно использовать приведение типов.

Выделение памяти

[править]

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

Занимать новые блоки памяти можно через макросы

Newx(pointer, number, type);
Newxc(pointer, number, type, cast);
Newxz(pointer, number, type);

Здесь pointer это имя переменной, которая будет указывать на новый участок памяти; number сколько блоков типа type нужно выделить. Для вычисления размера, внутри макроса над type используется sizeof().

В варианте Newxc() последний аргумент используется, когда тип pointer не совпадает с type. Вариант Newxz() дополнительно с выделением ещё и обнуляет участок памяти.

Для изменения размеров для уже выделенных участков, используются макросы

Renew(pointer, number, type);
Renewc(pointer, number, type, cast);
Safefree(pointer)

Все аргументы Renew() и Renewc() совпадают с Newx() и Newxc() соответственно.

Выделенные участки можно двигать, копировать и обнулять с помощью макросов

Move(source, dest, number, type);
Copy(source, dest, number, type);
Zero(dest, number, type);

В этих макросах source и dest указывают на исходную и целевую точки; number — над сколькими блоками выполняется операция для типа type.

Библиотека ввода-вывода

[править]

Разработчики Perl потратили немало релизов, чтобы создать абстрактную систему ввода-вывода, не привязанную к конкретной системе — PerlIO. В XSUB предпочтительно пользоваться именно ей, потому что это вам позволит улучшить портируемость ваших модулей.

Функции этой системы копируют в основном стандартную библиотеку stdio.h языка Си. Так, вы сразу поймете для чего нужны такие функции, как

/*
  В некоторых функциях-аналогах STDIO в PerlIO изменен порядок следования
  аргументов.
*/

PerlIO *PerlIO_open(const char *path,const char *mode);
PerlIO *PerlIO_fdopen(int fd, const char *mode);
PerlIO *PerlIO_reopen(const char *path, /* deprecated */
        const char *mode, PerlIO *old);
int     PerlIO_close(PerlIO *f);

int     PerlIO_stdoutf(const char *fmt,...)
int     PerlIO_puts(PerlIO *f,const char *string);
int     PerlIO_putc(PerlIO *f,int ch);
SSize_t PerlIO_write(PerlIO *f,const void *buf,size_t numbytes);
int     PerlIO_printf(PerlIO *f, const char *fmt,...);
int     PerlIO_vprintf(PerlIO *f, const char *fmt, va_list args);
int     PerlIO_flush(PerlIO *f);

/* и другие ... */

В то время как STDIO использует структуру FILE, библиотека PerlIO использует структуру PerlIO.

Подробнее с библиотекой можно ознакомиться на странице документации perlapio.

Введение в Perl XS

[править]

Основное назначение макроязыка XS это объявление так называемых XSUB-процедур (от англ. External Subroutine). XSUB-процедуры представляют собой склеивающий код на языке Си, часть которого пишется на макроязыке XS, которая затем с помощью специального компилятора xsubpp преобразуется в код языка Си, а вторая часть, в виде вставок, пишется непосредственно на языке Си. Обычно во второй части вы обращаетесь к Perl API, либо пишите произвольный код Си. Файлы, написанные на XS обычно имеют расширение .xs.

Когда XS-файлы написаны, они прогоняются через компилятор xsubpp, чтобы получить чистый код Си, а затем файлы компилируются стандартным компилятором языка Си (например, gcc). В конце разработки вы должны полученный бинарный код скомпоновать со стандартной библиотекой Perl. Есть два варианта сделать это: компоноваться динамически с получением динамической библиотеки на выходе, либо скомпоноваться со статической библиотекой Perl и получить новый исполняемый файл интерпретатора perl. Второй вариант на практике сейчас встречается крайне редко, так как практически все современные операционные системы поддерживают динамические библиотеки.

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

Структура исходного файла XS

[править]

Документ, описывающий формат XS, представлен на странице PerlXS.

Исходный файл XS, как было упомянуто выше, это помесь языка Си и вставок компилятора xsubpp. Шаблон этого файла, как будет показано позже, можно сгенерировать специальной утилитой, но в общих чертах он всегда выглядит как то так:

/*** Необязательные директивы расширений должны идти до заголовочных файлов. ***/
/*#define ...*/

/*** Заголовки интерпретатора Perl ***/
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
/*
 * Заголовки всегда должны идти именно в таком порядке.
 *   EXTERN.h в основном определяет возможности системного окружения в части компоновки и делает переключения.
 *   perl.h - главный заголовок - вводит Perl API.
 *   XSUB.h - вводит директивы, необходимые компилятору xsubpp.
 */

/* Далее могут идти необязательные заголовки Perl */
/* ppport.h используется для обратной совместимости со старыми версиями Perl */
#include "ppport.h"

/* Далее могут быть пользовательские заголовки, в которых вводятся определения компонуемых библиотек. */

/*** Пользовательский код на языке Си, если он есть ***/

/*** Наконец идут определения XSUB процедур ***/
/* Далее идет пример определения XSUB. */

MODULE = MyModule  PACKAGE = Util

int
is_even(input)
    int input
    CODE:
	    RETVAL = (input % 2 == 0);
	OUTPUT:
	    RETVAL

/* ... Больше XSUB пакета Util ...  */

MODULE = MyModule  PACKAGE = MyModule

/* ... Больше XSUB пакета MyModule ...  */
/* и так далее ... */

Начиная с ключевого слова MODULE весь дальнейший код рассматривается как XS-вставки. Ключевое слово MODULE вводит определение модуля. В пределах одного XS-файла имя модуля всегда следует держать одинаковым (хотя язык этого не требует), а в пределах этого модуля может определяться много пакетов через ключевое слово PACKAGE. Ключевое слово PACKAGE всегда должно следовать за словом MODULE на той же строке. Для модуля компилятор сгенерирует загружающий этот модуль PM-файл на языке Perl. Конкретно в этом примере вводится модуль MyModule, а значит для него будет создан каталог MyModule/ и два пакета MyModule.pm и Util.pm, в которых будет сгенерирован стандартный код, причем стандартный код загрузки всего модуля будет сгенерирован по последнему вхождению, т.е. внутри файла MyModule.pm.

На строке с объявлением модуля может идти необязательное ключевое слово PREFIX, например

MODULE = MyModule  PACKAGE = Util  PREFIX = my_
// Если PACKAGE нет, то слово должно следовать сразу за MODULE.
MODULE = MyModule  PREFIX = my_

которое требует удалять из XSUB вставок этот префикс при генерации окончательного кода. Это сделано для удобства, чтобы иметь возможность не пересекаться по именам в разных вставках. Например, если в модуле есть имя my_function, в Perl коде функция будет видна как function.

Вообще весь код XS-вставок очень чувствителен к отступам, поэтому вы должны следовать рекомендациям и не отступать от них.

Структура XSUB-вставки

[править]

В приведённом выше примере следующее определение

int
is_even(input)
    int input
    CODE:
	    RETVAL = (input % 2 == 0);
	OUTPUT:
	    RETVAL

вводит XSUB-процедуру. Конкретно в этом примере она не делает никаких обращений ни к коду третьей стороны, ни к Perl API, а на языке Си проверяет, является ли входящее число чётным или нечётным. Си код здесь реализуется вставкой

//...
  CODE:
    RETVAL = (input % 2 == 0);
//...

Простейшая XSUB-процедура состоит минимум из трех частей:

  • секция с описанием возвращаемого значения
    int
    // ...
    
  • секции с описанием прототипа-процедуры, которая включает в себя имя и передаваемый список параметров
    // ...
    is_even(input)
    // ...
    
  • секция-список, описывающая типы входящих аргументов (сегмент INPUT:)
    // ...
        int input
    // ...
    
    Этот список может быть пустым, если у функции нет параметров.

Оставшаяся часть представляет собой определения сегментов, состав которых может сильно разниться от одной процедуры к другой и зависит от потребностей программиста и сложности процедуры. Например, у процедуры is_even есть сегмент кода CODE: и сегмент описания исходящего значения OUTPUT: (так как в общем случае Perl разрешает возвращать массивы, здесь нужно конвертировать возвращаемый тип языка Си в тип языка Perl). Конец сегмента происходит в начале следующего, либо в конце определения XSUB-процедуры.

Чтобы xsubpp мог корректно распарсить XSUB-определение, необходимо следовать принятым соглашениям по оформлению кода:

  • Имя процедуры и возвращаемый тип ВСЕГДА должны располагаться на разных строках и выровнены по левому краю:
    // НЕПРАВИЛЬНО
    double sin(x)
        double x
    //...
    
    // ПРАВИЛЬНО
    double
    sin(x)
        double x
    
  • Оставшаяся часть кода может быть выровнена как угодно, но рекомендуется использовать отступы для улучшения читаемости кода. Парсер ограничивает пространство определения текущей процедуры началом определения следующей процедуры:
    // ДОПУСТИМО, НО НЕ ЧИТАЕМО
    double
    sin(x)
    double x
    //...
    
  • Входящие переменные можно описывать в стиле ANSI C, т.е. совмещать определения типов с именами переменных. Обычно это используется, когда вы копируете декларации из существующих заголовочных файлов:
    double
    sin(double x)
    // ...
    
    // ДОПУСТИМО СТАВИТЬ В КОНЦЕ ТОЧКУ С ЗАПЯТОЙ, НО НЕ ОБЯЗАТЕЛЬНО
    double
    sin(double x);
    
  • В языке Си есть знак операции & (взятие адреса переменной) и есть тип данных указатель, который помечается знаком * так, что
    int var = 0;
    int* ptr = &var;
    
    Однако, в XSUB сегмент INPUT: описывается с точки зрения Perl, поэтому знак & говорит xsubpp, что он должен сделать преобразование из типа Perl в тип Си, используя тип слева от знака и вернуть на результат указатель. Таким образом, прототипы
    f(char* a)
    // и
    f(char &a)
    
    имеют в аргументе указатель, но во втором случае ещё происходит преобразование с возвратом указателя.

Принципы написания XSUB-вставок

[править]

При написании вставок вы должны помнить, что Perl прекрасно «знает» язык Си (ведь он на нём написан), но знает он его по-своему (можно выразиться «на уровне Perl-диалекта»). Представьте себя стоящим на границе, где слева от вас стоит язык Perl, а справа язык ANSI Си. Казалось бы, они изъясняются одними и теми же конструкциями, но друг друга не понимают. XSUB-вставки призваны для выравнивания разницы (выступают переводчиком) в сторону ANSI C.

У XSUB-вставки есть внутренний инструментарий, который помогает ей конвертировать фразы, сказанные стороной Perl, в ANSI C и наоборот. Очевидно, что сказанное стороной Perl в сторону ANSI C поступает в XSUB через сегмент INPUT:, а результаты работы XSUB сторона Perl забирает через сегмент OUTPUT:. Все действия по переводу процедура проводит в сегменте CODE:. Попутно XSUB может делать дополнительные действия или учитывать нюансы вызова, используя дополнительные сегменты. В общем случае сторона Perl может не передавать параметров (пустой сегмент INPUT:), либо не принимать ничего в качестве результата, потому что не хочет (маскирование возвращаемого результата), либо потому что результат не предусмотрен стороной ANSI C (пустой сегмент OUTPUT:).

Так как Perl работает в своей парадигме данных (скаляры, массивы, хеши), а ANSI C в своей (примитивные типы данных, массивы, структуры, объединения), XSUB приходится идти на ухищрения и переводить из одной системы типов в другую. Для этого внутри реализации XS используются так называемые карты типов (англ. typemaps). По умолчанию XS знает, как преобразовывать стандартные типы ANSI C в систему типов языка Perl и наоборот, но в более сложных случаях (кастомные структуры) может понадобиться написание собственных карт типов.

XSUB стек

[править]

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

До значений в этом стеке можно добираться с помощью макроса ST(x), где аргумент указывает на позицию в стеке. В простых случаях xsubpp, пользуясь картами типов, сам генерирует код по встраиванию входящих/исходящих значений в стек, но в сложных случаях может потребоваться дополнительный код.

Манипулирование исходящим значением

[править]

Выше вы могли видеть переменную RETVAL, которая генерируется для XSUB автоматически и используется в качестве выходящего значения в системе типов языка Perl. В простых ситуациях она выводит на позицию стека ST(0). Если XSUB имеет тип возвращаемого значения void, то код для RETVAL вообще не генерируется.

Обычно в простых случаях, когда код работы со стеком тривиален, для программирования процедуры используется сегмент CODE:, но в сложных случаях (когда стеком управляете вы) должен использоваться его продвинутый аналог — PPCODE:, который пресекает генерацию тривиального кода. Например, сравните

void
say_hello_silently()
    PPCODE:
        /*
         * Конструируем скаляр и инициализируем его строкой "Hello".
         * Мы используем 0 во втором аргументе, чтобы длина строки
         * вычислялась автоматически (через вызов strlen()). Результат
         * записываем в стек ST(0) через указатель.
         */
        ST(0) = newSVpv("Hello", 0);
        /*
         * Следующий вызов забирает право владения ссылкой в стеке у XSUB
         * и передает право владения ей временному стеку. Это нужно сделать, чтобы
         * не было утечки памяти.
         */
        sv_2mortal(ST(0));
        /*
         * Завершаем выполнение XSUB и передаём размер исходящего стека.
         */
        XSRETURN(1);

и

SV *
say_hello()
    CODE:
        /*
         * Код в примере выше завуалирован в RETVAL.
         */
        RETVAL = newSVpv("Hello",0);
    OUTPUT:
        RETVAL

К сожалению, код последнего примера работает корректно только со скалярами. При попытке использовать в RETVAL ссылки на любые массивы или ссылку на скаляр, мы получим утечку памяти. Это известная проблема в Perl 5, которая не исправляется для сохранения обратной совместимости со старыми CPAN модулями. Для её исправления нужно либо использовать скорректированную карту типов T_AVREF_REFCOUNT_FIXED, доступную с версии 5.16, либо передавать право владения ссылкой стеку вручную следующим образом

AV *
get_array()
    CODE:
        /*
         * Конструируем пустой массив в парадигме Perl.
         */
        RETVAL = newAV();
        sv_2mortal((SV*)RETVAL);
        /* Другие действия ... */
    OUTPUT:
        RETVAL

Вернёмся к сегменту OUTPUT:. Данный сегмент говорит стороне Perl, что, по завершении работы XSUB, некоторые входящие параметры будут находиться в измененном состоянии (если используется принцип возвращать значение во входящих параметрах), либо XSUB готовит результат в возвращаемом параметре.

В следующем примере мы подсказываем компилятору, что входящий аргумент является и выходящим

void
input_output(double arg)
   CODE:
        /* Что-то делаем */
   OUTPUT:
       arg

Сегмент OUTPUT: должен использоваться всегда, когда присутствует CODE:, так как RETVAL не распознается автоматически как исходящая переменная, если этот сегмент есть.

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

void
input_output(double arg)
     OUTPUT:
         arg sv_setnv(ST(0), (double)arg);

При желании перед возвращаемым типом XSUB может помещаться ключевое слово NO_OUTPUT:

NO_OUTPUT int
no_output_xsub(char *name)
//...

Это позволяет замаскировать исходящее значение, сохраняемое в RETVAL, при это не отказываясь от него. Это ключевое слово не даёт генерировать автоматический код в сегменте OUTPUT:, связанный с RETVAL.

Манипулирование входящими значениями

[править]

Этапы жизненного цикла XSUB и способы манипулирования ими

[править]

Этапы сборки модуля

[править]

Примечания

[править]
  1. Официальный сайт проекта SWIG: www.swig.org
  2. Интересен тот факт, что Ларри отказывается называть своё детище PVM.
  3. В английской терминологии это называется нитьюthread.
  4. На METACPAN этот документ частично переведён на русский: perlguts
  5. Вообще, в Perl API всё, что имеет какую-то особенность, либо как то выделяется, принято называть магическим. В документации Perl вы можете ещё не раз встретить этот термин.



← Работа со строками и регулярные выражения Приложения →