perlinterp
СОДЕРЖАНИЕ
- ИМЯ
- ОПИСАНИЕ
- ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА
- ДЕРЕВЬЯ ОПЕРАЦИЙ
- СТЕКИ
- МИЛЛИОНЫ МАКРОСОВ
- ДОПОЛНИТЕЛЬНАЯ ЛИТЕРАТУРА
ИМЯ
perlinterp - Обзор интерпретатора Perl
ОПИСАНИЕ
Этот документ предоставляет обзор работы интерпретатора Perl на уровне кода C, вместе с указателями на соответствующие файлы исходного кода C.
ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА
Работа интерпретатора состоит из двух основных этапов: компиляции кода в внутреннее представление, или байткод, и затем его выполнения. "Компилированный код" в perlguts объясняет, как происходит этап компиляции.
Вот краткий обзор работы Perl:
Запуск
Действие начинается в perlmain.c. (или miniperlmain.c для miniperl) Это код очень высокого уровня, помещающийся на одном экране, и он похож на код, который можно найти в perlembed; большая часть реальных действий происходит в perl.c
perlmain.c генерируется из miniperlmain.c во время компиляции с помощью ExtUtils::Miniperl, поэтому вы должны сделать 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 просто добавляют структуру блока CXt_SUB или CXt_EVAL в стек контекста, которые содержат адрес операции, следующей за вызовом подпрограммы или eval. Затем они возвращают первую операцию этого вызова подпрограммы или блока eval, и выполнение продолжается в этом вызове или блоке. Позже, операция pp_leavesub или pp_leavetry извлекает операцию возврата из неё и возвращает её.
Обработка исключений
Обработка исключений Perl (т. е. die и т. д.) основана на низкоуровневых функциях setjmp()/longjmp() библиотеки C. В основном они предоставляют способ захвата текущих регистров PC и SP процессора и последующего их восстановления: т. е. выполнение longjmp() продолжается в той точке кода, где был выполнен предыдущий setjmp(), при этом всё, что находится выше в стеке C, теряется. (Вот почему код всегда должен сохранять значения с помощью SAVE_FOO вместо автоматических переменных.)
Ядро Perl оборачивает setjmp() и longjmp() в макросы JMPENV_PUSH и JMPENV_JUMP. Операция добавления, а также установка setjump(), сохраняет некоторые временные состояния в структуре, локальной для текущей функции (выделенной с помощью dJMPENV). В частности, она хранит указатель на предыдущую структуру JMPENV и обновляет PL_top_env для указания на самую новую, образуя цепочку состояний JMPENV. Как операция добавления, так и переход могут выводить отладочную информацию под perl -Dl.
Основное правило внутренней части perl заключается в том, что все выходы интерпретатора выполняются с помощью JMPENV_JUMP(). В частности:
-
уровень 2: выход perl() и внутренний my_exit()
Они разворачивают все стеки, затем выполняют JMPENV_JUMP(2).
-
уровень 3: die() perl() и внутренний croak()
Если в настоящее время находится внутри eval, эти значения выталкивают стек контекста до ближайшей рамки
CXt_EVAL, устанавливают$@соответственно, устанавливаютPL_restartopна операцию, следующую за eval, связанной с этой рамкой, затем выполняют JMPENV_JUMP(3).В противном случае сообщение об ошибке печатается в
STDERR, затем оно обрабатывается как выход: разворачиваются все стеки и выполняется JMPENV_JUMP(2). -
уровень 1: не используется
JMPENV_JUMP(1) в настоящее время не используется, кроме как в perl_run().
-
уровень 0: нормальное возвращение.
Значение нуль используется для нормального возврата из JMPENV_PUSH()
Таким образом, интерпретатор Perl ожидает, что в любое время имеется соответствующий JMPENV_PUSH (и в соответствующей позиции в стеке вызовов процессора), который может перехватывать и обрабатывать переход с 2 или 3 значениями; и в случае 3, начать новый цикл runops для выполнения PL_restartop и всех оставшихся операций (как будет объяснено вскоре).
Все точки входа в интерпретатор Perl предоставляют такую возможность. Например, perl_parse(), perl_run() и call_sv(cv, G_EVAL) все содержат что-то подобное по схеме:
{
dJMPENV;
JMPENV_PUSH(ret);
switch (ret) {
case 0: /* normal return from JMPENV_PUSH() */
redo_body:
CALLRUNOPS(aTHX);
break;
case 2: /* caught longjmp(2) - exit / die */
break;
case 3: /* caught longjmp(3) - eval { die } */
PL_op = PL_restartop;
goto redo_body;
}
JMPENV_POP;
} Цикл runops, такой как Perl_runops_standard() (настроенный с помощью CALLRUNOPS()), по своей сути — просто:
while ((PL_op = PL_op->op_ppaddr(aTHX))) { 1; } который вызывает функцию pp(), связанную с каждой операцией, полагаясь на то, что она вернёт указатель на следующую выполняемую операцию.
Помимо установки перехватов в точках входа в интерпретатор Perl, вы можете ожидать, что perl также выполнит JMPENV_PUSH() в местах, таких как pp_entertry(), непосредственно перед выполнением некоторых ловимых операций. Фактически, perl обычно этого не делает. Недостаток заключается в том, что со вложенным или рекурсивным кодом, таким как:
sub foo { my ($i) = @_; return if $i < 0; eval { foo(--$i) } } Затем стек C быстро переполнится парами записей, подобными
...
#N+3 Perl_runops()
#N+2 Perl_pp_entertry()
#N+1 Perl_runops()
#N Perl_pp_entertry()
... Вместо этого perl размещает свои защитные механизмы в вызывающих циклах runops. Затем, сколько угодно вложенных вызовов подпрограмм и eval можно вызвать все в одном цикле runops. Если произойдёт исключение, управление передаётся вызывающему цикл, который сразу же запускает новый цикл с PL_restartop как следующей операцией для вызова.
Итак, в обычной работе, где есть несколько вложенных evals, будет несколько CXt_EVAL записей в стеке контекста, но только один цикл runops, защищённый одним JMPENV_PUSH. Каждый перехваченный eval вытолкнет следующий CXt_EVAL из стека, установит PL_restartop, затем выполнит longjmp() обратно в perl_run() и продолжит.
Однако, операции иногда выполняются внутри внутреннего цикла runops, например, в коде tie, sort или overload. В этом случае, что-то вроде
sub FETCH { eval { die }; .... } если не обработано специально, вызовет longjmp() прямо обратно в охрану в perl_run(), удалив обе петли runops — что явно неправильно. Один из способов избежать этого — для кода tie выполнить JMPENV_PUSH перед выполнением FETCH во внутреннем цикле runops, но по соображениям эффективности Perl просто временно устанавливает флаг с использованием CATCH_SET(TRUE). Этот флаг предупреждает все последующие require, entereval или entertry операции, что вызывающий объект больше не обещает перехватывать любые поднятые исключения от своего имени.
Эти операции проверяют этот флаг, и если он истинен, они (через docatch()) выполняют JMPENV_PUSH и запускают новый цикл runops для выполнения кода, а не с текущим циклом.
Вследствие этого, по выходе из блока eval в FETCH выше, выполнение кода после блока всё ещё продолжается во внутреннем цикле (то есть том, что был создан pp_entertry()). Чтобы избежать путаницы, если затем возникает другое исключение, docatch() сравнивает уровень JMPENV с CXt_EVAL с PL_top_env, и если они отличаются, просто повторно бросает исключение. Таким образом, любые внутренние циклы удаляются, и исключение будет обработано должным образом уровнем, который его ожидает.
Вот пример.
1: eval { tie @a, 'A' };
2: sub A::TIEARRAY {
3: eval { die };
4: die;
5: } Для выполнения этого кода вызывается perl_run(), который выполняет JMPENV_PUSH(), затем входит в цикл runops. Этот цикл выполняет операции entereval и tie в строке 1, при этом entereval добавляет CXt_EVAL в стек контекста.
pp_tie() выполняет CATCH_SET(TRUE), затем запускает второй цикл runops для выполнения тела TIEARRAY(). Когда цикл выполняет операцию entertry в строке 3, CATCH_GET() истинно, поэтому pp_entertry() вызывает docatch(), который выполняет JMPENV_PUSH и запускает третий цикл runops, который перезапускает pp_entertry(), затем выполняет операцию die. На этом этапе стек вызовов C выглядит так:
#10 Perl_pp_die()
#9 Perl_runops() # runops loop 3
#8 S_docatch() # JMPENV level 2
#7 Perl_pp_entertry()
#6 Perl_runops() # runops loop 2
#5 Perl_call_sv()
#4 Perl_pp_tie()
#3 Perl_runops() # runops loop 1
#2 S_run_body()
#1 perl_run() # JMPENV level 1
#0 main() а стеки контекста и данных, как показано perl -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() удаляет первый CXt_EVAL из стека контекста, устанавливает PL_restartop из него, выполняет JMPENV_JUMP(3), и управление возвращается к уровню JMPENV установленному в docatch(). Это затем запускает ещё один цикл runops третьего уровня, который выполняет операции nextstate, pushmark и die из строки 4. В момент вызова второго pp_die() стек вызовов C выглядит точно так же, как выше, даже если мы больше не находимся внутри вложенного eval. Однако, стек контекста теперь выглядит так, то есть с верхним CXt_EVAL удалённым:
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 удаляет стек контекста до CXt_EVAL, оставив его как:
STACK 0: MAIN
CX 0: BLOCK => Как обычно, PL_restartop извлекается из CXt_EVAL, и выполняется JMPENV_JUMP(3), который возвращает стек C обратно в docatch():
#8 S_docatch() # JMPENV level 2
#7 Perl_pp_entertry()
#6 Perl_runops() # runops loop 2
#5 Perl_call_sv()
#4 Perl_pp_tie()
#3 Perl_runops() # runops loop 1
#2 S_run_body()
#1 perl_run() # JMPENV level 1
#0 main() В этом случае, поскольку уровень JMPENV , записанный в CXt_EVAL, отличается от текущего, docatch() просто выполняет JMPENV_JUMP(3) для повторной отправки исключения, и стек C разворачивается до:
#1 perl_run() # JMPENV level 1
#0 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 — это макрос, который очищает данные с признаком «загрязнённые», если режим «загрязнённые данные» включён.
AVs и HVs более сложные, но SVs — это, пожалуй, самый распространённый тип переменных, используемых в программе. После того, как мы увидели, как мы манипулируем ими, перейдём к тому, как построено дерево операций.
ДЕРЕВЬЯ ОПЕРАЦИЙ
Во-первых, что такое дерево операций? Дерево операций — это анализируемое представление вашей программы, как мы видели в нашем разделе об анализе, и это последовательность операций, которые 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, — это порядок выполнения: каждый 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)); } Это создаёт новый бинарный op и передаёт ему несколько переменных. Переменные ссылаются на токены: $1 — первый токен во входных данных, $2 — второй, и так далее — думайте о обратных ссылках регулярных выражений. $$ — op, возвращаемый этим сокращением. Итак, мы вызываем newBINOP для создания нового бинарного оператора. Первый параметр newBINOP, функции в op.c, — тип op. Это оператор сложения, поэтому мы хотим, чтобы тип был ADDOP. Мы могли бы указать это напрямую, но оно находится прямо там, как второй токен во входных данных, поэтому мы используем $2. Второй параметр — флаги op: 0 означает «ничего особенного». Затем вещи для сложения: левая и правая часть нашего выражения в скалярном контексте.
Функции, создающие op, имеющие имена вроде newUNOP и newBINOP, вызывают функцию «проверки», связанную с каждым типом op, перед возвращением op. Функции проверки могут изменять op по своему усмотрению и даже заменить его совершенно новым. Эти функции определены в op.c и имеют префикс Perl_ck_. Вы можете узнать, какая функция проверки используется для определенного типа op, посмотрев в 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 } Один из особенно интересных аспектов заключается в том, что если у op нет детей (т. е. readline() или <>) op освобождается и заменяется совершенно новым, который ссылается на *ARGV (строки 12—16).
СТЭКИ
Когда perl выполняет что-то вроде addop, как он передаёт свои результаты следующему op? Ответ — через использование стэков. 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. Обратите особое внимание на "Фоновый и MULTIPLICITY" в perlguts для получения информации о [pad]THX_? макросах.
ДОПОЛНИТЕЛЬНОЕ ЧТЕНИЕ
Для получения дополнительной информации об внутренней реализации Perl, см. документы, перечисленные в "Внутренняя реализация и интерфейс языка C" в perl.
© 1993–2023 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.38.0/perlinterp