Pull to refresh

Создание расширений PHP: Введение в PHP и Zend

Reading time20 min
Views32K
Original author: Sara Golemon
Перевод статьи Sara Golemon «Extension Writing Part I: Introduction to PHP and Zend».



Введение


Если вы читаете данную статью, скорее всего у вас есть некоторый интерес к созданию расширений для языка PHP. Если же нет… возможно, когда вы прочтёте эту статью, то обнаружите в себе этот интерес, не смотря на то, что вы о нём даже не подозревали!

Материал, изложенный в данной статье, подразумевает знакомство как с самим языком PHP, так и с языком, на котором написан интерпретатор PHP: C.

Начнём с того, что определим причины, по которым вы хотите написать расширение для PHP:
  1. Существование какой-нибудь библиотеки или специфичного вызова ОС, который не может быть сделан из PHP напрямую из-за уровня абстракции принятого в языке;
  2. Вы хотите заставить PHP работать нестандартным способом;
  3. У вас уже есть решение, написанное на PHP, но вы знаете, что оно может быть быстрее, компактней и потреблять меньше памяти в процессе работы;
  4. У вас есть особенный код, который вы хотели бы продать. Однако, важно, что бы покупатель мог запускать Ваш код, но не смотреть исходники.

Все перечисленные выше причины являются вполне адекватными, но, создавая расширение, прежде всего вам стоит понимать, что это в первую очередь расширение.



Что такое Расширение?


Если вам приходилось использовать PHP, то вы использовали и расширения. За небольшим исключением каждая доступная для использования функция в языке PHP сгруппирована в то или иное расширение. Основная часть функций (более 400) входит в состав стандартного расширения. Исходные коды PHP распространяются с порядка 86 расширениями, имеющими примерно по 30 функций в каждом. Посчитав, получим где-то 2500 функций в сумме. Если этого не достаточно, репозитарий PECL предлагает свыше 100 дополнительных расширений, ещё больше можно найти на бескрайних просторах интернета.

«Что же, с учётом всего этого множества функций, живущих в расширениях, тогда остаётся вне расширений?» — спросите вы. «Что расширения расширяют? Что такое ядро PHP

Ядро PHP реализовано в виде 2-х отдельных частей. Техническая часть языка представлена в виде Zend Engine (ZE). ZE отвечает за преобразование понятного для человека скрипта в понятные для компьютера токены (tokens), после чего выполняет их. Кроме того, ZE отвечает за управление памятью, область видимости переменных, обработку вызова функций.
Второй частью ядра является то, что непосредственно называется «ядром» (the PHP core). Оно отвечает за взаимодействие со слоем SAPI (Server Application Programming Interface, интерфейс взаимодействия PHP с другим серверным ПО — CLI, CGI, Apache и так далее). Кроме того, ядро реализует обобщённый слой контроля для проверок safe_mode и open_basedir (данные фичи объявлены depricated с версии 5.3), так же, как и слой потоков, который ассоциирует файловые и сетевые I/O операции с функциями fopen, fread и fwrite.



Жизненный цикл


Когда происходит запуск заданного SAPI (например, при запуске сервера Apache по команде /usr/local/apache/bin/apachectl start), PHP начинает свою работу с запуска подсистемы ядра. К концу процедуры запуска он загружает код каждого расширения и вызывает его функцию Module Initialization (MINIT). Это даёт каждому расширению возможность инициализировать внутренние переменные, выделить память под ресурсы, зарегистрировать обработчики ресурсов и свои функции в ZE, что бы при вызове каким-нибудь скриптом функции этого расширения ZE знал, какой код ему выполнять.

Далее PHP ждёт от слоя SAPI запроса на обработку страницы. В случае CGI или CLI SAPI это происходит незамедлительно и только один раз. В случае SAPI Apache, IIS или другого полноценного web-сервера запрос на обработку страницы происходит каждый раз при запросе (возможно конкурентном) страницы удалённым пользователем. Однако, вне зависимости от того, каким образом пришёл запрос, его обработка начинается с того, что ядро PHP просит ZE настроить окружающую среду для запуска скрипта, после чего вызывает функцию Request Initialization (RINIT) для каждого расширения. RINIT даёт расширениям возможность настроить специфичные переменные окружения, выделить память для специфичных ресурсов запроса и выполнить другие задания. Наглядным примером функции RINIT в действии может служить расширение session, в котором при включенной настройке session.auto_start функция RINIT автоматически вызывает исполнение функции session_start и инициализирует переменную $_SESSION.

