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, например, код tie, sort или overload. В этом случае что-то вроде
sub FETCH { eval { die } } привело бы к длинному переходу прямо обратно в блок-охранитель в perl_run, удаляя оба цикла runops, что явно неверно. Один способ избежать этого — для кода tie выполнить 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 в памяти. Сами SV являются очень простыми структурами, но они содержат указатель на более сложную структуру. В данном случае это 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 - это макрос, который очищает заражённые данные, если режим заражения включён.
AVs и HVs сложнее, но SV - это, безусловно, самый распространённый тип переменных. Узнав, как мы ими манипулируем, перейдём к тому, как строится дерево op.
ДЕРЕВЬЯ OP
Во-первых, что такое дерево op? Дерево op - это разобранное представление вашей программы, как мы видели в разделе о парсинге, и это последовательность операций, через которые Perl проходит, чтобы выполнить вашу программу, как мы видели в "Выполнение".
Op - это основная операция, которую может выполнить Perl: все встроенные функции и операторы являются op, и есть ряд op, которые обрабатывают концепции, необходимые интерпретатору - вход и выход из блока, завершение оператора, извлечение переменной и так далее.
Дерево op соединяется двумя способами: можно представить, что в нём есть два "маршрута", два порядка, в которых можно пройти по дереву. Во-первых, порядок парсинга отражает, как парсер понял код, а во-вторых, порядок выполнения указывает Perl, в каком порядке выполнять операции.
Самый простой способ исследовать дерево op - остановить Perl после завершения парсинга и попросить его вывести дерево. Именно это делают компиляционные бэкэнды 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 - это null-оператор: он ничего не делает. Зачем он там? Если вы видите null-оператор, это признак того, что что-то было оптимизировано после парсинга. Как мы упомянули в "Оптимизации", на стадии оптимизации иногда две операции объединяются в одну, например, при извлечении скалярной переменной. Когда это происходит, вместо переписывания дерева op и очистки висячих указателей проще заменить избыточную операцию на null-оператор. Изначально дерево выглядело бы так:
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 op, а операции, которые необходимо выполнить для каждого оператора, являются дочерними элементами nextstate. enter - это единственный op, который служит маркером.
Вот как Perl анализировал программу сверху вниз:
Program
|
Statement
|
=
/ \
/ \
$a +
/ \
$b $c Однако невозможно выполнить операции в этом порядке: вы должны найти значения $b и $c перед тем, как сложить их, например. Таким образом, другой поток, который проходит через дерево op, - это порядок выполнения: каждый op имеет поле op_next, которое указывает на следующий op для выполнения, поэтому, следуя этим указателям, мы узнаём, как 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)); } Если вы не знакомы с чтением грамматик BNF, вот как это работает: вам подаются определённые вещи лексическим анализатором, которые обычно заканчиваются заглавными буквами. 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 для Perl; строка 3 устанавливает объявления переменных для стека аргументов и целевого значения, возвращаемого операцией. Строка 4 пытается определить, перегружен ли оператор сложения; если это так, вызывается соответствующая подпрограмма.
Строка 6 — это еще одно объявление переменной — все объявления переменных начинаются с d — которое извлекает из вершины стека аргументов два NV (отсюда nn) и помещает их в переменные right и left, откуда rl. Это два операнда оператора сложения. Затем мы вызываем SETn для установки NV возвращаемого значения в результат сложения двух значений. После этого мы возвращаемся — макрос RETURN гарантирует правильную обработку нашего возвращаемого значения, и мы передаем следующий оператор для выполнения обратно в основной цикл.
Большинство этих макросов описаны в perlapi, а некоторые из наиболее важных — в perlxs тоже. Обратите особое внимание на "Фон и MULTIPLICITY" в perlguts для информации о макросах [pad]THX_?.
ДОПОЛНИТЕЛЬНОЕ ЧТЕНИЕ
Для получения дополнительной информации о внутренних компонентах Perl, обратитесь к документам, перечисленным в "Внутренняя реализация и интерфейс языка C" в perl.
© 1993–2021 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.36.0/perlinterp