PHP модуль — это всё ещё просто. Часть вторая

    Пока nerezus сочиняет статью о встраивании PHP, я постараюсь продолжить его рассказ о написании расширений. Рассказано будет далеко не всё, поскольку я считаю, что сложность наращивать надо постепенно, иначе материал будет трудноусвояемым и совсем не питательным. В связи с этим я всё-таки не расскажу в этот раз, как подменить операторы в классе, кто захочет, может почитать исходники модуля Operator от Сары Големон — основного автора какой бы то ни было информации о разработке расширений PHP.
    Поскольку разработку я веду исключительно в линуксе, то писать мы будем без всяких хитрых аддонов к 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
    Поделиться публикацией

    Похожие публикации

    Комментарии 20
      +7
      Пост с Того Хабра [х]
        0
        черт, вы меня опередили )
        Хотя я все равно напишу!
          0
          Я три дня в свободное время её готовил :) Шустрее надо быть, шустрее.
            0
            Хорошо вам. Я месяц готовлю %) Мало времени совсем.
            Уже 3 раза бросал с мыслями, что писать — не мое, что никогда не допишу и т.п. Но решил добить все-таки…
          0
          добавил в избранное
            +1
            =) осталось пнуть зенда чтоб реализовали в ядре строки и числа обьектами
              0
              кстати, по моему эта функция не совсем корректна:
              PHP_METHOD(PHP_MYSTRING_CLASS_NAME, __toString)
              {
              	RETURN_STRINGL((char*)PHP_MYSTRING_CLASS_NAME, sizeof(PHP_MYSTRING_CLASS_NAME)-1, 1);
              }


              наверное надо было возвращать содержимое вместо названия класса, это ведь как никак строка =)
                0
                Ну, мы можем вернуть и строку, это не проблема, как мы уже выяснили — как сама строка, так и ее длина с лёгкостью вытягиваются. Другое дело, что мы лишний раз куда-то приткнули имя класса :) А еще очень часто существует практика отображения класс->строка = «имя класса», чем я в данном случае и руководствовался.
                0
                А еще есть такая штука Swig. Генерирует обертки к библиотеке, так что в результате билда получается экстенжн. Притом не только к PHP, но и к куче других языков можно (Ruby, Perl, Java, Lua etc).
                  0
                  Я её пробовал. Не знаю, как с другими языками, а с PHP у нее получается хрень собачья.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Генерит экстеншен, компилится, линкуется и… при запуске говорит «файл.so не является PHP-экстеншеном».
                  0
                  Книга Сары устаревшая
                  там про классы почти ничего нет
                  но почитать ее стоит

                  статья в целом отличная
                  лично я учился по ману
                  спасибо

                    +1
                    Про классы там есть и, в принципе, достаточно для большинства задач.
                    Просто кроме Сары источников информации нет, только такие же редкие авторы статей. Если на том же php.net зайти в мануал — там есть доки по ZE1, и вообще нет документации по ZE2. Про ZE3 я молчу. Поэтому и возникает проблема.
                    0
                    есть немного в книге Профф программирование на РНР
                      0
                      хотя я согласен инфу надо собирать по кусочкам
                      и в основном з исиходников
                      0
                      да, такой вопрос:
                      сейчас модно реализовывать такие штучки:
                      $myClass = myClass();
                      $myClass->connect()
                      ->setLimit(25)
                      ->setQueueParms(27)
                      -> etc

                      достаточно ли будет в этом случае вернуть getThis()?
                        0
                        Домашнее задание: догадаться, как вернуть сам объект, чтобы можно было составлять цепочки вида $a->append(«a»)->append(«b»)...

                        Не буду же я абсолютно всё разжёвывать, в самом деле :)
                        Попробуйте, узнаете.
                          0
                          ну почему так остро воспринимать такой простой вопрос?
                          с этим, при необходимости я бы мог давно поиграться и сам. Как-никак три-четыре расширения за моими плечами. Но ответ, может пригодится для тех, на кого рассчитана эта статья.
                            0
                            Я не остро воспринимаю. Просто, как и автор первой части, хочу, чтобы люди думали и экспериментировали сами.

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

                      Самое читаемое