После того, как запрос инициализирован, ZE транслирует PHP скрипт в токены, а затем в опкоды (opcodes), которые он может выполнить. Если какой-нибудь из этих опкодов запрашивает вызов функции из расширения, ZE формирует аргументы для вызова этой функции и временно передаёт ей управление до её завершения.

После того как скрипт завершил своё выполнение, PHP вызывает функцию Request Shutdown (RSHUTDOWN) для каждого расширения, что выполнить все необходимые для завершения чистки (например, сохранение сессионных переменных на диск). Следующим шагом ZE выполняет процесс чистки (также известный как сборка мусора), который фактически выполняет метод unset для каждой переменной, использованной в выполненном скрипте (начиная с PHP 5.3 механизм сборки мусора значительно улучшен).

Завершив обработку запроса, PHP ждёт от SAPI либо запроса на обработку другого скрипта, либо сигнала на завершение. В случае CGI или CLI SAPI «следующий запрос» невозможен, поэтому SAPI инициализирует завершение работы PHP незамедлительно. В процессе завершения PHP перебирает все расширения и для каждого вызывает функцию Module Shutdown (MSHUTDOWN), после чего завершает свою собственную подсистему ядра.

Этот процесс поначалу может показаться немного запутанным, но, погрузившись в работу над расширениями, вы постепенно прочувствуете его.



Выделение памяти


Для того, что бы избежать утечек памяти в плохо написанных расширениях, ZE использует свой собственный внутренний механизм управления памятью, основанный на дополнительном флаге для определения времени жизни данных. Постоянное (persistent) выделение памяти означает, что память будет выделена более, чем на время обработки запроса одной страницы. Непостоянное (non-persistent) выделение памяти означает освобождение памяти после обработки запроса вне зависимости от того, была ли вызвана функция освобождения памяти. К примеру, выделение памяти под пользовательские переменные носит непостоянный характер, так как по завершению обработки запроса они становятся бесполезными.

Несмотря на то, что расширение в теории может возложить на ZE освобождение непостоянной памяти автоматически по завершению каждого запроса, делать это не рекомендуется. Выделенная память будет оставаться долгое время невостребованной, ассоциированные с этой памятью ресурсы будут иметь меньше шансов быть закрытыми правильно, и, в конце концов, создание путаницы с освобождением памяти — плохая практика. Как вы увидите позже, убедиться, что все данные отчищены верно, достаточно легко.

Давайте кратко сравним традиционные функции выделения памяти (которые стоит использовать только при работе с внешними библиотеками) с функциями постоянного и непостоянного выделения памяти в PHP/ZE.

Традиционные Непостоянные Постоянные
malloc(count) emalloc(count) pemalloc(count, 1)*
calloc(count, num) ecalloc(count, 1) pecalloc(count, num, 1)
strdup(str) estrdup(str) pestrdup(str, 1)
strndup(str, len) estrndup(str, len) pemalloc() & memcpy()
free(ptr) efree(ptr) pefree(ptr, 1)
realloc(ptr, newsize) erealloc(ptr, newsize) perealloc(ptr, newsize, 1)
malloc(count * num + extr)** safe_emalloc(count, num, extr) safe_pemalloc(count, num, extr)
* Семейство функций pemalloc принимает в качестве параметра флаг «постоянности», который позволяет им вести себя как их непостоянные аналоги.
К примеру: emalloc(1234) тоже самое, что и pemalloc(1234, 0)
** safe_emalloc() и (в PHP 5) safe_pemalloc реализуют дополнительную проверку целочисленных переполнений.




Настройка и сборка окружения


Теперь, когда вы ознакомились с теорией работы PHP и Zend Engine, могу поспорить, вам не терпится погрузиться в работу и начать что-нибудь делать. Однако, перед этим вам нужно обзавестись кое-какими утилитами для сборки и настроить окружение для ваших целей.

