Взгляд на расширение 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, такие сборки патчатся, чтобы разрешать символы при загрузке, а не лениво, так что этот трюк не работает.