Пока nerezus сочиняет статью о встраивании PHP, я постараюсь продолжить его рассказ о написании расширений. Рассказано будет далеко не всё, поскольку я считаю, что сложность наращивать надо постепенно, иначе материал будет трудноусвояемым и совсем не питательным. В связи с этим я всё-таки не расскажу в этот раз, как подменить операторы в классе, кто захочет, может почитать исходники модуля Operator от Сары Големон — основного автора какой бы то ни было информации о разработке расширений PHP.
Поскольку разработку я веду исключительно в линуксе, то писать мы будем без всяких хитрых аддонов к Visual Studio, ручками, с нуля :) А что, лучше сразу разобраться, а потом уже упрощать свой труд.
Первым делом нам надо будет написать два файла: config.m4 и Makefile. Первый нужен для сборки нашего расширения, а второй… чтобы эта самая сборка не загадила наш каталог с исходниками :)
Итак, приступим. Файлы для сборки писать не слишком интересно, поэтому я их просто приведу и расскажу, что к чему. Вот общий вид нашего конфига.
Строки, начинающиеся с «dnl» — это комментарии.
Первые две строки добавляют нам опцию --enable-mystring для сборки. Так мы можем положить наш модуль в папку /ext/ исходников пхп и выбирать — собрать пхп с ним или без него.
Если закомментировать строки 1-2 и раскомментировать 3-4, то мы получим опцию --with-mystring, которой можно дополнительно указать путь. Зачем? Для всех оставшихся в файле строк :) Если мы заходим использовать какую-то специфическую библиотеку (я в примере указал библиотеки mylib1 и mylib2), то мы сможем при конфигурировании передать путь к ней. Собственно в строках 12-29 мы и пытаемся данные библиотеки найти и подключить.
Дальше обращаем внимание на строку 9. Она добавляет в наш конфиг переменную <ИМЯ_МОДУЛЯ>_SHARED_LIBADD, куда добавляются библиотеки для линковки. Вообще привыкайте, что m4 требует строгих, а не произвольных имён переменных. Это Спарта.
В строках 33-35 мы добавляем к линковке самые обычные библиотеки — их искать не требуется.
Ну и в строке 36 — указываем, из каких файлов надо собрать наш экстеншен.
Строка 32 дополнительно указывает, что мы пишем наш модуль не на C, а на С++ (в деле написания модуля — разница небольшая, а вот если вы будете подключать плюсовые библиотеки или плюсовый код — оно вам потребуется).
Поскольку в этот раз мы ничего не подключаем — мы наш конфиг значительно укоротим. Всего 10 строк — ничего лишнего.
Мейкфайлы, я думаю, каждый уважающий себя программист хоть раз в жизни да писал, поэтому пояснений требоваться не должно. Так что берем и пользуемся. Обращаю внимание, что команда `php-config --extension-dir` выдаёт нам каталог, где хранятся расширения PHP, а каталог __build — это тот, в котором мы и будем всё собирать.
Что нам теперь надо? Как и в любой приличной программе на C/C++ — заголовочный файл и файл с кодом. Начнём с заголовочного. Файл у нас опять-таки будет простой, меньше 40 строк. Пройдёмся по нему.
Строками 4-5 мы объявим название и версию нашего расширения. Нужно это с единственной целью — вывести данную ценную информацию позже в инфо о модуле.
Строка 7 задаёт нам имя класса. Конечно, писать «mystring» короче, чем PHP_MYSTRING_CLASS_NAME, но зато в любой момент сменить имя класса можно будет в одном единственном месте. Например, переименовать класс в «Mystring» — с большой буквы, как подобает приличным людям.
Строки 9-11 инклюдят файл config.h, который сгенерируется в процессе сборке — сами мы его писать не будем.
Дальше инклюдим стандартные заголовки и объявляем функции. Остановимся здесь подробнее. Функции PHP_MINIT_FUNCTION и PHP_MSHUTDOWN_FUNCTION — это функции, выполняющиеся при загрузке и выгрузке модуля. Помимо них бывают *_GINIT_* и *_GSHUTDOWN_* — функции для глобального запуска/выгрузки PHP, и *_RINIT_* и *_RSHUTDOWN_* — при активации/деактивации модуля. Функция PHP_MINFO_FUNCTION — это функция, выполняющаяся при вызове phpinfo(), она сгенерирует нам информацию о модуле.
Дальше мы объявим несколько методов — помните, в первой части вы объявляли их при помощи макроса PHP_FUNCTION и они были глобальными? Здесь мы делаем по сути то же самое, только макрос принимает два параметра — класс и имя его метода.
Ну и под конец 35-ой строкой объявим наш модуль.
Больше нам собственно в заголовочном файле ничего не нужно — переходим к коду.
Здесь, код посложнее. К сожалению, не удалось его поместить прямо в статью — хабр протестовал — поэтому я выложил сразу весь код сюда, здесь же мы рассмотрим только его содержимое.
Пока всё просто — объявляем наш будущий класс, и декларируем наш модуль. Обратите внимание — мы указали его название, версию, а также функции MINIT, MSHUTDOWN и MINFO. Глобальных функций мы в этот раз не объявляем, RINIT и RSHUTDOWN нам тоже не нужны, всё остальное можно считать дефолтным.
Теперь мы подставили макрос, говорящий PHP, что наш модуль можно скомпилировать как расширение, а не только как часть PHP. Затем объявили набор функций класса. Для каждой функции указывается 4 параметра: имя класса, имя функции, указатель на структуру arg_info и параметры доступа. Внятного описания структуры arg_info я ни разу не встречал, поэтому мы будем по старинке передавать NULL, а проверять параметры уже в самой функции.
Основные параметры доступа — это, разумеется, ZEND_ACC_PUBLIC, ZEND_ACC_PROTECTED и ZEND_ACC_PRIVATE. Дополнительно к ним можно указать ZEND_ACC_STATIC или ZEND_ACC_ALLOW_STATIC (такой метод можно использовать и как статический, и как обычный). Для конструкторов, деструкторов, а также оператора clone (в PHP объявляется как функция __clone) надо указывать соответственно флаги ZEND_ACC_CTOR, ZEND_ACC_DTOR и ZEND_ACC_CLONE.
Впрочем, полный список вы можете посмотреть в заголовочных файлах, на моей машине это файл /usr/include/php5/Zend/zend_compile.h
Обратите внимание, что после каждого метода, описанного с помощью PHP_ME() запятую ставить не нужно. А в конце всегда должен быть массив из трёх NULL'ов.
А тем временем мы едем дальше.
Определили три объявленных ранее метода — загрузка, выгрузка и информация о модуле. Из всех трёх интерес нам представляет только один: загрузка модуля. Для начала мы должны зарегистрировать наш объявленный класс.
Затем мы в зарегистрированном классе создаём свойство, для чего передаём указатель на зарегистрированный класс, название свойства, длину этого названия (об этом строчкой позже), значение по умолчанию и опять-таки параметры доступа. В конце мы должны обязательно написать волшебное слово TSRMLS_CC. Это слово надо упомянуть особо. Чтобы внутри функции работать с текущим состоянием PHP — знать, где мы сейчас находимся, иметь информацию о классах и т.д. и т.п., надо данную информацию передавать. Для этого при объявлении функции надо в конце написать макрос TSRMLS_DC, а при вызове — TSRMLS_CC. Запятую перед макросом в обоих случаях ставить не нужно.
Теперь вернемся к длине названия свойства. Информация об объявлении свойств в разных источниках весьма скудна и противоречива. Так, в руководстве той же Сары Големон упоминается, что если свойство объявлено как приватное, оно должно иметь название вида "\0имя_класса\0имя_свойства", для чего в PHP существует метод zend_mangle_property_name(), конструирующий подобное название для protected и private свойств. На практике же оказалось, что если использовать zend_mangle_property_name, PHP при обращении к свойству грязно ругается, в то время как в вышеупомянутом вызове — всё замечательно. Имейте в виду, если будете читать литературу по теме.
Давайте теперь определим стандартные методы класса. Поехали по пунктам.
Функция getThis() возвращает нам указатель на объект класса, из которого мы вызвали метод. Для конструктора и деструктора мы проверяем этот указатель — нам не хочется, чтобы кто-то вызывал данные методы статически. Затем в конструкторе мы — просто чтобы показать вам данную функцию — меняем уже задекларированное свойство «str». Для этого мы передаем тип объекта (класс), указатель на сам объект, имя и длину имени свойства, а также новое значение. Обратите внимание, что мы используем *_stringl(), а не *_string() функцию — она требует передать еще и длину нового значения. В принципе она используется в случаях, когда в строке может присутствовать символ '\0', или когда мы наоборот хотим записать только первые N символов от строки, но из-за того что она работает чуть быстрее, чем *_string() вариант, мы используем её и в такой тривиальной ситуации- нам же не жалко лишний параметр указать.
Аналогично мы используем макрос RETURN_STRINGL в функции приведения класса к строке. Обратите внимание на последний параметр «1» — он означает, что мы хотим передать не сам объект, а его копию. Мало ли что потом с нашей строкой захотят делать.
Давайте опишем метод добавления строки.
Первым делом мы проверяем, передали ли нам строку в качестве параметра — это уже рассматривалось в первой части.
Затем мы считываем из нашего объекта уже храняющуюся там строку. Обратите внимание, что хотя мы создавали свойство как string, указатель нам вернулся на zval. Всё дело в том, что абсолютно все типы данных в php представляются типом zval. Если вам интересно, как он устроен, смотрите файл /usr/include/php5/Zend/zend.h на предмет определения struct _zval_struct и union _zvalue_value.
Наконец, мы выделяем память под новую строку и копируем туда исходную строку и «добавку». Затем пользуемся уже знакомым методом zend_update_property_stringl() и записываем новое значение. Вместо malloc, calloc и free в PHP принято пользоваться зендовскими аналогами emalloc, ecalloc и efree.
Готово, новое значение лежит там, где надо, можно вернуть NULL. Домашнее задание: догадаться, как вернуть сам объект, чтобы можно было составлять цепочки вида $a->append(«a»)->append(«b»)…
Ну и наконец реализуем метод compare. В нём мы не будем реализовывать настоящее сравнивание (чёрт знает, почему я решил обозвать метод так), а выведем нашу строку — должна же она хоть где-то выводиться — и покажем чуть-чуть магии.
Для начала мы считали и вывели нашу строку str. Синтаксис функции zend_printf(), как вы могли заметить, абсолютно идентичен стандартному printf(). А вот дальше… что это? Да, это мы легко и непринужденно взяли и выполнили кусок PHP-кода. В качестве параметров передаём строку кода, указатель на переменную, куда положить результат выполнения, а также название нашего скрипта. Чтобы ничего не выдумывать, я просто написал myvardump.
Набираем `make all install`. Теперь вводим: php -r '$s = new mystring(); echo $s."\n"; $s->append(" Попячсо!"); $s->compare();', смотрим на вывод:
Первая строка — это мы вывели наш класс, приведенный к строке (см. __toString()). Вторая — вывели уже измененную строку str. А оставшиеся три строки — это результат нашего zend_eval_string — дамп класса. Наблюдаем наше созданное приватное свойство — всё честно.
Во-первых, в рамках самопиара рекомендую зачекаутить только недавно написанный мною экстеншен:
svn checkout http://dingo-php.googlecode.com/svn/trunk/ dingo-php
Большинству пользователей сам экстеншен будет абсолютно неинтересен с точки зрения практического применения, но зато в нём можно посмотреть:
— как сделать в одном экстеншене несколько классов, которые можно включать и выключать при компиляции
— как оборачивать и использовать сторонние библиотеки
— ну и немножко реального кода
Кроме того, рекомендую книгу Сары Големон:
>> Sara Golemon «Extending and Embedding PHP»
В России она стоит какого-то бешеного количества тенге, но при известном старании в интернете можно найти электронную версию.
Книга немного устаревшая, но зато именно там можно узнать, зачем нужны RINIT/RSHUTDOWN, как навесить свои хэндлеры на события, происходящие с классами, как создавать потоки (streams), работать с ini-конфигом, и творить с объектами очень многое другое.
P.s. Для тех, кому для экспериментов лень будет копипастить, я выложил архив с готовым к компиляции модулем: narod.ru/disk/15177450000/mystring.tar.gz.html
Поскольку разработку я веду исключительно в линуксе, то писать мы будем без всяких хитрых аддонов к Visual Studio, ручками, с нуля :) А что, лучше сразу разобраться, а потом уже упрощать свой труд.
Первым делом нам надо будет написать два файла: config.m4 и Makefile. Первый нужен для сборки нашего расширения, а второй… чтобы эта самая сборка не загадила наш каталог с исходниками :)
1. config.m4
Итак, приступим. Файлы для сборки писать не слишком интересно, поэтому я их просто приведу и расскажу, что к чему. Вот общий вид нашего конфига.
Строки, начинающиеся с «dnl» — это комментарии.
Первые две строки добавляют нам опцию --enable-mystring для сборки. Так мы можем положить наш модуль в папку /ext/ исходников пхп и выбирать — собрать пхп с ним или без него.
Если закомментировать строки 1-2 и раскомментировать 3-4, то мы получим опцию --with-mystring, которой можно дополнительно указать путь. Зачем? Для всех оставшихся в файле строк :) Если мы заходим использовать какую-то специфическую библиотеку (я в примере указал библиотеки mylib1 и mylib2), то мы сможем при конфигурировании передать путь к ней. Собственно в строках 12-29 мы и пытаемся данные библиотеки найти и подключить.
Дальше обращаем внимание на строку 9. Она добавляет в наш конфиг переменную <ИМЯ_МОДУЛЯ>_SHARED_LIBADD, куда добавляются библиотеки для линковки. Вообще привыкайте, что m4 требует строгих, а не произвольных имён переменных. Это Спарта.
В строках 33-35 мы добавляем к линковке самые обычные библиотеки — их искать не требуется.
Ну и в строке 36 — указываем, из каких файлов надо собрать наш экстеншен.
Строка 32 дополнительно указывает, что мы пишем наш модуль не на C, а на С++ (в деле написания модуля — разница небольшая, а вот если вы будете подключать плюсовые библиотеки или плюсовый код — оно вам потребуется).
Поскольку в этот раз мы ничего не подключаем — мы наш конфиг значительно укоротим. Всего 10 строк — ничего лишнего.
2. Makefile
Мейкфайлы, я думаю, каждый уважающий себя программист хоть раз в жизни да писал, поэтому пояснений требоваться не должно. Так что берем и пользуемся. Обращаю внимание, что команда `php-config --extension-dir` выдаёт нам каталог, где хранятся расширения PHP, а каталог __build — это тот, в котором мы и будем всё собирать.
3. Создаём mystring.h-файл
Что нам теперь надо? Как и в любой приличной программе на C/C++ — заголовочный файл и файл с кодом. Начнём с заголовочного. Файл у нас опять-таки будет простой, меньше 40 строк. Пройдёмся по нему.
Строками 4-5 мы объявим название и версию нашего расширения. Нужно это с единственной целью — вывести данную ценную информацию позже в инфо о модуле.
Строка 7 задаёт нам имя класса. Конечно, писать «mystring» короче, чем PHP_MYSTRING_CLASS_NAME, но зато в любой момент сменить имя класса можно будет в одном единственном месте. Например, переименовать класс в «Mystring» — с большой буквы, как подобает приличным людям.
Строки 9-11 инклюдят файл config.h, который сгенерируется в процессе сборке — сами мы его писать не будем.
Дальше инклюдим стандартные заголовки и объявляем функции. Остановимся здесь подробнее. Функции PHP_MINIT_FUNCTION и PHP_MSHUTDOWN_FUNCTION — это функции, выполняющиеся при загрузке и выгрузке модуля. Помимо них бывают *_GINIT_* и *_GSHUTDOWN_* — функции для глобального запуска/выгрузки PHP, и *_RINIT_* и *_RSHUTDOWN_* — при активации/деактивации модуля. Функция PHP_MINFO_FUNCTION — это функция, выполняющаяся при вызове phpinfo(), она сгенерирует нам информацию о модуле.
Дальше мы объявим несколько методов — помните, в первой части вы объявляли их при помощи макроса PHP_FUNCTION и они были глобальными? Здесь мы делаем по сути то же самое, только макрос принимает два параметра — класс и имя его метода.
Ну и под конец 35-ой строкой объявим наш модуль.
Больше нам собственно в заголовочном файле ничего не нужно — переходим к коду.
4. Пишем код модуля — mystring.cc
Здесь, код посложнее. К сожалению, не удалось его поместить прямо в статью — хабр протестовал — поэтому я выложил сразу весь код сюда, здесь же мы рассмотрим только его содержимое.
Строки 1-27
Пока всё просто — объявляем наш будущий класс, и декларируем наш модуль. Обратите внимание — мы указали его название, версию, а также функции MINIT, MSHUTDOWN и MINFO. Глобальных функций мы в этот раз не объявляем, RINIT и RSHUTDOWN нам тоже не нужны, всё остальное можно считать дефолтным.
Строки 29-49
Теперь мы подставили макрос, говорящий PHP, что наш модуль можно скомпилировать как расширение, а не только как часть PHP. Затем объявили набор функций класса. Для каждой функции указывается 4 параметра: имя класса, имя функции, указатель на структуру arg_info и параметры доступа. Внятного описания структуры arg_info я ни разу не встречал, поэтому мы будем по старинке передавать NULL, а проверять параметры уже в самой функции.
Основные параметры доступа — это, разумеется, ZEND_ACC_PUBLIC, ZEND_ACC_PROTECTED и ZEND_ACC_PRIVATE. Дополнительно к ним можно указать ZEND_ACC_STATIC или ZEND_ACC_ALLOW_STATIC (такой метод можно использовать и как статический, и как обычный). Для конструкторов, деструкторов, а также оператора clone (в PHP объявляется как функция __clone) надо указывать соответственно флаги ZEND_ACC_CTOR, ZEND_ACC_DTOR и ZEND_ACC_CLONE.
Впрочем, полный список вы можете посмотреть в заголовочных файлах, на моей машине это файл /usr/include/php5/Zend/zend_compile.h
Обратите внимание, что после каждого метода, описанного с помощью PHP_ME() запятую ставить не нужно. А в конце всегда должен быть массив из трёх NULL'ов.
А тем временем мы едем дальше.
Строки 51-76
Определили три объявленных ранее метода — загрузка, выгрузка и информация о модуле. Из всех трёх интерес нам представляет только один: загрузка модуля. Для начала мы должны зарегистрировать наш объявленный класс.
Затем мы в зарегистрированном классе создаём свойство, для чего передаём указатель на зарегистрированный класс, название свойства, длину этого названия (об этом строчкой позже), значение по умолчанию и опять-таки параметры доступа. В конце мы должны обязательно написать волшебное слово TSRMLS_CC. Это слово надо упомянуть особо. Чтобы внутри функции работать с текущим состоянием PHP — знать, где мы сейчас находимся, иметь информацию о классах и т.д. и т.п., надо данную информацию передавать. Для этого при объявлении функции надо в конце написать макрос TSRMLS_DC, а при вызове — TSRMLS_CC. Запятую перед макросом в обоих случаях ставить не нужно.
Теперь вернемся к длине названия свойства. Информация об объявлении свойств в разных источниках весьма скудна и противоречива. Так, в руководстве той же Сары Големон упоминается, что если свойство объявлено как приватное, оно должно иметь название вида "\0имя_класса\0имя_свойства", для чего в PHP существует метод zend_mangle_property_name(), конструирующий подобное название для protected и private свойств. На практике же оказалось, что если использовать zend_mangle_property_name, PHP при обращении к свойству грязно ругается, в то время как в вышеупомянутом вызове — всё замечательно. Имейте в виду, если будете читать литературу по теме.
Строки 78-106
Давайте теперь определим стандартные методы класса. Поехали по пунктам.
Функция getThis() возвращает нам указатель на объект класса, из которого мы вызвали метод. Для конструктора и деструктора мы проверяем этот указатель — нам не хочется, чтобы кто-то вызывал данные методы статически. Затем в конструкторе мы — просто чтобы показать вам данную функцию — меняем уже задекларированное свойство «str». Для этого мы передаем тип объекта (класс), указатель на сам объект, имя и длину имени свойства, а также новое значение. Обратите внимание, что мы используем *_stringl(), а не *_string() функцию — она требует передать еще и длину нового значения. В принципе она используется в случаях, когда в строке может присутствовать символ '\0', или когда мы наоборот хотим записать только первые N символов от строки, но из-за того что она работает чуть быстрее, чем *_string() вариант, мы используем её и в такой тривиальной ситуации- нам же не жалко лишний параметр указать.
Аналогично мы используем макрос RETURN_STRINGL в функции приведения класса к строке. Обратите внимание на последний параметр «1» — он означает, что мы хотим передать не сам объект, а его копию. Мало ли что потом с нашей строкой захотят делать.
Строки 108-133
Давайте опишем метод добавления строки.
Первым делом мы проверяем, передали ли нам строку в качестве параметра — это уже рассматривалось в первой части.
Затем мы считываем из нашего объекта уже храняющуюся там строку. Обратите внимание, что хотя мы создавали свойство как string, указатель нам вернулся на zval. Всё дело в том, что абсолютно все типы данных в php представляются типом zval. Если вам интересно, как он устроен, смотрите файл /usr/include/php5/Zend/zend.h на предмет определения struct _zval_struct и union _zvalue_value.
Наконец, мы выделяем память под новую строку и копируем туда исходную строку и «добавку». Затем пользуемся уже знакомым методом zend_update_property_stringl() и записываем новое значение. Вместо malloc, calloc и free в PHP принято пользоваться зендовскими аналогами emalloc, ecalloc и efree.
Готово, новое значение лежит там, где надо, можно вернуть NULL. Домашнее задание: догадаться, как вернуть сам объект, чтобы можно было составлять цепочки вида $a->append(«a»)->append(«b»)…
Строки 135-148
Ну и наконец реализуем метод compare. В нём мы не будем реализовывать настоящее сравнивание (чёрт знает, почему я решил обозвать метод так), а выведем нашу строку — должна же она хоть где-то выводиться — и покажем чуть-чуть магии.
Для начала мы считали и вывели нашу строку str. Синтаксис функции zend_printf(), как вы могли заметить, абсолютно идентичен стандартному printf(). А вот дальше… что это? Да, это мы легко и непринужденно взяли и выполнили кусок PHP-кода. В качестве параметров передаём строку кода, указатель на переменную, куда положить результат выполнения, а также название нашего скрипта. Чтобы ничего не выдумывать, я просто написал myvardump.
Итоги
Набираем `make all install`. Теперь вводим: php -r '$s = new mystring(); echo $s."\n"; $s->append(" Попячсо!"); $s->compare();', смотрим на вывод:
mystring
upchk Попячсо!
object(mystring)#1 (1) {
["str:private"]=>
string(21) "upchk Попячсо!"
}
Первая строка — это мы вывели наш класс, приведенный к строке (см. __toString()). Вторая — вывели уже измененную строку str. А оставшиеся три строки — это результат нашего zend_eval_string — дамп класса. Наблюдаем наше созданное приватное свойство — всё честно.
Что дальше?
Во-первых, в рамках самопиара рекомендую зачекаутить только недавно написанный мною экстеншен:
svn checkout http://dingo-php.googlecode.com/svn/trunk/ dingo-php
Большинству пользователей сам экстеншен будет абсолютно неинтересен с точки зрения практического применения, но зато в нём можно посмотреть:
— как сделать в одном экстеншене несколько классов, которые можно включать и выключать при компиляции
— как оборачивать и использовать сторонние библиотеки
— ну и немножко реального кода
Кроме того, рекомендую книгу Сары Големон:
>> Sara Golemon «Extending and Embedding PHP»
В России она стоит какого-то бешеного количества тенге, но при известном старании в интернете можно найти электронную версию.
Книга немного устаревшая, но зато именно там можно узнать, зачем нужны RINIT/RSHUTDOWN, как навесить свои хэндлеры на события, происходящие с классами, как создавать потоки (streams), работать с ini-конфигом, и творить с объектами очень многое другое.
P.s. Для тех, кому для экспериментов лень будет копипастить, я выложил архив с готовым к компиляции модулем: narod.ru/disk/15177450000/mystring.tar.gz.html