Прежде всего, вам необходим сам PHP и набор средств сборки, необходимых для PHP. Если вам не приходилось собирать PHP из исходников, предлагаю взглянуть на эту статью. Несмотря на то, что использование бинарного пакета с исходниками PHP может показаться заманчивым, такие сборки зачастую лишены двух важных параметров программы ./configure, которые очень полезны во время процесса разработки. Первый из них это --enable-debug. Эта опция компилирует PHP с дополнительной отладочной информацией в исполняемых файлах, так что при возникновении ошибки сегментации (segfault) вы сможете получить дамп ядра и воспользоваться отладчиком gdb, что бы выяснить, где и почему произошла ошибка.
Название второй опции зависит от того, с какой версией PHP вы собираетесь работать. В PHP 4.3 она называется --enable-experimental-zts, начиная с PHP 5, она переименована в --enable-maintainer-zts. Эта опция заставит PHP думать, что он работает в многопотоковой (multithread) среде, и позволит вам отловить общие ошибки, которые незаметны в среде без потоков, но повлекут нестабильную работу вашего расширения в многопотоковой среде.
Скомпилировав PHP с дополнительными опциями и установив его на сервер разработки (или рабочую станцию), вы можете преступать к созданию своего первого расширения.



Hello World


Какая вводная статья по программированию может считаться завершённой без примера ставшей уже обязательной программы Hello World? В данном случае, мы рассмотрим создание расширения, которое добавляет в PHP одну единственную функцию, возвращающую строку со словом «Hello World». На PHP вы возможно реализовали бы это примерно так:
<?php

function hello_world() {
  return 'Hello World';
}

?>

Теперь давайте сделаем из этой функции расширение PHP. Первым делом создадим подкаталог hello в каталоге ext/ дерева каталогов исходников PHP и сделаем cd в него. В принципе, этот каталог может располагаться где угодно внутри или снаружи дерева каталогов с исходниками, но вам следует поместить его именно сюда для возможности ссылаться на него из статьи в дальнейшем. Здесь вам нужно создать три файла:
  1. конфигурационный файл (config.m4), используемый утилитой phpize для подготовки расширения к компиляции:
    PHP_ARG_ENABLE(hello, whether to enable Hello World support,
    [ --enable-hello Enable Hello World support])

    if test "$PHP_HELLO" = "yes"; then
    AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World])
    PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
    fi

  2. заголовочный файл (php_hello.h), содержащий указания, используемые PHP для загрузки расширений:
    #ifndef PHP_HELLO_H
    #define PHP_HELLO_H 1

    #define PHP_HELLO_WORLD_VERSION "1.0"
    #define PHP_HELLO_WORLD_EXTNAME "hello"

    PHP_FUNCTION(hello_world);

    extern zend_module_entry hello_module_entry;
    #define phpext_hello_ptr &hello_module_entry

    #endif



  3. файл с исходниками (hello.c), содержащий тело функции hello_world:
    #ifdef HAVE_CONFIG_H
    #include "config.h"
    #endif

    #include "php.h"
    #include "php_hello.h"

    static function_entry hello_functions[] = {
      PHP_FE(hello_world, NULL)
      {NULL, NULL, NULL}
    };

    zend_module_entry hello_module_entry = {
    #if ZEND_MODULE_API_NO >= 20010901
      STANDARD_MODULE_HEADER,
    #endif
      PHP_HELLO_WORLD_EXTNAME,
      hello_functions,
      NULL,
      NULL,
      NULL,
      NULL,
      NULL,
    #if ZEND_MODULE_API_NO >= 20010901
      PHP_HELLO_WORLD_VERSION,
    #endif
      STANDARD_MODULE_PROPERTIES
    };

    #ifdef COMPILE_DL_HELLO
    ZEND_GET_MODULE(hello)
    #endif

    PHP_FUNCTION(hello_world)
    {
      RETURN_STRING("Hello World", 1);
    }



Большая часть кода, которую вы видите в предыдущем примере, всего лишь своего рода «клей» — протокол для регистрации расширения в PHP и установления диалога между ними. Реальным же кодом являются только последние 4 строки в файле hello.c, которые выполняют действия на уровне, который может быть вызван из пользовательского скрипта. Несомненно, этот код довольно похож на PHP код, который мы видели ранее, и может быть легко разобран на части:
  • объявление функции с именем hello_world,
  • установка в качестве возвращаемого значения строки «Hello World»,
  • хм… 1? Что же значит эта 1?

