Некоторые сведения о 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 и другие.
Гибридный компилятор сочетает в себе возможности классической компиляции (когда исходный код переводится компилятором в некоторый другой) и интерпретатора (когда код не переводится, а исполняется интерпретатором строка за строкой). В такой технике сначала исполняется часть компилятора, когда из исходных файлов получается некоторое промежуточное представление[2]. Конечным продуктом фазы компиляции в 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)[3], которое пытается изолировать программу Perl от транслятора конкретной системы, на которой эта программа исполняется. Это даёт преимущество в том, что мы можем абстрагировать компилятор и не привязывать код, который он производит к конкретной исполняющей компьютерной архитектуре, делая код компилятора мультиплатформенным. Именно проблема портируемости программ остро стояла на начальном рубеже развития компьютерной техники.
Интерпретатор языка Perl реализует озвученную задачу исполняя свой виртуальный стек вызовов из кодов операций дерева, которые в терминах этого стека называются PP-кодами (от Push-Pop codes). По сравнению с компилятором, интерпретатор устроен очень просто: всё что от него требуется, это развернуть дерево в длинную линию[4] и выполнить коды операций в этой линии по порядку. Для этого он задействует большое количество стеков:
- Стек операндов — хранит значения операндов текущей операции;
- Временный стек — хранит значения, которые позже нужно восстановить для другой операции;
- Стек области видимости — упрощенный динамический стек, который решает, когда нужно извлечь тот или иной временный стек;
- Стек контекста — тяжелый динамический контекст, который по сути является стеком вызовов;
- Jumpenv-стек — стек, используемый для реализации исключительных ситуаций;
- Стек возврата — хранит точку откуда мы пришли в данную подпрограмму;
- Mark-стек — определяет место, с которого начинается список аргументов переменного размера;
- Стек хранения лексических переменных рекурсии.
Хотя Perl использует множество стеков, игнорировать стек языка Си язык Perl полностью не может, но он старается им пользоваться по минимуму, так как его сложно восстанавливать после longjump вызовов, которые использует интерпретатор.
Введение в Perl API
[править]Лучший способ изучить интерпретатор Perl это читать его исходные файлы, однако, изучать их без вводных будет сложно. Основной документ, описывающий API более-менее формально, это perlguts[5]. С точки зрения заголовочных файлов, основным заголовком является 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[6]. Тем не менее, 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);
- В языке Си есть знак операции
&(взятие адреса переменной) и есть тип данных указатель, который помечается знаком*так, чтоОднако, в XSUB сегментint var = 0; int* ptr = &var;
INPUT:описывается с точки зрения Perl, поэтому знак&говоритxsubpp, что он должен сделать преобразование из типа Perl в тип Си, используя тип слева от знака и вернуть на результат указатель. Таким образом, прототипыимеют в аргументе указатель, но во втором случае ещё происходит преобразование с возвратом указателя.f(char* a) // и f(char &a)
Ключевые слова XSUBPP
[править]Ниже приведена сводная таблица по ключевым словам, которые вводит XSUBPP-компилятор.
| Ключевое слово | Описание |
|---|---|
MODULE
|
Определяет в исходном файле начало кода XS-языка, а также вводит пакет, к которому XSUB процедуры будут принадлежать. |
PACKAGE
|
Если в одном исходном файле объявлено несколько XSUB процедур, но по замыслу они должны принадлежать разным пакетам, то данное слово позволяет ввести новое пространство в рамках одного модуля. |
PREFIX
|
Назначает строку префикса в рамках модуля, который должен отсекаться при экспорте XSUB в клиентский код. Обычно это используется, чтобы устранить пересечение по именам на уровне языка Си (в котором, вообще говоря, нет пространств имён), и чтобы не тащить префиксы в код языка Perl. |
OUTPUT
|
Вводит секцию-преобразователь исходящих параметров. В этой секции описываются преобразования, которые должны быть выполнены над исходящей переменной, прежде чем её вернуть в код Perl. |
NO_OUTPUT
|
Ключевое слово используется, чтобы запретить возврат значения в код языка Perl. Обычно используется, когда логика XSUB требует использования RETVAL, но при этом ничего возвращать вызывающему коду не требуется.
|
CODE
|
Используется, когда требуется один или несколько промежуточных вызовов кода языка Си в относительно простых ситуациях. Для этой секции всегда инициализируется исходящая переменная, спрятанная за макросом RETVAL, однако, вы всегда должны явно показывать, что именно её значение возвращается XSUB наружу в секции OUTPUT.
|
INIT
|
Похожа на CODE, но для неё не инициализируется RETVAL. Обычно используется, чтобы выполнить проверки над входящими значениями и проинициализировать их значениями по умолчанию.
|
TYPEMAP
|
Позволяет ввести карту типов typemap прямо в исходном файле XSUB (начиная с версии 5.16). До этого карта типов вводилась исключительно через отдельный файл. Обычно используется для отладки, либо когда модуль только начинает разрабатываться и карта типов должна быть под носом.
|
PREINIT
|
Позволяет ввести дополнительные переменные сразу после обработки секции INPUT и до обработки карты типов typemap. Используется в сложных случаях, когда нужно инициализировать переменные в обход карты типов.
|
SCOPE
|
Используется в сложных случаях, чтобы временно включать и отключать записи карты типов для некоторых процедур. |
INPUT
|
Обычно используется совместно с PREINIT, когда нужно установить момент времени обработки входящих параметров до неё, сразу после входа в процедуру.
|
INOUTLISTIN_OUTLISTOUTIN_OUT
|
Используется для тонкого обозначения типов передаваемых параметров в категориях входящий/исходящий. По умолчанию все параметры относятся к категории IN — входящие. Главным образом используются для оптимизации проверок памяти, так как для различных категорий она выполняется по-разному.
|
C_ARGS
|
Используется, когда интерфейс функции на стороне Perl и на стороне Си не согласуются, либо такое несогласование сделано умышленно. Позволяет передать другой список аргументов, отличный от того, что был передан на стороне Perl, но скрывая это внутри XSUB. |
PPCODE
|
Похож на CODE, но не инициализирует RETVAL и вообще требует от вас управлять стеком вызова XSUB самостоятельно. Используется для более гибкого управления вызовами, нежели CODE.
|
REQUIRE
|
Используется для обозначения того, какая меньшая версия компилятора xsubpp ожидается. Самая меньшая версия, которую можно указать, 1.922.
|
CLEANUP
|
Должно следовать за сегментами CODE и OUTPUT, и используется, когда нужна особая очистка окружения исполнения, прежде чем отдать управление вызвавшему XSUB коду.
|
POSTCALL
|
Должно следовать до сегментов OUTPUT и CLEANUP, и используется в ситуациях, когда за вызовом кода языка Си всегда должен выполняться некоторый особый вызов.
|
BOOT
|
Используется, когда разработчику нужны некоторые действия в момент загрузки XSUB в память Perl. |
VERSIONCHECK
|
Соответствует параметрам компилятора -versioncheck и -noversioncheck, но имеет повышенный приоритет. Используется, когда нужно сравнить версии исходного файла XSUB с версией загружающего его модуля .pm.
|
PROTOTYPES
|
Соответствует параметрам компилятора -prototypes и -noprototypes, но имеет повышенный приоритет. Разрешает/запрещает передавать прототипы со стороны кода Perl (если они там определены) в XSUB процедуру. По умолчанию такая возможность включена.
|
PROTOTYPE
|
Похоже на PROTOTYPES, но позволяет указать прототип явно в исходном файле XSUB. Если прототип задан прямо в XSUB с помощью этого ключевого слова, то он будет перекрывать прототип, идущий со стороны кода Perl.
|
ALIAS
|
Позволяет задать один и более псевдонимов, под которыми данная процедура может быть вызвана со стороны Perl. |
OVERLOAD
|
Похожа на ALIAS, но вызов процедуры осуществляется посредством перегрузки знака операции, подобно директивному модулю overload. Используется, когда перегрузить операцию удобно непосредственно из XSUB.
|
FALLBACK
|
Используется вместе с OVERLOAD. Смысл такой же, как у флага fallback в директивном модуле overload. По умолчанию установлен в TRUE.
|
INTERFACE
|
По смыслу очень похоже на ALIAS, позволяя вызывать одну и ту же XSUB по разным именам, но с одинаковым интерфейсом, с той разницей, что в ALIAS вы должны программировать вызовы по псевдонимам на стороне Perl, а здесь генерация определений полностью осуществляется на стороне XS.
|
INTERFACE_MACRO
|
Используется как альтернатива INTERFACE, чтобы определить логику выбора из псевдонимов через макросы. Используется в особых ситуациях, когда выбор реализации может быть завязан на архитектуру исполняющего окружения.
|
INCLUDE
|
В небольших модулях весь код XSUB может храниться в одном файле, однако, вы можете его разнести по разным файлам и включать его в текущий исходный файл до компиляции xsubpp. По смыслу тоже самое, что и директива препроцессора #include в языке Си.
|
INCLUDE_COMMAND
|
Позволяет выполнить произвольную команду перед выполнением INCLUDE. Это может быть простой вывод содержимого включаемого файла, либо вызов ещё одного препроцессора.
|
CASE
|
Похоже на switch..case, но относительно всего тела XSUB-процедуры. Позволяет вам проверить некоторое условие контекста вызова и по нему, возможно, выбрать одну из множества реализаций этой процедуры.
|
EXPORT_XSUB_SYMBOLS
|
До версии 5.16.0 ничего не делает. С версии 5.16.0 значение ENABLE делает все функции XSUB статическими с точки зрения языка Си. Значение по умолчанию DISABLE. Обычно не требуется менять это значение, но если требуется, то скорее всего для разрешения проблем компоновки.
|
Принципы написания XSUB-вставок
[править]При написании вставок вы должны помнить, что Perl прекрасно «знает» язык Си (ведь он на нём написан), но знает он его по-своему (можно выразиться «на уровне Perl-диалекта»). Представьте себя стоящим на границе, где слева от вас стоит язык Perl, а справа язык ANSI Си. Казалось бы, они изъясняются одними и теми же конструкциями, но друг друга не понимают. XSUB-вставки призваны для выравнивания разницы[7] (выступают переводчиком) в сторону 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 и способы манипулирования ими
[править]Этапы сборки модуля
[править]Новый XS-модуль начинается с типового каркаса, который вы можете сгенерировать утилитой h2xs (акроним англ. headers to XS). У генерирующей утилиты довольно много опций, использование которых во многом зависит от начальных условий. Например, вы могли бы предоставить утилите уже готовые прототипы функций из заголовочных файлов языка Си, по которым утилита могла бы сгенерировать часть исходного кода.
Когда проект создается с нуля, обычно утилита вызывается с опциями -c или -A, которые не требуют заголовочные файлы, и -n <имя-модуля> для создания типичного проекта расширения Perl.
$ h2xs -cn Example
Defaulting to backwards compatibility with perl 5.42.0
If you intend this module to be compatible with earlier perl versions, please
specify a minimum perl version with the -b option.
Writing Example/ppport.h
Writing Example/lib/Example.pm
Writing Example/Example.xs
Writing Example/Makefile.PL
Writing Example/README
Writing Example/t/Example.t
Writing Example/Changes
Writing Example/MANIFEST
В выводе генератора выводится рекомендация об использовании также опции -b <номер-версии> для указания минимального номера версии Perl, с которым ваше расширение будет иметь обратную совместимость. При использовании этой опции генератор кода будет учитывать особенности старых версий, поэтому код может изобиловать устаревшими конструкциями или дополнительными проверками. В нашем случае генератор кода использует текущую версию Perl 5.42, как минимальную.
Содержимое проекта
[править]Генератор создает директорию с определенным набором файлов. Большинство из них, вообще говоря, необязательны, но требуются, если вы планируете передавать модуль кому-либо еще, так как за много лет существования CPAN к ним уже привыкли многие разработчики.
MANIFEST— список файлов, которые несет в себе модуль. В этом файле будет перечислено всё, что сгенерировалh2xs. Обычно этот файл используется автоматическими сканерами при размещении модуля в репозитории, для индексации.Changes— текстовый файл, в котором разработчик ведет перечень изменений в проекте от версии к версии. Этот файл составляется в произвольной форме и обычно читается только людьми. Такой формат был распространен на заре компьютерной эры, до появления более совершенных систем контроля версий.Makefile.PL— мета-makefile для утилиты MakeMaker. Используется для генерации сборочного файла утилитыmake— основного инструмента сборки кода на языке Си в *nix системах.- директория
t/— директория с модульными тестами. В этой директории разработчик размещает тесты, призванные проверить совместимость модуля с целевой системой пользователя. Обычно тесты в этом каталоге запускаются до установки модуля в системные директории. Если вы посмотрите в эту директорию, то увидите, что в ней был создан шаблонный файл с тестами следующего содержания:Код тестов по умолчанию генерируется под фреймворк# Before 'make install' is performed this script should be runnable with # 'make test'. After 'make install' it should work as 'perl Example.t' ######################### # change 'tests => 1' to 'tests => last_test_to_print'; use strict; use warnings; use Test::More tests => 1; BEGIN { use_ok('Example') }; ######################### # Insert your test code below, the Test::More module is use()ed here so read # its man page ( perldoc Test::More ) for help writing this test script.
Test::More. Обычно тесты составляются так, чтобы проверить некоторое утверждение, которое возвращает ИСТИНУ или ЛОЖЬ. Далее вызовы фреймворка проверяют утверждение и делают вывод об успешности теста. Хотя нам нечего проверять, один тест всегда генерируется автоматически: это возможность загрузить модуль в программу (use_ok('Example')). ppport.h— заголовочный файл с директивами для обратной совместимости. Хранит директивы, которые эмулируют поведение новых функций в старых версиях интерпретатора и наоборот. Используется, если вы хотите, чтобы ваш модуль работал в широком круге версий интерпретатора Perl.Example.pmиExample.xs— собственно заготовки для будущего расширяющего модуля. ФайлExample.pmпредставляет загружающий модуль в код языка Perl, аExample.xs— XS-вставки. Загружающий модуль лежит в директорииlib/, что сделано специально для имитации установки модуля в системе. Файл XS находится в корне, так как он является промежуточным, и его расположение не играет большой роли.
В каталог lib/ нужно помещать весь Perl код, относящийся к модулю, так как он упаковывается в дистрибутивный архив.
Этот набор файлов является джентльменским для любого расширения языка Perl. Разработчик волен добавлять любые другие файлы, если того требует его модуль.
Подготовка сборочного файла
[править]Основной сборочной утилитой для проектов Perl была и остается make[8]. В *nix эта утилита обычно всегда установлена в системе. В Windows нужно установить её порт Make for Windows, либо, если Perl был установлен из ActivePerl или StrawberryPerl, утилита идет в комплекте с дистрибутивом.
Так как сборка в различных архитектурах может сильно отличаться, сборочный файл нужно генерировать для каждой системы каждый раз заново. К счастью, с Perl идет специальный модуль MakeMaker, который упрощает эту процедуру. Для этого модуля вы должны предоставить только некоторые параметры для компилятора и компоновщика, а также некоторые детали вашего проекта.
Среди сгенерированных файлов проекта вы можете найти Makefile.PL, который хранит входные данные для MakeMaker. Если вы откроете заготовку, то увидите примерно следующее:
use 5.042000;
use ExtUtils::MakeMaker;
# See lib/ExtUtils/MakeMaker.pm for details of how to influence
# the contents of the Makefile that is written.
WriteMakefile(
NAME => 'Example',
VERSION_FROM => 'lib/Example.pm', # finds $VERSION, requires EU::MM from perl >= 5.5
PREREQ_PM => {}, # e.g., Module::Name => 1.1
ABSTRACT_FROM => 'lib/Example.pm', # retrieve abstract from module
AUTHOR => 'A. U. Thor <a.u.thor@a.galaxy.far.far.away>',
#LICENSE => 'perl',
#Value must be from legacy list of licenses here
#https://metacpan.org/pod/Module::Build::API
LIBS => [''], # e.g., '-lm'
DEFINE => '', # e.g., '-DHAVE_SOMETHING'
INC => '-I.', # e.g., '-I. -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
);
Утилита имеет довольно много параметров, с которыми можно ознакомится на странице ExtUtils::MakeMaker. Набор параметров может меняться от релиза к релизу. Ниже перечислены некоторые из них.
NAME— обязательный параметр, который используется как имя дистрибутива в системе именования CPAN. Данное имя должно быть допустимым с точки зрения языка Perl, а также оно должно правильно соотносится с загружающим.pmмодулем как по имени, так и по его фактическому расположению. В нашем примере модуль не вложен ни в одно пространство, но если бы он был вложен, эта вложенность также должна быть учтена в его имени.VERSION_FROM— определяет файл, из которого можно взять версию модуля. До этого версию обычно писали через атрибутVERSION, но со временем такая практика оказалась неудобной, так как нужно было контролировать актуальность версии в нескольких местах проекта.PREREQ_PM— хеш, представляющий перечень модулей, от которых зависит текущий, и без которых он не может быть запущен. В качестве ключа используется дистрибутивное имя модуля, а в качестве значения — минимальный номер версии зависимости. Если указать в качестве значения 0, то это будет означать любую версию. Эта информация используется для генерации специального мета-файла, который может быть использован при размещении модуля в репозитории, а также при его установке.ABSTRACT_FROM— указывает файл, в котором хранится описание модуля в формате POD. Используется системой автоматического документирования.LIBS— список библиотек, с которым ваш модуль будет компоноваться. Используется формат, поддерживаемый системным компоновщиком.DEFINE— список внешних директив компилятора.INC— список путей для поиска заголовочных файлов для компилятора.OBJECT— список объектных файлов для компоновщика, если они получаются через сборку из другого проекта. Используется в продвинутых сценариях сборки.
Чтобы получить сборочный сценарий для утилиты make, достаточно просто запустить Makefile.PL как программу Perl:
$ perl Makefile.PL
После этого утилита сгенерирует файл Makefile, который можно передавать утилите make. Помимо этого, будут сгенерированы файлы MYMETA.json и MYMETA.yml, используемые при размещении дистрибутива модуля в CPAN-репозитории.
Если вы знакомы с утилитой make, то вы можете ознакомиться с правилами сборки, но ни в коем случае ничего не редактируйте вручную. Наибольший интерес представляют цели, которые генерирует MakeMaker. Например, следующие цели генерируются в версии 5.42:
config— проверяет сборочный каталог. Если он не готов для сборки, то подготавливает структуру каталогов.static— добавляет/замещает объектный файл в архиве статической библиотеки.dynamic— строит динамическую библиотеку.test— запускает тест на возможность динамической или статической компоновки, а также запуск модульных тестов.linkext— синоним целиdynamic.manifest— строит файл манифеста.blibdirs— промежуточная цель целиconfig, которая строит структуру директорииblib/.clean— очищает каталог сборки от результатов предыдущей сборки.realclean— промежуточная цель целиclean, которая очищает сборочный каталог от временных файлов.dist— упаковывает сборку в дистрибутивный архив.disttest— выполняет тест сборочного каталога на предмет возможности создать на его основе дистрибутив.distdir— создает структуру дистрибутива.pure_all— полный цикл сборки динамической библиотеки.subdirs— промежуточная цель сборки динамической библиотеки. Используется при компоновке со сторонними библиотеками.clean_subdirs— по умолчанию ничего не делает. Очищает промежуточные каталоги от результатов.makemakerdflt— запускает стандартный конвейер сборки.
Сборка
[править]Для сборки вам понадобится совместимый компилятор и компоновщик. Как правило, в *nix инструменты разработчика уже установлены в системе. В Windows, если Perl устанавливался из дистрибутивов ActivePerl или StrawberryPerl, все инструменты сборки уже идут в комплекте, и это учитывается утилитой MakeMaker.
Для начала сборки откройте командную оболочку и сделайте сборочный каталог текущим, после чего запустите сборку простой командой
$ make
По умолчанию, утилита make ищет файл Makefile в текущей директории и запускает самую первую его цель. Ниже показана распечатка процесса сборки в Windows:
cp lib/Example.pm blib\lib\Example.pm
AutoSplitting blib\lib\Example.pm (blib\lib\auto\Example)
Running Mkbootstrap for Example ()
"C:\Strawberry\perl\bin\perl.exe" -MExtUtils::Command -e chmod -- 644 "Example.bs"
"C:\Strawberry\perl\bin\perl.exe" -MExtUtils::Command::MM -e cp_nonempty -- Example.bs blib\arch\auto\Example\Example.bs 644
"C:\Strawberry\perl\bin\perl.exe" "C:\Strawberry\perl\lib\ExtUtils/xsubpp" -typemap C:\STRAWB~1\perl\lib\ExtUtils\typemap Example.xs > Example.xsc
Please specify prototyping behavior for Example.xs (see perlxs manual)
"C:\Strawberry\perl\bin\perl.exe" -MExtUtils::Command -e mv -- Example.xsc Example.c
gcc -c -I. -std=c99 -DWIN32 -DWIN64 -DPERL_TEXTMODE_SCRIPTS -DMULTIPLICITY -DPERL_IMPLICIT_SYS -DUSE_PERLIO -D__USE_MINGW_ANSI_STDIO -fwrapv -fno-strict-aliasing -mms-bitfields -O2 -DVERSION=\"0.01\" -DXS_VERSION=\"0.01\" "-IC:\STRAWB~1\perl\lib\CORE" Example.c
"C:\Strawberry\perl\bin\perl.exe" -MExtUtils::Mksymlists \
-e "Mksymlists('NAME'=>\"Example\", 'DLBASE' => 'Example', 'DL_FUNCS' => { }, 'FUNCLIST' => [], 'IMPORTS' => { }, 'DL_VARS' => []);"
g++ Example.def -o blib\arch\auto\Example\Example.xs.dll -shared -s -L"C:\STRAWB~1\perl\lib\CORE" -L"C:\STRAWB~1\c\lib" -L"C:\STRAWB~1\c\x86_64-w64-mingw32\lib" -L"C:\STRAWB~1\c\lib\gcc\x86_64-w64-mingw32\13.2.0" Example.o "C:\STRAWB~1\perl\lib\CORE\libperl542.a" -lmoldname -lkernel32 -luser32 -lgdi32 -lwinspool -lcomdlg32 -ladvapi32 -lshell32 -lole32 -loleaut32 -lnetapi32 -luuid -lws2_32 -lmpr -lwinmm -lversion -lodbc32 -lodbccp32 -lcomctl32 -Wl,--enable-auto-image-base
"C:\Strawberry\perl\bin\perl.exe" -MExtUtils::Command -e chmod -- 755 blib\arch\auto\Example\Example.xs.dll
Сначала процедура сборки строит каталог blib/, в котором будут размещаться все промежуточные результаты. Этот каталог является своего рода полигоном: структура этого каталога воспроизводит файловую систему целевой системы. Например, в нем создаются следующие каталоги:
arch/— архитектурно-зависимые файлы, например, разделяемые библиотеки, скомпонованные под конкретную программную архитектуру.bin/— каталог с программами, которыми, возможно, пользуется ваш модуль.lib/— каталог с модулями Perl, которыми, возможно, пользуется ваш модуль.man1/— каталог с MAN-страницами, генерируемыми для вашего модуля, для категории 1 (Исполняемые программы или команды оболочки).man3/— каталог с MAN-страницами, генерируемыми для вашего модуля, для категории 3 (Функции в библиотеках программ).script/— здесь размещаются разного рода скрипты, которые используются во время установки модуля в системе.
Далее вызывается утилита xsubpp, которая пытается сгенерировать промежуточный код на языке Си из XS-вставок. Результатом этого этапа является файл Example.c. Так как наш пример не содержит реального кода, файл Example.c ничего не содержит, кроме некоторых технологических директив, но и этого достаточно, чтобы собрать объектный файл.
Последние два этапа это компиляция и компоновка в разделяемую библиотеку Example.xs.dll. Обратите внимание, что разделяемая библиотека компонуется с конкретной версией Perl API (в данном случае libperl542.a), что и является возможной причиной несовместимости со старыми версиями Perl. Тем не менее, выше мы говорили, что библиотека предусматривает кучу макросов для возможности сохранения обратной совместимости.
Тестирование
[править]Для запуска модульных тестов, достаточно запустить цель test, находясь в сборочной директории:
$ make test
"C:\Strawberry\perl\bin\perl.exe" -MExtUtils::Command::MM -e cp_nonempty -- Example.bs blib\arch\auto\Example\Example.bs 644
"C:\Strawberry\perl\bin\perl.exe" "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness(0, 'blib\lib', 'blib\arch')" t/*.t
t/Example.t .. ok
All tests successful.
Files=1, Tests=1, 0 wallclock secs ( 0.05 usr + 0.00 sys = 0.05 CPU)
Первым действием сценарий пытается найти не пустые .bs сценарии (от англ. bootstrap), которые используются в некоторых системах для загрузки разделяемых библиотек в память. В Windows эти файлы не используются, поэтому они генерируются пустыми. Затем запускается модуль Test::Harness, который может автоматически запускать тесты. Для этого в процедуру test_harness() передается три аргумента: уровень логирования (0 означает тихий режим); директория для массива @INC; директория для добавления в @INC платформозависимых модулей. Аргументом всей команды является путь до директории с тестами.
В нашем примере всего один тест, который проверяет отработку директивы use на нашем модуле. В данном случае тест проходит успешно, то есть модуль может быть загружен и исполнен в текущем окружении. В конце тестов выводятся некоторые метрики, призванные примерно оценить производительность кода.
Динамический загрузчик
[править]Хотя все выглядит прозрачно, не совсем понятно, как происходит загрузка разделяемой библиотеки в память во время запуска. Если вы загляните в исходный код загружающего модуля Example.pm, то увидите в нем такие строки
# ...
require XSLoader;
XSLoader::load('Example', $VERSION);
# ...
В этих строках и кроется весь секрет. Модуль XSLoader служит для загрузки динамических символов в память программы. Его функции XSLoader::load() передается имя пространства имен и версия модуля. Далее модуль, пользуясь @INC, пытается найти файл разделяемой библиотеки. Если файл найден, то он открывается динамическим компоновщиком (dlopen() в *nix и LoadLibrary() в Windows), а затем из него читаются динамические символы и размещаются в памяти процесса. Так как процедуры скомпонованы по нужным Perl соглашениям, ему не трудно транслировать вызовы к ним из программы Perl напрямую. Если какие-то XSUB процедуры компилировались с сегментом BOOT, то коды этих сегментов выполняются сразу после загрузки библиотеки в память.
До появления XSLoader для загрузки символов обычно использовался модуль DynaLoader, и код с его участием выглядел бы как то так
# ...
require DynaLoader;
# ...
bootstrap Example $VERSION;
# ...
DynaLoader часто можно увидеть в старом коде, однако, в новом коде в большинстве случаев рекомендуется использовать более современный XSLoader для загрузки XSUB процедур. Разница между загрузчиками следующая:
- загрузчик
XSLoaderустроен проще, а значит у него меньше накладных расходов и улучшенная производительность; - загрузчик
XSLoaderоптимизирован под XS-вставки, тогда какDynaLoaderразрабатывался для загрузки любых динамических символов.
Другими словами, если расширение было построено через Perl XS, то используйте XSLoader, однако, если вам нужна большая гибкость в загрузке динамических библиотек, то следует воспользоваться DynaLoader.
Использование blib для поиска модулей
[править]Выше мы упомянули о том, что директория blib является состоянием модуля между его разработкой и установкой в систему. Возникает вопрос, как воспользоваться разрабатываемым модулем в произвольной программе в произвольный момент времени?
В большинстве ситуаций вы должны воспользоваться модулем blib, который ориентирован на структуру, получаемую MakeMaker. Если каталог проекта является текущим, то этот модуль ищет директорию blib/ относительно него. Допустим мы хотим воспользоваться нашим модулем из такой программы:
# Файл: mytest.pl
use Example;
print "Hello\n";
Так как модуль еще не установлен в системе, вызвать нашу программу мы должны так:
$ perl -Mblib mytest.pl
# или так
$ perl -Mblib="." mytest.pl
# где путь указан явно.
Существуют и другие способы. Так как нам нужно добавить пути поиска загружающего модуля и динамической библиотеки в @INC, мы можем это делать из исходного кода директивами.
# Файл: mytest1.pl
use blib ".";
use Example;
print "Hello\n";
$ perl mytest1.pl
Hello
или так
# Файл: mytest2.pl
use lib qw [ blib/lib blib/arch ];
use Example;
print "Hello\n";
$ perl mytest2.pl
Hello
Упаковка в дистрибутивный архив
[править]После того, как основная работа над модулем будет завершена, его следует упаковать в дистрибутивный архив для распространения. В этот архив, как правило, упаковываются следующие файлы:
lib/*— исходные коды, в основном написанные на языке Perl (и других языках, если проект комплексный).t/*— набор модульных и/или интеграционных тестов.Changes— текстовый файл с описанием изменений в релизе.*.xs(если есть) — исходные файлы с XS-кодом. Если модуль очень простой, то их может не быть.Makefile.PL(илиBuild.PL) — инструкции генерации сборочного сценария. Как правило, ожидаются в формате MakeMaker, но если вы используете иную систему сборки, вы можете использовать её. Главное отразить это в README-файле.MANIFEST— перечень всех файлов в архиве. Как правило используется сторонним сканером архива при загрузке файлов в хранилище. Любые другие файлы, не попавшие в манифест, обычно игнорируются.META.(json|yml)— метаданные дистрибутивного архива. Могут использоваться сторонними решениями, например, они могут использоваться специальными парсерами для генерации шаблонной веб-страницы, если у хранилища есть сайт.*.h(если есть) — заголовочные файлы модуля, которые могут использоваться во время сборки.README— текстовый файл с инструкциями по сборке и установке. В нем может быть размещена любая другая информация, которую пользователь должен прочитать в первую очередь. MakeMaker всегда генерирует этот файл с шаблонной инструкцией по использованию утилиты make.
Конечный архив может зависеть от пожеланий целевого пользователя или от используемого в целевой системе менеджера пакетов. Самый базовый вариант, который реализует MakeMaker, это упаковка файлов в tar-архив, возможно, с сжатием (через gzip). Для этого в Makefile реализована цель dist:
$ make dist
Итоговый файл будет носить имя модуля, записанного в NAME атрибут MakeMaker, где разделитель паркетов :: заменяется на тире -. В конце имени будет указан номер версии модуля. Расширение может зависеть от наличия утилиты gzip в системе: если утилиты нет, то результатом будет не сжатый tar-ball, иначе tar.gz архив со степенью сжатия best. Для нашего примера архив будет иметь имя Example-0.01.tar.gz.
Примечания
[править]- ↑ Официальный сайт проекта SWIG: www.swig.org
- ↑ На самом деле в Perl стадии компиляции и интерпретации строго не отделены. Компиляция может вызываться во время интерпретации (например,
eval) и, наоборот, некоторая интерпретация может производиться во время компиляции (блокBEGIN). Следует рассматривать perl как конечный автомат, который может находиться либо в состоянии компиляции, либо в состоянии интерпретации. Но как бы там ни было, большая часть кода исполняется после компиляции. - ↑ Интересен тот факт, что Ларри отказывается называть своё детище PVM.
- ↑ В английской терминологии это называется нитью — thread.
- ↑ На METACPAN этот документ частично переведён на русский: perlguts
- ↑ Вообще, в Perl API всё, что имеет какую-то особенность, либо как то выделяется, принято называть магическим. В документации Perl вы можете ещё не раз встретить этот термин.
- ↑ В англоязычной литературе используется термин склеивающий код (англ. glue code).
- ↑ Существует альтернативный вариант сборки через модуль
Module::Build, который генерируетBuild.PLсборочный скрипт. Этот вариант не будет рассматриваться в этом учебнике, так как он не так сильно прижился в сообществе. Его основным плюсом является то, что он не требует установки утилиты make и следовательно знаний синтаксиса её сборочного файла, что делает сборку более портируемой. Тем не менее, на практике обычно окружение разработки всегда готово к сборке, что не делает убедительным это преимущество.