Взгляд на расширение PHP и скелет расширения¶
Здесь мы подробно рассмотрим, как выглядит расширение PHP и как сгенерировать скелет с помощью некоторых инструментов. Это позволит нам взять код скелета и доработать его, вместо того чтобы создавать каждый необходимый элемент вручную с нуля.
Мы также расскажем, как следует/можно организовать файлы вашего расширения, как движок их загружает, и в целом всё, что нужно знать о расширении PHP.
Как движок загружает расширения¶
Вы помните главу о сборке расширений PHP, так что вы знаете, как скомпилировать/собрать и установить его.
Вы можете собирать статически скомпилированные расширения — это расширения, которые являются частью ядра PHP и сплавлены с ним. Они представлены не файлами .so, а объектами .o, которые линкуются в итоговый исполняемый файл PHP (ELF). Таким образом, такие расширения не могут быть отключены, они являются частью тела кода исполняемого файла PHP: они здесь, внутри, что бы вы ни говорили и делали. Некоторые расширения обязаны быть статически собраны, например ext/core, ext/standard, ext/spl и ext/mysqlnd (список неполный).
Список статически скомпилированных расширений можно найти, посмотрев в файл main/internal_functions.c, который
генерируется в процессе компиляции PHP. Этот шаг подробно описан в
главе о сборке PHP.
Кроме того, вы можете собирать динамически загружаемые расширения. Это те самые знаменитые файлы extension.so, которые появляются на свет в конце процесса отдельной компиляции. Динамически загружаемые расширения дают преимущество подключаемости и отключаемости во время выполнения и не требуют перекомпиляции всего PHP для включения или отключения. Недостаток в том, что время запуска процесса PHP увеличивается, когда ему нужно загружать файлы .so. Но это вопрос миллисекунд, и вы не особо от этого страдаете.
Другой недостаток динамически загружаемых расширений — порядок их загрузки. Некоторым расширениям может требоваться, чтобы перед ними были загружены другие расширения. Хотя это не лучшая практика, мы увидим, что система расширений PHP позволяет объявлять зависимости, чтобы управлять таким порядком, но зависимости обычно являются плохой идеей и их следует избегать.
И последнее: статически скомпилированные расширения PHP запускаются раньше динамически собранных. Это означает, что
их MINIT() вызывается раньше, чем MINIT() файлов extensions.so.
Когда PHP запускается, он быстро переходит к разбору своих различных INI-файлов. Если они присутствуют, в них может быть объявлена загрузка расширений с помощью строки вида “extension=some_ext.so”. Затем PHP собирает каждое расширение, разобранное из конфигурации INI, и пытается загрузить их в том же порядке, в котором они были добавлены в INI-файл, если только некоторые расширения не объявили зависимости (которые в этом случае будут загружены раньше).
Note
Если вы используете менеджер пакетов операционной системы, вы могли заметить, что пакетировщики обычно называют файлы своих расширений с числовым префиксом, например 00_ext.ini, 01_ext.ini и т.д. Это делается, чтобы управлять порядком загрузки расширений. Некоторым редким расширениям требуется конкретный порядок запуска. Напомним, что зависимость от загрузки других расширений перед вашим — плохая идея.
Для загрузки расширений используется libdl и его функции dlopen()/dlsym().
Ищется символ get_module() — это означает, что ваше расширение должно экспортировать его, чтобы быть загруженным.
Обычно это так и есть, поскольку если вы использовали скрипт-скелет (мы рассмотрим его через минуту), то он
сгенерировал код с использованием макроса ZEND_GET_MODULE(your_ext), который выглядит так:
#define ZEND_GET_MODULE(name) \
BEGIN_EXTERN_C()\
ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }\
END_EXTERN_C()
Как видите, этот макрос при использовании объявляет глобальный символ: функцию get_module(), которая будет вызвана движком при попытке загрузить ваше расширение.
Note
Исходный код, который PHP использует для загрузки расширений, расположен в ext/standard/dl.c
Что такое расширение PHP?¶
Расширение PHP, которое не следует путать с Zend-расширением, определяется с помощью
структуры zend_module_entry:
struct _zend_module_entry {
unsigned short size; /*
unsigned int zend_api; * STANDARD_MODULE_HEADER
unsigned char zend_debug; *
unsigned char zts; */
const struct _zend_ini_entry *ini_entry; /* Unused */
const struct _zend_module_dep *deps; /* Module dependencies */
const char *name; /* Module name */
const struct _zend_function_entry *functions; /* Module published functions */
int (*module_startup_func)(INIT_FUNC_ARGS); /*
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); *
int (*request_startup_func)(INIT_FUNC_ARGS); * Lifetime functions (hooks)
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); *
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); */
const char *version; /* Module version */
size_t globals_size; /*
#ifdef ZTS *
ts_rsrc_id* globals_id_ptr; *
#else * Globals management
void* globals_ptr; *
#endif *
void (*globals_ctor)(void *global); *
void (*globals_dtor)(void *global); */
int (*post_deactivate_func)(void); /* Rarely used lifetime hook */
int module_started; /* Has module been started (internal usage) */
unsigned char type; /* Module type (internal usage) */
void *handle; /* dlopen() returned handle */
int module_number; /* module number among others */
const char *build_id; /* build id, part of STANDARD_MODULE_PROPERTIES_EX */
};
Первые четыре параметра уже были объяснены в
главе о сборке расширений. Они обычно заполняются с помощью макроса
STANDARD_MODULE_HEADER.
Вектор ini_entry фактически не используется. Вы регистрируете INI-записи с помощью
специальных макросов.
Затем вы можете объявить зависимости — это означает, что вашему расширению может требоваться загрузка другого
расширения перед ним, либо оно может объявить конфликт с другими расширениями. Это делается с помощью поля deps.
На практике это используется очень редко, и в целом создавать зависимости между расширениями PHP считается плохой
практикой.
После этого вы объявляете name. Тут нечего добавить — это имя вашего расширения (которое может отличаться от
имени его собственного файла .so). Учтите, что имя чувствительно к регистру в большинстве операций, мы советуем
использовать что-то короткое, в нижнем регистре, без пробелов (чтобы немного облегчить себе жизнь).
Далее идёт поле functions. Это указатель на некоторые функции PHP, которые расширение хочет зарегистрировать в
движке. Об этом мы говорили в отдельной главе.
Продолжая, идут 5 хуков жизненного цикла. Смотрите их отдельную главу.
Ваше расширение может опубликовать номер версии в виде char *, используя поле version. Это поле считывается
только как часть информации о вашем расширении — функцией phpinfo() или через Reflection API как
ReflectionExtension::getVersion().
Далее мы видим множество полей, связанных с глобальными переменными. Управлению глобальными переменными посвящена отдельная глава.
Наконец, завершающие поля обычно являются частью макроса STANDARD_MODULE_PROPERTIES, и вам не нужно
взаимодействовать с ними вручную. Движок присвоит вам module_number для своего внутреннего учёта, а тип
расширения будет установлен в MODULE_PERSISTENT. Он может быть и MODULE_TEMPORARY, если бы ваше расширение
было загружено с помощью пользовательской функции PHP dl(), но такой сценарий очень редок, не работает с каждым
SAPI, и временные расширения обычно создают множество проблем в движке.
Генерация скелета расширения с помощью скриптов¶
Теперь посмотрим, как сгенерировать скелет расширения, чтобы вы могли начать новое расширение с минимальным содержимым и структурой, которые не придётся создавать вручную с нуля.
Скрипт-генератор скелета расположен в php-src/ext/ext_skel, а структура, которую он использует как шаблон, хранится в php-src/ext/skeleton.
Note
Скрипт и структура немного меняются по мере развития версий PHP.
Вы можете изучить эти скрипты, чтобы понять, как они работают, но базовое использование выглядит так:
> cd /tmp
/tmp> /path/to/php/ext/ext_skel --skel=/path/to/php/ext/skeleton --extname=pib
[ ... generating ... ]
/tmp> tree pib/
pib/
├── config.m4
├── config.w32
├── CREDITS
├── EXPERIMENTAL
├── php_pib.h
├── pib.c
├── pib.php
└── tests
└── 001.phpt
/tmp>
Вы видите, что сгенерировалась очень простая и минимальная структура. В главе о сборке расширений вы узнали, что файлы вашего расширения, которые нужно компилировать, должны быть объявлены в config.m4. Скелет сгенерировал только файл <your-ext-name>.c. В нашем примере расширение называлось “pib”, так что мы получили файл pib.c, и нам нужно раскомментировать строку –enable-pib в config.m4, чтобы он компилировался.
К каждому C-файлу (обычно) прилагается заголовочный файл. Здесь структура такая: php_<your-ext-name>.h, то есть для нас php_pib.h. Не меняйте это имя — система сборки ожидает именно такое соглашение об именовании заголовочного файла.
Вы можете видеть, что также была сгенерирована минимальная структура тестов.
Откроем pib.c. Там всё закомментировано, так что писать придётся не так уж много строк.
В целом мы видим, что символ модуля, необходимый движку для загрузки нашего расширения, публикуется здесь:
#ifdef COMPILE_DL_PIB
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(pib)
#endif
Макрос COMPILE_DL_<YOUR-EXT-NAME> определяется, если вы передаёте флаг –enable-<my-ext-name> скрипту
configure. Мы также видим, что в режиме ZTS указатель локального хранилища TSRM определяется как часть макроса
ZEND_TSRMLS_CACHE_DEFINE().
После этого больше ничего не остаётся сказать, поскольку всё закомментировано и должно быть вам понятно.
Новая эра генератора скелета расширения¶
Начиная с этого коммита генератор скелета расширения приобрёл новый стиль:
Теперь он будет работать на Windows без Cygwin и прочих сложностей. Он больше не включает способ генерации XML-документации (утилиты PHP-документации уже содержат инструменты для этого в svn, в phpdoc/doc-base), и он больше не поддерживает заготовки функций (function stubs).
и вот доступные опции:
php ext_skel.php --ext <name> [--experimental] [--author <name>]
[--dir <path>] [--std] [--onlyunix]
[--onlywindows] [--help]
--ext <name> The name of the extension defined as <name>
--experimental Passed if this extension is experimental, this creates
the EXPERIMENTAL file in the root of the extension
--author <name> Your name, this is used if --header is passed and
for the CREDITS file
--dir <path> Path to the directory for where extension should be
created. Defaults to the directory of where this script
lives
--std If passed, the standard header and vim rules footer used
in extensions that is included in the core, will be used
--onlyunix Only generate configure scripts for Unix
--onlywindows Only generate configure scripts for Windows
--help This help
Новый генератор скелета расширения сгенерирует скелет с тремя фиксированными функциями, вы можете определить любые другие функции и изменить конкретное тело как захотите.
Note
Помните, что новый ext_skel больше не поддерживает proto-файлы.
Публикация API¶
Если мы откроем заголовочный файл, то увидим такие строки:
#ifdef PHP_WIN32
# define PHP_PIB_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
# define PHP_PIB_API __attribute__ ((visibility("default")))
#else
# define PHP_PIB_API
#endif
Эти строки определяют макрос с именем PHP_<EXT-NAME>_API (для нас PHP_PIB_API), который раскрывается в
пользовательский атрибут GCC
visibility(“default”).
В C вы можете указать линкеру скрыть все символы итогового объекта. Именно это сделано в PHP для каждого символа, а не только для статических (которые по определению не публикуются).
Warning
Стандартная строка компиляции PHP указывает компилятору скрывать все символы и не экспортировать их.
После этого вам нужно “раскрыть” символы, которые вы хотите опубликовать в своём расширении, чтобы их можно было использовать в других расширениях или других частях итогового файла ELF.
Note
Напомним, что вы можете прочитать опубликованные и скрытые символы ELF с помощью nm в Unix.
Мы не можем объяснить эти концепции подробно здесь, возможно, вам помогут такие ссылки:
Итак, в целом, если вы хотите, чтобы какой-то ваш C-символ был публично доступен другим расширениям, вам следует
объявить его с помощью специального макроса PHP_PIB_API. Традиционный сценарий использования — публикация
символов классов (тип zend_class_entry*), чтобы другие расширения могли подключаться к опубликованным вами
классам и заменять некоторые их обработчики.
Note
Обратите внимание, что это работает только с традиционным PHP. Если вы используете PHP из дистрибутива Linux, такие сборки патчатся, чтобы разрешать символы при загрузке, а не лениво, так что этот трюк не работает.