Вспомним, что ZE использует усовершенствованный механизм управления памятью, который проверяет, что все запрошенные ресурсы высвобождены на момент завершения работы скрипта. В мире управления памятью повторное освобождение одного и того же блока памяти считается большим ай-ай-ай. Такое действие называется «двойное освобождение» и является частой причиной ошибок сегментации, так как заставляет программу попытаться получить доступ к блоку памяти, который ей больше не принадлежит.
Проще говоря, что бы ZE не начал повторно освобождать память, занимаемую статической строкой (какой является «Hello World» в нашем примере), функция hello_world должна каждый раз при вызове возвращать копию строки.
В принципе функция RETURN_STRING может взять на себя ответственность за то, что любая строка, переданная в неё должна быть скопирована, так что бы была возможность безопасно освободить занимаемую ей память позже. Однако, так как выделение памяти под строку, копирование строки и возвращение копии как результата не является стандартным поведением для внутренних функций ZE, RETURN_STRING позволяет указать, необходимо ли делать копию переменной или нет с помощью второго аргумента. Для более наглядного описания данной концепции, обратите внимание на следующий фрагмент кода, который функционально идентичен предыдущему:
PHP_FUNCTION(hello_world)
{
  char *str;

  str = estrdup("Hello World");
  RETURN_STRING(str, 0);
}



В этом примере вы вручную выделили непостоянную память для строки «Hello World», которую передали обратно в вызывающий скрипт с помощьюфункции RETURN_STRING, указав ей 0 в качестве второго параметра, что заставляет функцию не делать копию строки, так как она может воспользоваться нашей.



Сборка расширения


Последним шагом в этом упражнении будет сборка вашего расширения как динамически подгружаемого модуля. Если вы скопировали предыдущие примеры верно, остаётся лишь запустить три команды из каталога ext/hello/:
$ phpize
$ ./configure --enable-hello
$ make

Обратите внимание, что для корректной работы утилиты phpize необходимо наличие утилит m4 и autoconf.
После запуска всех этих команд у вас должен появится файл hello.so в папке ext/hello/modules/. Теперь, как и любое другое расширение PHP, вы можете просто скопировать его каталог с расширениями (по-умолчанию это /usr/local/lib/php/extensions/, но на всякий случай проверьте настройки php.ini или воспользуйтесь командой php-config --extension-dir) и добавьте строку extension=hello.so в php.ini для инициализации загрузки расширения при запуске PHP. Для SAPI типа CGI/CLI это будет следующий запуск скрипта, для SAPI web-серверов типа Apache это будет следующий перезапуск сервера. Давайте попробуем вызвать функцию из командной строки:
$ php –r ‘echo hello_world();’

Если всё пойдёт так, как должно, вы увидите строку Hello World в качестве результата работы скрипта, так как функция hello_world из загруженного расширения возвращает строку, а команда echo отображает то, что ей было передано на вход (в данном случае – результат работы функции hello_world).

В данном примере мы возвращали строку, другие скалярные типы данных могут быть возвращены по схожему принципу: RETURN_LONG для целочисленных значений, RETURN_DOUBLE для чисел с плавающей точкой, RETURN_BOOL для TRUE/FALSE и RETURN_NULL для, как вы догадались, NULL-значений. Давайте посмотрим на каждую из них в действии, добавив строки с макросом PHP_FE в структуру function_entity и соответствующие им макросы PHP_FUNCTION в файле hello.c:
static function_entry hello_functions[] = {
  PHP_FE(hello_world, NULL)
  PHP_FE(hello_long, NULL)
  PHP_FE(hello_double, NULL)
  PHP_FE(hello_bool, NULL)
  PHP_FE(hello_null, NULL)
  {NULL, NULL, NULL}
};

PHP_FUNCTION(hello_long)
{
  RETURN_LONG(42);
}

PHP_FUNCTION(hello_double)
{
  RETURN_DOUBLE(3.1415926535);
}

PHP_FUNCTION(hello_bool)
{
  RETURN_BOOL(1);
}

PHP_FUNCTION(hello_null)
{
  RETURN_NULL();
}



Кроме того, нам придётся добавить прототипы для этих функций рядом с прототипом для функции hello_world в заголовочном файле php_hello.h для того, что бы процесс сборки прошёл без проблем:
PHP_FUNCTION(hello_world);
PHP_FUNCTION(hello_long);
PHP_FUNCTION(hello_double);
PHP_FUNCTION(hello_bool);
PHP_FUNCTION(hello_null);



