perlinterp
СОДЕРЖАНИЕ
- НАЗВАНИЕ
- ОПИСАНИЕ
- ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА
- ДЕРЕВЬЯ ОПЕРАЦИЙ
- СТЕКИ
- МИЛЛИОНЫ МАКРОСОВ
- ДОПОЛНИТЕЛЬНАЯ ЛИТЕРАТУРА
НАЗВАНИЕ
perlinterp - Обзор интерпретатора Perl
ОПИСАНИЕ
Этот документ предоставляет обзор работы интерпретатора Perl на уровне кода C, а также ссылки на соответствующие файлы исходного кода C.
ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА
Работа интерпретатора проходит в два основных этапа: компиляция кода во внутреннее представление, или байткод, и затем его выполнение. "Компилируемый код" в perlguts подробно описывает, как происходит этап компиляции.
Вот краткое описание работы Perl:
Запуск
Действие начинается в файле perlmain.c. (или miniperlmain.c для miniperl) Это код очень высокого уровня, который помещается на один экран, и он похож на код в perlembed; большая часть реального действия происходит в perl.c
perlmain.c генерируется ExtUtils::Miniperl из miniperlmain.c во время сборки, поэтому вам следует собрать Perl, чтобы следовать этому.
Сначала perlmain.c выделяет некоторую память и создаёт интерпретатор Perl, примерно так:
1 PERL_SYS_INIT3(&argc,&argv,&env);
2
3 if (!PL_do_undump) {
4 my_perl = perl_alloc();
5 if (!my_perl)
6 exit(1);
7 perl_construct(my_perl);
8 PL_perl_destruct_level = 0;
9 } Строка 1 — макрос, и его определение зависит от вашей операционной системы. Строка 3 ссылается на PL_do_undump, глобальную переменную — все глобальные переменные в Perl начинаются с PL_. Это говорит о том, был ли текущий запущенный процесс создан с флагом -u для perl и затем undump, что означает, что он будет ложным в любом разумном контексте.
Строка 4 вызывает функцию в perl.c для выделения памяти для интерпретатора Perl. Это довольно простая функция, и её суть выглядит так:
my_perl = (PerlInterpreter*)PerlMem_malloc(sizeof(PerlInterpreter)); Здесь вы видите пример абстракции системы Perl, которую мы увидим позже: PerlMem_malloc — это либо системная malloc, либо собственная malloc Perl, определённая в malloc.c, если вы выбрали этот вариант при настройке.
Далее, в строке 7, мы создаём интерпретатор с помощью perl_construct, также в perl.c; это устанавливает все специальные переменные, необходимые для Perl, стеки и т. д.
Теперь мы передаём Perl параметры командной строки и говорим ему начать:
if (!perl_parse(my_perl, xs_init, argc, argv, (char **)NULL))
perl_run(my_perl);
exitstatus = perl_destruct(my_perl);
perl_free(my_perl); perl_parse фактически является оболочкой вокруг S_parse_body, как определено в perl.c, которая обрабатывает параметры командной строки, настраивает все статически связанные XS-модули, открывает программу и вызывает yyparse для её парсинга.
Парсинг
Цель этого этапа — взять исходный код Perl и преобразовать его в дерево операций. Мы увидим, как выглядит одно из них позже. Строго говоря, здесь происходит три вещи.
yyparse, парсер, находится в perly.c, хотя вам лучше прочитать исходный файл YACC в perly.y. (Да, в Perl есть грамматика YACC!) Задача парсера — взять ваш код и "понять" его, разделив его на предложения, определив, какие операнды соответствуют каким операторам и так далее.
Парсеру помогает лексический анализатор, который разбивает ваш ввод на токены и определяет тип каждого токена: имя переменной, оператор, оголённое слово, подпрограмма, внутренняя функция и так далее. Основной входной точкой в лексический анализатор является yylex, и её связанные функции находятся в toke.c. Perl сильно отличается от других языков программирования; он иногда сильно зависит от контекста, поэтому сложно определить тип токена или где он заканчивается. В связи с этим между лексическим анализатором и парсером много взаимодействий, что может быть довольно пугающим, если вы к этому не привыкли.
По мере понимания парсером программы Perl он строит дерево операций для интерпретатора во время выполнения. Функции, которые строят и связывают различные операции, находятся в op.c и будут рассмотрены позже.
Оптимизация
Теперь этап парсинга завершен, и готовое дерево представляет собой операции, которые интерпретатор Perl должен выполнить для выполнения нашей программы. Затем Perl выполняет прогон по дереву в поисках оптимизаций: константные выражения, такие как 3 + 4, будут вычислены сейчас, и оптимизатор также проверит, можно ли заменить несколько операций одной. Например, чтобы получить доступ к переменной $foo, вместо того, чтобы получить доступ к глобальной переменной *foo и посмотреть на скалярную часть, оптимизатор изменяет дерево операций, чтобы использовать функцию, которая непосредственно ищет нужный скаляр. Основной оптимизатор — peep в op.c, и многие операции имеют свои функции оптимизации.
Выполнение
Теперь мы, наконец, готовы: у нас есть скомпилированный байткод Perl, и всё, что осталось сделать, — это запустить его. Фактическое выполнение выполняется функцией runops_standard в run.c; точнее, это делается этими тремя безобидными строками:
while ((PL_op = PL_op->op_ppaddr(aTHX))) {
PERL_ASYNC_CHECK();
} Вам может быть удобнее использовать Perl-версию:
PERL_ASYNC_CHECK() while $Perl::op = &{$Perl::op->{function}}; Ну, может быть, и нет. В любом случае, каждая операция содержит указатель на функцию, которая указывает на функцию, которая фактически выполняет операцию. Эта функция вернёт следующую операцию в последовательности — это позволяет выполнять такие вещи, как if , которые выбирают следующую операцию динамически во время выполнения. PERL_ASYNC_CHECK гарантирует, что такие вещи, как сигналы, прерывают выполнение, если это необходимо.
Фактические вызываемые функции известны как PP-код, и они распределены между четырьмя файлами: pp_hot.c содержит «горячий» код, который чаще всего используется и оптимизирован, pp_sys.c содержит все функции, специфичные для системы, pp_ctl.c содержит функции, которые реализуют управляющие структуры (if, while и т. д.), а pp.c содержит всё остальное. Это, если хотите, код C для встроенных функций и операторов Perl.
Обратите внимание, что каждая pp_ функция должна возвращать указатель на следующую операцию. Вызовы подпрограмм Perl (и блоков eval) обрабатываются в том же цикле runops и не занимают дополнительное место в стеке C. Например, pp_entersub и pp_entertry просто помещают структуру блока CxSUB или CxEVAL в стек контекста, содержащую адрес операции, следующей за вызовом подпрограммы или eval. Затем они возвращают первую операцию этого подпрограммы или блока eval, и выполнение продолжается в этой подпрограмме или блоке. Позже операция pp_leavesub или pp_leavetry извлекает структуру блока CxSUB или CxEVAL, извлекает возвращающую операцию из неё и возвращает её.
Обработка исключений
Обработка исключений в Perl (т. е. die и т. д.) основана на низкоуровневых функциях setjmp()/longjmp() библиотеки C. В основном они обеспечивают способ захвата текущих регистров PC и SP и последующего их восстановления; т. е. longjmp() продолжается в той точке кода, где ранее была выполнена setjmp(), при этом всё, что выше в стеке C, теряется. Вот почему код всегда должен сохранять значения с помощью SAVE_FOO вместо автоматических переменных.
Ядро Perl оборачивает setjmp() и т. д. в макросы JMPENV_PUSH и JMPENV_JUMP. Основное правило исключений Perl состоит в том, что exit, и die (при отсутствии eval) выполняют JMPENV_JUMP(2), в то время как die внутри eval выполняет JMPENV_JUMP(3).
В точках входа в Perl, таких как perl_parse(), perl_run() и call_sv(cv, G_EVAL), каждый выполняет JMPENV_PUSH, затем входит в цикл runops или что-то подобное и обрабатывает возможные возвращаемые значения исключений. При возвращении 2 выполняется заключительная очистка, например, извлечение стеков и вызов блоков CHECK или END. Среди прочего, именно так происходит очистка области видимости при exit.
Если die может найти блок CxEVAL в стеке контекста, то стек извлекается до этого уровня, и возвращающая операция в этом блоке присваивается PL_restartop; затем выполняется JMPENV_JUMP(3). Это обычно возвращает управление обратно защите. В случае perl_run и call_sv, ненулевой PL_restartop запускает повторный вход в цикл runops. Это обычный способ обработки die или croak внутри eval.
Иногда операции выполняются внутри внутреннего цикла runops, например, код привязки, сортировки или перегрузки. В этом случае что-то вроде
sub FETCH { eval { die } } вызовет longjmp прямо обратно в защитник в perl_run, извлекая оба цикла runops, что очевидно неправильно. Один из способов избежать этого — для кода привязки выполнить JMPENV_PUSH перед выполнением FETCH во внутреннем цикле runops, но по причинам эффективности Perl фактически устанавливает флаг, используя CATCH_SET(TRUE). Операции pp_require, pp_entereval и pp_entertry проверяют этот флаг, и если он истинный, они вызывают docatch, который выполняет JMPENV_PUSH и запускает новый уровень цикла runops для выполнения кода, а не делает это в текущем цикле.
В качестве дополнительной оптимизации при выходе из блока eval в FETCH, выполнение кода после блока всё ещё продолжается во внутреннем цикле. Когда возникает исключение, docatch сравнивает уровень JMPENV блока CxEVAL с PL_top_env и если они различаются, просто перебрасывает исключение. Таким образом, любые внутренние циклы извлекаются.
Вот пример.
1: eval { tie @a, 'A' };
2: sub A::TIEARRAY {
3: eval { die };
4: die;
5: } Для выполнения этого кода вызывается perl_run, который выполняет JMPENV_PUSH, а затем входит в цикл runops. Этот цикл выполняет операции eval и tie на строке 1, при этом eval помещает CxEVAL в стек контекста.
pp_tie выполняет CATCH_SET(TRUE), затем запускает второй цикл runops для выполнения тела TIEARRAY. При выполнении операции entertry на строке 3 значение CATCH_GET истинно, поэтому pp_entertry вызывает docatch, который выполняет JMPENV_PUSH и запускает третий цикл runops, который затем выполняет операцию die. В этот момент стек вызовов C выглядит следующим образом:
Perl_pp_die
Perl_runops # third loop
S_docatch_body
S_docatch
Perl_pp_entertry
Perl_runops # second loop
S_call_body
Perl_call_sv
Perl_pp_tie
Perl_runops # first loop
S_run_body
perl_run
main а стеки контекста и данных, как показано -Dstv, выглядят так:
STACK 0: MAIN
CX 0: BLOCK =>
CX 1: EVAL => AV() PV("A"\0)
retop=leave
STACK 1: MAGIC
CX 0: SUB =>
retop=(null)
CX 1: EVAL => *
retop=nextstate Die извлекает первый CxEVAL из стека контекста, устанавливает PL_restartop из него, выполняет JMPENV_JUMP(3) и управление возвращается к верхнему docatch. Это затем запускает ещё один цикл runops третьего уровня, который выполняет операции nextstate, pushmark и die на строке 4. В тот момент, когда вызывается второй pp_die, стек вызовов C выглядит точно так же, как и выше, даже если мы больше не находимся внутри внутреннего eval; это из-за упомянутой ранее оптимизации. Однако стек контекста теперь выглядит следующим образом, то есть с извлечённым верхним CxEVAL:
STACK 0: MAIN
CX 0: BLOCK =>
CX 1: EVAL => AV() PV("A"\0)
retop=leave
STACK 1: MAGIC
CX 0: SUB =>
retop=(null) Die на строке 4 извлекает стек контекста вниз до CxEVAL, оставив его следующим образом:
STACK 0: MAIN
CX 0: BLOCK => Как обычно, PL_restartop извлекается из CxEVAL, и выполняется JMPENV_JUMP(3), которая возвращает стек C обратно к docatch:
S_docatch
Perl_pp_entertry
Perl_runops # second loop
S_call_body
Perl_call_sv
Perl_pp_tie
Perl_runops # first loop
S_run_body
perl_run
main В этом случае, поскольку уровень JMPENV, записанный в CxEVAL, отличается от текущего, docatch просто выполняет JMPENV_JUMP(3), и стек C разворачивается до:
perl_run
main Поскольку PL_restartop не равно нулю, run_body запускает новый цикл runops, и выполнение продолжается.
ТИПЫ ВНУТРЕННИХ ПЕРЕМЕННЫХ
К этому моменту вы, вероятно, уже ознакомились с perlguts, в котором рассказывается о внутренних типах переменных Perl: SVs, HVs, AVs и других. Если нет, сделайте это сейчас.
Эти переменные используются не только для представления переменных в Perl, но и для представления любых констант в коде, а также для некоторых полностью внутренних структур Perl. Например, таблица символов — это обычный хеш Perl. Ваш код представлен как SV при чтении в парсер; любые вызываемые вами файлы программ открываются с помощью обычных файловых дескрипторов Perl и так далее.
Основной модуль Devel::Peek позволяет нам исследовать SVs из программы Perl. Например, давайте посмотрим, как Perl обрабатывает константу "hello".
% perl -MDevel::Peek -e 'Dump("hello")'
1 SV = PV(0xa041450) at 0xa04ecbc
2 REFCNT = 1
3 FLAGS = (POK,READONLY,pPOK)
4 PV = 0xa0484e0 "hello"\0
5 CUR = 5
6 LEN = 6 Чтение выходных данных Devel::Peek требует немного практики, поэтому давайте пройдёмся по каждой строке.
Строка 1 говорит нам, что мы смотрим на SV, который находится в памяти по адресу 0xa04ecbc. Сами SVs — это очень простые структуры, но они содержат указатель на более сложную структуру. В данном случае это PV, структура, которая хранит строковое значение по адресу 0xa041450. Строка 2 — счётчик ссылок; других ссылок на эти данные нет, поэтому он равен 1.
Строка 3 — флаги для этого SV — его можно использовать как PV, это SV только для чтения (поскольку это константа), и данные внутри — это PV. Далее у нас есть содержимое строки, начиная с адреса 0xa0484e0.
Строка 5 даёт нам текущую длину строки — обратите внимание, что это не включает нулевой терминатор. Строка 6 — это не длина строки, а длина текущего выделенного буфера; по мере роста строки Perl автоматически расширяет доступное хранилище с помощью процедуры, называемой SvGROW.
Вы можете очень легко получить доступ к любому из этих количеств из C; просто добавьте Sv к имени поля, показанному в фрагменте, и вы получите макрос, который вернёт значение: SvCUR(sv) возвращает текущую длину строки, SvREFCOUNT(sv) возвращает счётчик ссылок, SvPV(sv, len) возвращает саму строку вместе с её длиной и так далее. Более подробные макросы для управления этими свойствами можно найти в perlguts.
Давайте рассмотрим пример манипулирования PV, из sv_catpvn, в sv.c
1 void
2 Perl_sv_catpvn(pTHX_ SV *sv, const char *ptr, STRLEN len)
3 {
4 STRLEN tlen;
5 char *junk;
6 junk = SvPV_force(sv, tlen);
7 SvGROW(sv, tlen + len + 1);
8 if (ptr == junk)
9 ptr = SvPVX(sv);
10 Move(ptr,SvPVX(sv)+tlen,len,char);
11 SvCUR(sv) += len;
12 *SvEND(sv) = '\0';
13 (void)SvPOK_only_UTF8(sv); /* validate pointer */
14 SvTAINT(sv);
15 } Это функция, которая добавляет строку ptr длиной len в конец PV, хранящегося в sv. Первое, что мы делаем на строке 6, — это убедиться, что у SV есть действительный PV, вызвав макрос SvPV_force для принудительного создания PV. В качестве побочного эффекта, tlen устанавливается в текущее значение PV, а сам PV возвращается в junk.
На строке 7 мы убеждаемся, что у SV будет достаточно места для размещения старой строки, новой строки и нулевого терминатора. Если LEN недостаточно велико, SvGROW перевыделит для нас память.
Теперь, если junk совпадает со строкой, которую мы пытаемся добавить, мы можем получить строку непосредственно из SV; SvPVX — это адрес PV в SV.
Строка 10 выполняет фактическое конкатенирование: макрос Move перемещает кусок памяти: мы перемещаем строку ptr в конец PV — это начало PV плюс его текущая длина. Мы перемещаем len байт типа char. После этого нам нужно сообщить Perl, что мы расширили строку, изменив CUR для отражения новой длины. SvEND — это макрос, который даёт нам конец строки, поэтому он должен быть "\0".
Строка 13 манипулирует флагами; поскольку мы изменили PV, любые значения IV или NV больше не будут действительными: если у нас есть $a=10; $a.="6";, мы не хотим использовать старое IV 10. SvPOK_only_utf8 — это специальная версия SvPOK_only, учитывающая UTF-8, макрос, который отключает флаги IOK и NOK и включает POK. Конечный SvTAINT — это макрос, который очищает данные, если включён режим taint.
AVs и HVs сложнее, но SVs — это, безусловно, наиболее распространённый тип переменной, используемый. Увидели, как мы манипулируем ими, давайте перейдём к рассмотрению того, как строится дерево op.
ДЕРЕВЬЯ OP
Во-первых, что такое дерево op? Дерево op — это проанализированное представление вашей программы, как мы видели в разделе о парсинге, и это последовательность операций, которые Perl выполняет для выполнения вашей программы, как мы видели в "Running".
Op — это фундаментальная операция, которую может выполнить Perl: все встроенные функции и операторы — это op, а также есть ряд op, которые обрабатывают концепции, необходимые интерпретатору — вход и выход из блока, завершение оператора, получение переменной и так далее.
Дерево op соединено двумя способами: можно представить, что есть два «пути» через него, два порядка, в которых можно пройти по дереву. Во-первых, порядок парсинга отражает, как парсер понял код, а во-вторых, порядок выполнения указывает Perl, в каком порядке выполнять операции.
Самый простой способ изучить дерево op — это остановить Perl после завершения парсинга и заставить его вывести дерево. Именно это делают компиляторы backends B::Terse, B::Concise и модуль CPAN <B::Debug.
Давайте посмотрим, как Perl видит $a = $b + $c:
% perl -MO=Terse -e '$a=$b+$c'
1 LISTOP (0x8179888) leave
2 OP (0x81798b0) enter
3 COP (0x8179850) nextstate
4 BINOP (0x8179828) sassign
5 BINOP (0x8179800) add [1]
6 UNOP (0x81796e0) null [15]
7 SVOP (0x80fafe0) gvsv GV (0x80fa4cc) *b
8 UNOP (0x81797e0) null [15]
9 SVOP (0x8179700) gvsv GV (0x80efeb0) *c
10 UNOP (0x816b4f0) null [15]
11 SVOP (0x816dcf0) gvsv GV (0x80fa460) *a Начнём с середины, с строки 4. Это BINOP, бинарный оператор, расположенный по адресу 0x8179828. Конкретный оператор — sassign — присваивание скаляра — и вы можете найти код, который его реализует, в функции pp_sassign в pp_hot.c. Как бинарный оператор, он имеет двух потомков: оператор сложения, предоставляющий результат $b+$c, находится вверху строки 5, а левая часть — на строке 10.
Строка 10 — это нулевой оператор: он вообще ничего не делает. Зачем он там? Если вы видите нулевой оператор, это значит, что что-то было оптимизировано после парсинга. Как мы упоминали в "Optimization", на этапе оптимизации иногда две операции объединяются в одну, например, при получении скалярной переменной. В таких случаях вместо переписывания дерева op и очистки висячих указателей проще заменить ненужную операцию на нулевой оператор. Первоначально дерево выглядело бы так:
10 SVOP (0x816b4f0) rv2sv [15]
11 SVOP (0x816dcf0) gv GV (0x80fa460) *a То есть взять запись a из основной таблицы символов, а затем посмотреть на скалярную составляющую: gvsv (pp_gvsv в pp_hot.c) делает и то, и другое.
Правая часть, начиная со строки 5, похожа на то, что мы только что видели: у нас есть оператор add (pp_add, также в pp_hot.c), который складывает два gvsv.
И что же это такое?
1 LISTOP (0x8179888) leave
2 OP (0x81798b0) enter
3 COP (0x8179850) nextstate enter и leave — это операторы области видимости, и их задача — выполнять все необходимые действия каждый раз, когда вы входите в блок и выходите из него: лексические переменные приводятся в порядок, неиспользуемые переменные уничтожаются и так далее. В каждой программе будут эти три первые строки: leave — это список, а его потомки — все операторы в блоке. Операторы отделяются друг от друга nextstate, поэтому блок — это коллекция операторов nextstate, а операторы, которые необходимо выполнить для каждого оператора, являются потомками nextstate. enter — это единственный оператор, который служит маркером.
Вот как Perl парсил программу сверху вниз:
Program
|
Statement
|
=
/ \
/ \
$a +
/ \
$b $c Однако невозможно выполнить операции в этом порядке: необходимо определить значения $b и $c перед их сложением, например. Поэтому другой поток, который проходит через дерево op, — это порядок выполнения: каждый оператор имеет поле op_next, которое указывает на следующий оператор, который нужно выполнить, поэтому прослеживание этих указателей показывает, как Perl выполняет код. Мы можем пройтись по дереву в этом порядке, используя опцию exec для B::Terse:
% perl -MO=Terse,exec -e '$a=$b+$c'
1 OP (0x8179928) enter
2 COP (0x81798c8) nextstate
3 SVOP (0x81796c8) gvsv GV (0x80fa4d4) *b
4 SVOP (0x8179798) gvsv GV (0x80efeb0) *c
5 BINOP (0x8179878) add [1]
6 SVOP (0x816dd38) gvsv GV (0x80fa468) *a
7 BINOP (0x81798a0) sassign
8 LISTOP (0x8179900) leave Это, вероятно, более понятно человеку: вход в блок, начало оператора. Получить значения $b и $c, сложить их. Найти $a, и присвоить одно другому. Затем выход.
Способ, которым Perl строит эти деревья op во время процесса парсинга, можно изучить, изучив toke.c, лексер, и perly.y, грамматику YACC. Давайте посмотрим на код, который строит дерево для $a = $b + $c.
Сначала мы рассмотрим функцию Perl_yylex в лексическом анализаторе. Мы хотим найти case 'x', где x — первый символ оператора. (Кстати, при поиске кода, обрабатывающего ключевое слово, вам нужно будет найти KEY_foo , где «foo» — это ключевое слово.) Вот код, обрабатывающий присваивание (существует довольно много операторов, начинающихся с =, поэтому большинство из них опущено для краткости):
1 case '=':
2 s++;
... code that handles == => etc. and pod ...
3 pl_yylval.ival = 0;
4 OPERATOR(ASSIGNOP); Мы видим на строке 4, что наш тип токена — ASSIGNOP (OPERATOR — макрос, определённый в toke.c, который, среди прочего, возвращает тип токена). И +:
1 case '+':
2 {
3 const char tmp = *s++;
... code for ++ ...
4 if (PL_expect == XOPERATOR) {
...
5 Aop(OP_ADD);
6 }
...
7 } Строка 4 проверяет, какой тип токена мы ожидаем. Aop возвращает токен. Если вы найдёте Aop в другом месте в toke.c, вы увидите, что он возвращает токен типа ADDOP.
Теперь, когда мы знаем два типа токенов, которые мы хотим найти в парсере, давайте возьмём фрагмент perly.y, необходимый для построения дерева для $a = $b + $c
1 term : term ASSIGNOP term
2 { $$ = newASSIGNOP(OPf_STACKED, $1, $2, $3); }
3 | term ADDOP term
4 { $$ = newBINOP($2, 0, scalar($1), scalar($3)); } Если вы не знакомы с чтением грамматик Бэкуса — Наура, вот как это работает: вы получаете определённые вещи от токенизатора, которые обычно записываются заглавными буквами. ADDOP и ASSIGNOP — примеры «терминальных символов», потому что их нельзя упростить.
Грамматика, строки один и три фрагмента выше, показывает, как строить более сложные формы. Эти сложные формы, «нетерминальные символы», обычно записываются строчными буквами. term здесь является нетерминальным символом, представляющим одно выражение.
Грамматика даёт вам следующее правило: вы можете создать то, что слева от двоеточия, если увидите все элементы справа в последовательности. Это называется «сведением», и цель парсинга — полностью свести входные данные. Есть несколько способов выполнить сведение, разделённые вертикальными чертами: например, term следом за = следом за term образует term, и term следом за + следом за term также может образовать term.
Таким образом, если вы видите два термина с = или + между ними, вы можете преобразовать их в одно выражение. При этом вы выполняете код в блоке на следующей строке: если вы видите =, вы выполните код в строке 2. Если вы видите +, вы выполните код в строке 4. Именно этот код вносит вклад в дерево op.
| term ADDOP term
{ $$ = newBINOP($2, 0, scalar($1), scalar($3)); } Это создаёт новый бинарный оператор и передаёт ему несколько переменных. Переменные ссылаются на токены: $1 — первый токен на входе, $2 — второй и так далее — думайте о ссылках обратных ссылок в регулярных выражениях. $$ — оператор, возвращаемый этим сведением. Таким образом, мы вызываем newBINOP для создания нового бинарного оператора. Первый параметр для newBINOP, функции из op.c, — тип оператора. Это оператор сложения, поэтому мы хотим, чтобы тип был ADDOP. Мы могли бы указать это напрямую, но оно находится прямо там, как второй токен на входе, поэтому мы используем $2. Второй параметр — флаги оператора: 0 означает «ничего особенного». Затем добавляемые элементы: левая и правая части нашего выражения в скалярном контексте.
Функции, создающие операторы, которые имеют имена, подобные newUNOP и newBINOP, вызывают функцию «проверки», связанную с каждым типом оператора, прежде чем вернуть оператор. Функции проверки могут изменять оператор по своему усмотрению и даже заменить его совершенно новым. Эти функции определены в op.c и имеют префикс Perl_ck_. Вы можете узнать, какая функция проверки используется для определённого типа оператора, посмотрев в regen/opcodes. Например, OP_ADD. (OP_ADD — значение токена из Aop(OP_ADD) в toke.c, который парсер передаёт newBINOP в качестве своего первого аргумента.) Вот соответствующая строка:
add addition (+) ck_null IfsT2 S S В данном случае функция проверки — Perl_ck_null, которая ничего не делает. Давайте рассмотрим более интересный случай:
readline <HANDLE> ck_readline t% F? И вот функция из op.c:
1 OP *
2 Perl_ck_readline(pTHX_ OP *o)
3 {
4 PERL_ARGS_ASSERT_CK_READLINE;
5
6 if (o->op_flags & OPf_KIDS) {
7 OP *kid = cLISTOPo->op_first;
8 if (kid->op_type == OP_RV2GV)
9 kid->op_private |= OPpALLOW_FAKE;
10 }
11 else {
12 OP * const newop
13 = newUNOP(OP_READLINE, 0, newGVOP(OP_GV, 0,
14 PL_argvgv));
15 op_free(o);
16 return newop;
17 }
18 return o;
19 } Особый аспект заключается в том, что если у оператора нет детей (т. е., readline() или <>) оператор освобождается и заменяется совершенно новым, который ссылается на *ARGV (строки 12-16).
СТЭКИ
Когда Perl выполняет что-то вроде addop, как он передаёт свои результаты следующему оператору? Ответ — через использование стеков. Perl имеет несколько стеков для хранения элементов, над которыми он в данный момент работает, и здесь мы рассмотрим три самых важных.
Стек аргументов
Аргументы передаются коду PP и возвращаются из кода PP с помощью стека аргументов, ST. Типичный способ обработки аргументов — извлечь их из стека, обработать их по своему усмотрению и затем вернуть результат в стек. Так, например, работает оператор косинуса:
NV value;
value = POPn;
value = Perl_cos(value);
XPUSHn(value); Мы увидим более сложный пример этого, когда рассмотрим макросы Perl ниже. POPn даёт вам NV (значение с плавающей точкой) верхнего SV в стеке: $x в cos($x). Затем мы вычисляем косинус и возвращаем результат в виде NV. X в XPUSHn означает, что стек должен быть расширен при необходимости — здесь это не требуется, так как мы знаем, что есть место для одного дополнительного элемента в стеке, поскольку мы только что удалили один! Макросы XPUSH* по крайней мере гарантируют безопасность.
В качестве альтернативы вы можете непосредственно управлять стеком: SP даёт вам первый элемент в вашей части стека, а TOP* — верхний SV/IV/NV и т. д. в стеке. Например, чтобы выполнить унарное отрицание целого числа:
SETi(-TOPi); Просто установите значение целого числа верхнего элемента стека в его отрицание.
Работа со стеком аргументов в ядре идентична работе в XSUB-функциях — см. perlxstut, perlxs и perlguts для более подробного описания макросов, используемых в работе со стеком.
Стек меток
Я говорю «ваша часть стека» выше, потому что код PP не обязательно получает весь стек целиком: если ваша функция вызывает другую функцию, вы захотите выделить только аргументы, предназначенные для вызываемой функции, и не (необязательно) дать ей доступ к вашим собственным данным. Для этого мы имеем «виртуальное» дно стека, доступное каждой функции. Стек меток содержит метки местоположений в стеке аргументов, доступных каждой функции. Например, при работе со связанной переменной (внутренне, что-то с «магией P») Perl должен вызывать методы для доступа к связанным переменным. Однако нам нужно отделить аргументы, доступные методу, от аргументов, доступных исходной функции — хранение, извлечение или что-то другое. Вот приблизительно, как реализована связанная переменная push; см. av_push в av.c:
1 PUSHMARK(SP);
2 EXTEND(SP,2);
3 PUSHs(SvTIED_obj((SV*)av, mg));
4 PUSHs(val);
5 PUTBACK;
6 ENTER;
7 call_method("PUSH", G_SCALAR|G_DISCARD);
8 LEAVE; Давайте рассмотрим всю реализацию для практики:
1 PUSHMARK(SP); В стек меток записывается текущее состояние указателя стека. Это нужно для того, чтобы, когда мы закончим добавлять элементы в стек аргументов, Perl знал, сколько элементов мы добавили недавно.
2 EXTEND(SP,2);
3 PUSHs(SvTIED_obj((SV*)av, mg));
4 PUSHs(val); Мы добавим ещё два элемента в стек аргументов: когда у вас связанный массив, подпрограмма PUSH получает объект и значение, которое должно быть добавлено, и именно это мы имеем здесь — связанный объект, полученный с помощью SvTIED_obj, и значение, SV val.
5 PUTBACK; Далее мы сообщим Perl об обновлении глобального указателя стека из нашей внутренней переменной: dSP дал нам только локальную копию, а не ссылку на глобальную.
6 ENTER;
7 call_method("PUSH", G_SCALAR|G_DISCARD);
8 LEAVE; ENTER и LEAVE локализуют блок кода — они гарантируют, что все переменные приведены в порядок, что все локализованные элементы возвращают свои предыдущие значения и т. д. Подумайте о них как об { и } блоке Perl.
Для фактического вызова магического метода нам нужно вызвать подпрограмму в пространстве Perl: call_method позаботится об этом, и это описано в perlcall. Мы вызываем метод PUSH в скалярном контексте и будем игнорировать его возвращаемое значение. Функция call_method() удаляет верхний элемент стека меток, поэтому вызывающий не должен ничего очищать.
Стек сохранения
В C нет понятия локальной области, поэтому Perl предоставляет его. Мы видели, что ENTER и LEAVE используются как скобки области; стек сохранения реализует эквивалент C, например:
{
local $foo = 42;
...
} См. "Локализация изменений" в perlguts о том, как использовать стек сохранения.
Миллионы макросов
Вы заметите, что исходный код Perl содержит множество макросов. Кто-то считает повсеместное использование макросов самым сложным для понимания, другие находят, что это добавляет ясности. Давайте рассмотрим пример — упрощённую версию кода, реализующего оператор сложения:
1 PP(pp_add)
2 {
3 dSP; dATARGET;
4 tryAMAGICbin_MG(add_amg, AMGf_assign|AMGf_numeric);
5 {
6 dPOPTOPnnrl_ul;
7 SETn( left + right );
8 RETURN;
9 }
10 } В каждой строке (кроме фигурных скобок, конечно) есть макрос. Первая строка устанавливает объявление функции, как ожидается от кода PP; строка 3 устанавливает объявления переменных для стека аргументов и цели, возвращаемого значения операции. Строка 4 пытается определить, перегружен ли оператор сложения; если да, то вызывается соответствующая подпрограмма.
Строка 6 — ещё одно объявление переменной — все объявления переменных начинаются с d — которая извлекает из вершины стека аргументов два NV (отсюда nn) и помещает их в переменные right и left, откуда и rl. Это два операнда оператора сложения. Далее мы вызываем SETn для установки NV возвращаемого значения в результат сложения двух значений. После этого мы возвращаемся — макрос RETURN гарантирует правильную обработку возвращаемого значения, и мы передаём следующий оператор для выполнения обратно в основной цикл.
Большинство этих макросов описаны в perlapi, а некоторые из более важных — также в perlxs. Обратите особое внимание на "Обзор и PERL_IMPLICIT_CONTEXT" в perlguts для получения информации о макросах [pad]THX_?.
Дополнительное чтение
Для получения дополнительной информации об интерпретации Perl см. документы по "Внутреннее устройство и интерфейс языка C" в perl.
© 1993–2020 Larry Wall and others
Licensed under the GNU General Public License version 1 or later, or the Artistic License.
The Perl logo is a trademark of the Perl Foundation.
https://perldoc.perl.org/5.32.0/perlinterp