perlinterp
СОДЕРЖАНИЕ
- НАЗВАНИЕ
- ОПИСАНИЕ
- ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА
- ДЕРЕВЬЯ ОПЕРАЦИЙ
- СТЕКИ
- МИЛЛИОНЫ МАКРОС
- ДОПОЛНИТЕЛЬНАЯ ЛИТЕРАТУРА
НАЗВАНИЕ
perlinterp - Обзор интерпретатора Perl
ОПИСАНИЕ
В данном документе представлен обзор работы интерпретатора Perl на уровне кода C, а также ссылки на соответствующие файлы исходного кода C.
ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА
Работа интерпретатора состоит из двух основных этапов: компиляции кода во внутреннее представление, или байт-код, и затем его выполнения. "Компилированный код" в perlguts объясняет, как происходит этап компиляции.
Вот краткое описание работы Perl:
Запуск
Действие начинается в файле perlmain.c. (или miniperlmain.c для miniperl) Это код очень высокого уровня, достаточно компактный, и он напоминает код, встречающийся в perlembed; большая часть реальных действий происходит в файле perl.c
perlmain.c генерируется ExtUtils::Miniperl из miniperlmain.c во время сборки, поэтому вы должны следовать этому.
Сначала 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 } } вызвало бы longjmp прямо обратно к стражнику в 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 имеет значение true, поэтому 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 не равно null, run_body запускает новый цикл runops, и выполнение продолжается.
ТИПЫ ВНУТРЕННИХ ПЕРЕМЕННЫХ
Вы к этому моменту, должно быть, уже ознакомились со статьёй perlguts, которая описывает внутренние типы переменных Perl: SV, HV, AV и прочие. Если нет, сделайте это сейчас.
Эти переменные используются не только для представления переменных Perl-пространства, но и любых констант в коде, а также некоторых полностью внутренних структур Perl. Например, таблица символов — это обычный хеш Perl. Ваш код представлен как SV при его чтении в парсер; любые вызываемые вами файлы программ открываются через обычные Perl-дескрипторы файлов и так далее.
Основной модуль Devel::Peek позволяет нам исследовать SV из 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, это read-only 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.
AV и HV более сложные, но SV — это, безусловно, самый распространённый тип переменных. Увидев, как мы манипулируем ими, давайте перейдём к тому, как построено дерево операций.
ДЕРЕВЬЯ ОПЕРАЦИЙ
Во-первых, что такое дерево операций? Дерево операций — это обработанное представление вашей программы, как мы видели в разделе о парсинге, и это последовательность операций, которую Perl выполняет для исполнения вашей программы, как мы видели в "Выполнение".
Операция — это основная операция, которую может выполнить Perl: все встроенные функции и операторы — это операции, а также ряд операций, которые обрабатывают концепции, необходимые интерпретатору — вход и выход из блока, завершение оператора, извлечение переменной и так далее.
Дерево операций соединяется двумя способами: можно представить, что через него проходят две «дороги», два порядка обхода дерева. Во-перых, порядок парсинга отражает то, как парсер понял код, а во-вторых, порядок выполнения сообщает Perl, в каком порядке выполнять операции.
Самый простой способ проверить дерево операций — это остановить 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 — это нулевая операция: она ничего не делает. Зачем она здесь? Если вы видите нулевую операцию, это означает, что что-то было оптимизировано после парсинга. Как мы упоминали в "Оптимизации", на этапе оптимизации иногда две операции сводятся к одной, например, при извлечении скалярной переменной. В этом случае, вместо переписывания дерева операций и очистки висячих указателей, проще просто заменить ненужную операцию на нулевую операцию. Изначально дерево выглядело так:
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_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 создаёт эти деревья операций в процессе парсинга, можно проследить, изучив 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 } Каждая строка здесь (кроме фигурных скобок, конечно) содержит макрос. Первая строка устанавливает объявление функции так, как ожидает Perl для кода 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–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.34.0/perlinterp