Так как вы не вносили изменений в файл config.m4, технически нет необходимости повторять шаги phpize и ./configure, а можно сразу перейти к выполнению команды make. Тем не менее, на этот раз я попрошу вас пройти через все три шага сборки заново с целью убедиться, что никаких проблем не возникло. Кроме того, в качестве последнего шага вы можете использовать команду make clean all вместо make, что бы быть уверенным, что все файлы с исходниками будут пересобраны. Повторюсь, что это не необходимо ввиду типа изменений, которые вы сделали, но лучше перестраховаться, чем наткнуться на ошибку. После того, как модуль собран, вам остаётся скопировать его в каталог расширений, заменив им старую версию.

Сейчас вы можете снова вызвать интерпретатор PHP, передав ему нехитрые скрипты для того, что бы протестировать только что добавленные функции. На самом деле – почему бы ни сделать это сейчас? Я подожду вас здесь…

Готово? Хорошо. Если для просмотра результата работы каждой функции вы использовали var_dump, а не echo, то, возможно, обратили внимание, что hello_bool возвращает TRUE. Это результат равенства 1-це аргумента функции. Также как и в PHP-скриптах, целочисленное значение 0 эквивалентно FALSE, в то время как любое другое число эквивалентно TRUE. Авторы расширений зачастую используют соглашение, согласно которому этим числом является 1. Желательно, но не обязательно, что бы и вы придерживались этого соглашения. Кроме того, для большего удобства доступны макросы RETURN_TRUE и RETURN_FALSE. Ниже приведён пример функции hello_bool с использованием макроса RETURN_TRUE.
PHP_FUNCTION(hello_bool)
{
  RETURN_TRUE;
}



Обратите внимание, что никаких круглых скобок при вызове макроса не использовалось. В этом плане макросы RETURN_TRUE и RETURN_FALSE отличаются от остальных макросов семейства RETURN_*, так что будьте внимательны и не попадитесь на этом!

Вы, возможно, заметили, что во всех предыдущих примерах мы не передаём параметр, отвечающий за создание копии результата. Причина этого в том, что для таких простых скалярных величин не требуется выделение или освобождение дополнительной памяти.

Существуют ещё 3 дополнительных возвращаемых типа: RESOURCE (возвращаемый, к примеру, функциями mysql_connect, fsockopen или ftp_connect), ARRAY (также известный как HASH) и OBJECT (возвращаемый по ключевому слову new). Речь о них пойдёт позже.



INI-настройки


Zend Engine предоставляет два подхода для работы с INI-данными. Сейчас мы рассмотрим наиболее простой из них, а к более общему вернёмся после ознакомления с глобальными переменными.

Предположим, вы хотите объявить в файле php.ini настройку hello.greeting для вашего расширения, которая будет содержать значение для вывода функцией hello_world. Для этого нам придётся добавить несколько изменений в файлы hello.c и php_hello.h в рамках изменения структуры hello_module_entry. Начнём с добавления следующих прототипов перед списком прототипов пользовательских функций в файле php_hello.h:
PHP_MINIT_FUNCTION(hello);
PHP_MSHUTDOWN_FUNCTION(hello);

PHP_FUNCTION(hello_world);
PHP_FUNCTION(hello_long);
PHP_FUNCTION(hello_double);
PHP_FUNCTION(hello_bool);
PHP_FUNCTION(hello_null);



Далее откройте файл hello.c и замените текущую версию hello_module_entry на следующую:
zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
  STANDARD_MODULE_HEADER,
#endif
  PHP_HELLO_WORLD_EXTNAME,
  hello_functions,
  PHP_MINIT(hello),
  PHP_MSHUTDOWN(hello),
  NULL,
  NULL,
  NULL,
#if ZEND_MODULE_API_NO >= 20010901
  PHP_HELLO_WORLD_VERSION,
#endif
  STANDARD_MODULE_PROPERTIES
};

PHP_INI_BEGIN()
PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)
PHP_INI_END()

PHP_MINIT_FUNCTION(hello)
{
  REGISTER_INI_ENTRIES();

  return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(hello)
{
  UNREGISTER_INI_ENTRIES();

  return SUCCESS;
}



Теперь вам следует добавить соответствующую директиву #include в начале файла hello.c для получения заголовков для работы с INI-файлами:
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "php_hello.h"



Последнее, что вам осталось – это модифицировать функцию hello_world так, что бы она использовала INI-настройку:
PHP_FUNCTION(hello_world)
{
  RETURN_STRING(INI_STR("hello.greeting"), 1);
}



Обратите внимание, что вы создаёте копию значения, полученного от INI_STR. Причина в том, что строка, возвращаемая INI_STR является статической. Другими словами, если вы попытаетесь её изменить рабочее окружение PHP может стать нестабильным или даже аварийно завершиться.

Первый набор внесённых в этой главе изменений добавляет два новых метода, с которыми вы должны быть уже знакомы: MINIT и MSHUTDOWN. Как упоминалось ранее, эти методы вызываются при первом запуске SAPI-слоя и в процессе завершения его работы соответственно. Они не вызываются в течение или между запросами. В этом примере они используются для регистрации записей из файла php.ini, объявленных в вашем расширении. Также в этих функциях могут быть зарегистрированы ресурсы, объекты и обработчики потоков.

В вашей функции hello_world вы используете макрос INI_STR для получения текущего значения записи hello.greating в качестве строки. Существует совокупность других функций для получения целочисленных значений, значений с плавающей точкой, булевых значений перечисленных ниже. В дополнение эти функции имеют дубликаты с суффиксом ORIG, позволяющие получить значение записи в том виде, в котором оно записано в файле php.ini (до того, как могло быть изменено посредствам файла .htaccess или функции ini_set).
Текущее значение Оригинальное значение Тип
INI_STR(name) INI_ORIG_STR(name) Char * (NULL terminated)
INI_INT(name) INI_ORIG_INT(name) signed long
INI_FLT(name) INI_ORIG_FLT(name) signed double
INI_BOOL(name) INI_ORIG_BOOL(name) Zend_bool

Перейдём к функции PHP_INI_ENTRY. Первым параметром ей передаётся строка, содержащая имя интересующей вас записи в файле php.ini. Для того, что бы избежать коллизий между именами записей в php.ini, вам следует использовать те же соглашения, что при наименовании функций: имя должно начинаться с префикса, совпадающего с именем расширения. Так же соглашение предусматривает, что разделителем имени расширения от оригинального имени настройки в INI-файлах должна служить точка. В данном случае имя настройки будет выглядеть как hello.greeting.
Вторым параметром является начальное значение настройки, которое всегда задаётся как char* вне зависимости от того, является ли значение числом или нет. Это является следствием того факта, что все настройки в INI-файлах по своей сути текстовые, так как сам файл текстовый. Только последующее использование в скрипте макросов INI_INT, INI_FLT или INI_BOOL вызывает преобразование их типов.
Третьим параметром является модификатор уровня доступа. Это битовая маска, которая определяет когда и как данная INI-настройка может быть модифицирована. Для некоторых настроек, таких как register_globals, просто-напросто не имеет смысла позволять изменение значения внутри скрипта с помощью функции ini_set, так как данная настройка имеет смысл только во время подготовки обработки запроса – до того как скрипту дана возможность отработать. Другие, такие как allow_url_fopen, являются административными настройками, которые пользователи не должны иметь права изменять ни через ini_set, ни через директивы .htaccess. По-умолчанию значением для этого параметра является значение PHP_INI_ALL, указывающее, что значение настройки может меняться где угодно. Также возможны значения PHP_INI_SYSTEM|PHP_INI_PERDIR, указывающие, что значение настройки может быть изменено через php.ini или директиву в файле .htaccess, но через функцию ini_set(). Или же возможно значение PHP_INI_SYSTEM, означающее, что настройку можно изменить только через файл php.ini и нигде больше.

Последний четвёртый параметр позволяет указать функцию обратного вызова (callback), вызываемую при изменении настройки с помощью функции ini_set. Это позволяет расширениям производить более полный контроль над условиями изменения настройки или вызывать соответствующую функцию в зависимости от нового значения настройки.



Глобальные переменные


Довольно часто расширению требуется обработать переменную в отдельном запросе, сохраняя её значение независимым от других запросов, которые могут обрабатываться в тоже самое время. В немногопоточном SAPI это можно сделать очень просто: всего лишь объявите глобальную переменную в файле с исходным кодом и обращайтесь к ней, когда вам нужно. Проблема в том, что, так как PHP спроектирован для работы с многопоточными web-серверами (такими как Apache 2 и ISS), ему необходимо хранить глобальные переменные, используемые одним потоком, отдельно от глобальных переменных другого. PHP значительно упрощает эту задачу благодаря использованию слоя абстракции TSRM (Thread Safe Resource Manager) иногда называемого ZTS (Zend Thread Safety). Фактически в данной статье уже использовались части TSRM, чего вы даже не заметили. (Не пытайтесь найти их, так как пока для вас это слишком сложно).

Первая часть создания потокобезопасной глобальной переменной, как и любой другой глобальной переменной, заключается в её объявлении. В качестве примера мы объявим одну глобальную переменную типа long, начальным значением которой будет 0. Каждый раз, когда функция hello_long будет вызываться, мы будем увеличивать значение глобальной переменной и возвращать её значение.
Добавьте следующий фрагмент кода в файл php_hello.h сразу после строки #define PHP_HELLO_H:
#ifdef ZTS
#include "TSRM.h"
#endif

ZEND_BEGIN_MODULE_GLOBALS(hello)
  long counter;
ZEND_END_MODULE_GLOBALS(hello)

#ifdef ZTS
#define HELLO_G(v) TSRMG(hello_globals_id, zend_hello_globals *, v)
#else
#define HELLO_G(v) (hello_globals.v)
#endif



Вам также предстоит использовать метод RINIT, так что необходимо объявить его прототип в заголовке:
PHP_MINIT_FUNCTION(hello);
PHP_MSHUTDOWN_FUNCTION(hello);
PHP_RINIT_FUNCTION(hello);



Теперь давайте перейдём к файлу hello.c и добавим следующий код после блока include’ов:
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "php_hello.h"

ZEND_DECLARE_MODULE_GLOBALS(hello)


Изменим hello_module_entry, добавив в него PHP_RINIT(hello):
zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
  STANDARD_MODULE_HEADER,
#endif
  PHP_HELLO_WORLD_EXTNAME,
  hello_functions,
  PHP_MINIT(hello),
  PHP_MSHUTDOWN(hello),
  PHP_RINIT(hello),
  NULL,
  NULL,
#if ZEND_MODULE_API_NO >= 20010901
  PHP_HELLO_WORLD_VERSION,
#endif
  STANDARD_MODULE_PROPERTIES
};



И измените вашу функцию MINIT вместе с добавлением ещё нескольких функций для обработки инициализации при старте запроса:
static void php_hello_init_globals(zend_hello_globals *hello_globals)
{
}

PHP_RINIT_FUNCTION(hello)
{
  HELLO_G(counter) = 0;

  return SUCCESS;
}

PHP_MINIT_FUNCTION(hello)
{
  ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals, NULL);

  REGISTER_INI_ENTRIES();

  return SUCCESS;
}



И, наконец, вы можете изменить функцию hello_long, добавив в неё использование глобальной переменной:
PHP_FUNCTION(hello_long)
{
  HELLO_G(counter)++;

  RETURN_LONG(HELLO_G(counter));
}



В изменениях, которые вы внесли в файл php_hello.h, вы использовали пару макросов – ZEND_BEGIN_MODULE_GLOBALS и ZEND_END_MODULE_GLOBALS. С их помощью была определена структура zend_hello_globals, содержащая одну переменную типа long. После этого был определён макрос HELLO_G, позволяющий в зависимости от режима компиляции (с учётом или без многопоточности) получать значение из пула потоков или просто взять его из глобальной области видимости.
В файле hello.c с помощью макроса ZEND_DECLARE_MODULE_GLBALS вы объявили структуру zend_hello_globals либо как глобальную (при непотокобезопасной сборке), либо как член пула ресурсов потока. Нам как авторам расширения не нужно задумываться об этом механизме, так как всю работу берёт на себя ZE. И, наконец, вы используете функцию ZEND_INIT_MODULE_GLOBALS для выделения идентификатора потокобезопасного ресурса.
Возможно, вы заметили, что функция php_hello_init_globals на самом деле ничего не делает. Кроме того, инициализация счётчика значением 0 оказалась в функции RINIT. Почему?
Ключ к ответу на этот вопрос кроется в моменте, когда эти функции вызываются. Функция php_hello_init_globals вызывается только при запуске нового процесса или потока. Однако, каждый процесс может обслуживать более одного запроса, так что использование этой функции для инициализации нашего счётчика значением 0 будет верно только для первого запроса. Последующий запрос к этому же процессу по прежнему будет работать со старым значением счётчика и, следовательно, не будет начинать отчёт с нуля. Для инициализации счётчика значением 0 для каждого запроса мы использовали функция RINIT, которая, как вы уже читали, вызывается перед каждым запросом обработки скрипта. Мы включили функцию php_hello_init_globals в наш код как минимум из-за того, что передача NULL в качестве соответствующего параметра ZEND_INIT_MODULE_GLOBALS функции init приведёт к ошибке сегментации для платформ без поддержки потоков.



INI-настройки и глобальные переменные


Как вы уже знаете, настройки в файле php.ini, объявляемые с помощью функции PHP_INI_ENTRY считываются как строки и при необходимости конвертируются в другие форматы с помощью функций INI_INT, INI_FLT и INI_BOOL. Однако, некоторые настройки во время выполнения скрипта могут запрашиваться слишком часто, что приведёт к большому количеству ненужных операций чтения из файла. К счастью, существует возможность инструктировать ZE хранить INI-настройки в особенном типе данных и производить приведение типов только при изменении значения настройки. Давайте проверим эту возможность в работе, объявив ещё одну INI-настройку, на этот раз типа boolean, определяющую, должен ли счётчик в настоящий момент уменьшаться или увеличиваться. Начнём с изменения блока MODULE_GLOBALS в файле php_hello.h следующим образом:
ZEND_BEGIN_MODULE_GLOBALS(hello)
  long counter;
  zend_bool direction;
ZEND_END_MODULE_GLOBALS(hello)



Следующим шагом объявите INI-настройку, изменив блок PHP_INI_BEGIN:
PHP_INI_BEGIN()
  PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)
  STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_hello_globals, hello_globals)
PHP_INI_END()



Теперь инициализируйте настройку в функции init_globals:
static void php_hello_init_globals(zend_hello_globals *hello_globals)
{
  hello_globals->direction = 1;
}



И, наконец, используйте значение INI-настройки в функции hello_long для выбора операции над счётчиком:
PHP_FUNCTION(hello_long)
{
  if (HELLO_G(direction)) {
    HELLO_G(counter)++;
  } else {
    HELLO_G(counter)--;
  }

  RETURN_LONG(HELLO_G(counter));
}



Вот и всё! С помощью метода OnUpdateBool (метод является часть ZE), переданного в качестве третьего параметра макроса STD_PHP_INI_ENTRY, будет производиться автоматическое приведение типа значения настройки, полученного из файла php.ini, .htaccess, или с помощью функции ini_set, к соответствующему значению TRUE/FALSE, которое вы можете получить прямо внутри скрипта. Последние три параметра функции STD_PHP_INI_ENTRY указывают PHP, какую глобальную переменную изменить, как выглядит структура глобальных переменных нашего расширения, и имя контейнера глобальных переменных, в котором они содержаться.



Что дальше?


В этой статье мы разобрали структуру простого PHP расширения, которое экспортирует функции, возвращающие значения, загружает и обрабатывает INI-настройки, сохраняя их на протяжении обработки запроса.

В следующей статье мы рассмотрим внутреннюю структуру переменных PHP, способы их хранения и обработки. Поработаем с функцией zend_parse_parameters, используемой для получения параметров из программы при вызове функции, и откроем способы возвращения более сложных результатов из функции, таких как массив, объект и ресурс.



P.S.


В некоторых местах перевод имеет довольно вольный формат, что является следствием трудностей перевода, которые мне не удалось преодолеть в полной мере. Кроме того перевод содержит некоторое количество мелких дополнений, показавшихся мне актуальными.
Процесс сборки расширения согласно статье проверен на исходниках PHP 5.3.2.

У автора есть несколько статей по данной тематике: 1, 2.1, 2.2, 3. Четвёртая статья видимо так и не увидит свет ввиду публикации автором книги «Extending and Embedding PHP», посвящённой данной тематике.

Так же про Zend Engine можно почитать на php.net.
Tags:
Hubs:
Total votes 83: ↑77 and ↓6+71
Comments16

Articles