Современному PHP разработчику это знание может понадобиться скорее для расширения сознания, чем непосредственное руководство к действию, но несмотря на то, что в PHP уже встроено практически все необходимое, а в разнообразных PEAR и PECL репозитариях можно найти пакет дополнений на любой вкус, многим думаю будет интересно, а некоторым и полезно узнать как и что работает внутри PHP.
И раз уж Zend предоставил нам такие удобные инструменты, почему бы ими не воспользоваться? Например для оптимизации каких-то процессов, сокрытия своего когда в коммерческих приложениях и встраивания механизма лицензий, реализации многопоточности или для чего-то еще…
Есть два варианта расширений: сборка PHP с нашим кодом или динамически загружаемые модули. Оба варианта отличаются только одной строчкой — наличием макроса ZEND_GET_MODULE в последнем. Разницы между ними особой нет, но в процессе разработки и дальнейшей поддержки приложения гораздо удобнее работать с отдельными модулями.
Давайте рассмотрим немного измененный для простоты пример из официальной документации.
Все что делает наша первая программа — это возвращает «1», и дает нам представление о
стандартной структуре расширения. По сути этот минимальный набор макросов и API — скелет, который подойдет в большинстве случаев.
Что бы было понятно откуда что берется, я указал полные пути к заголовочным файлам.
Но если вы собираетесь работать над большим проектом, то стоит обратить внимание на
рекомендуемые и предустановленные средства, и воспользоваться утилитой ext_skel
(php-dist/ext/ext_skel --extname=mymod), которая сгенерит директорию с конфигами для нового
модуля. Тут мы не будем рассматривать это подробно, глобальной разницы нет, но для понимания
ручная сборка удобнее.
Теперь копируем test.so в 'extension_dir'. extension_dir мы должны или прописать в php.ini,
или же использовать имеющийся. Все зависит от предыдущей настройки Вашей системы.
Сразу после этого наш модуль готов к работе, остается только загрузить его в PHP-скрипте.
Создаем test.php
Для начала давайте напишем модуль, который будет получать три аргумента и просто выводить их.
Заголовок остается тот же, меняем только my_function
Тут я хотел бы остановиться на нескольких моментах. Так как в PHP нет типов переменных в том
виде, котором их понимает C, и содержать они могут все что угодно, то нужно определить для себя
в каком месте подготавливать передаваемые данные.
Это можно сделать в PHP-скрипте, и отдать их в функцию, как это сделано в примере или же
проверить все в модуле, в этом случае все передается как «z». Делать это в двух местах может
быть и правильно, но не очень разумно, мы же стремимся к оптимизации.
И если передаваемые аргументы за нас обрабатывает zend_parse_parameters, то с массивами
придется делать это самим.
— в примере ко всем элементам массива применяется
в данном случае это подходит — нам ведь нужно просто это напечатать.
Обработку и предподготовку аргументов в PHP оставим на ваше усмотрение, кто как умеет,
так и делает, Zend API же предлагает нам прекрасный набор макросов, выглядеть это может,
например, так:
Теперь давайте немного поговорим о том, что и как можно возвращать из нашей программы.
Как и следовало ожидать все типы кроме массивов возвращаются без особых усилий с нашей
стороны:
(С полным списков макросов можно познакомиться в официальной документации)
Для массивов же есть определенная процедура действий, которые мы должны выполнить:
array_init(return_value) — инициализируем массив
далее добавляем в него элементы следующими способами
add_next_index_* для индексного массива
или
add_index_* для индексного массива по ключу
или
add_assoc_* для ассоциативного массива по ключу
* это
Вот небольшой пример того как вернуть массив элементами которого являются другие
массивы, содержащие в свою очередь результат выполнения SELECT.
В официальной документации я не нашел никакой информации относительно особенностей
использования программных потоков при написании расширений для PHP, так что можно сделать
вывод, что они работают так же как и любая другая библиотека включенная в наш код. Не так
давно на хабре была ссылка на статью про многопоточность средствами самого PHP. На мой взгляд,
если уж Вы решились на такой шаг, то лучше использовать pthread и модули написанные на C.
Давайте проверим, что все это работает. Напишем небольшой tcp сервер, который одновременно
будет открывать 3 входящих порта. Таким образом, мы точно сможем убедиться, что все работает корректно, просто запустив netstat -ln или открыв одновременно 3 соединения.
Заголовок оставляем прежним (как в первом примере), меняем только my_func и добавляем функцию
thr.
Немного меняем наш test.php
Официальная документация
Статьи на Зенде, посвещенные расширениям
А так же очень рекомендую Вам ознакомиться с исходным кодом расширений включенных в
дистрибутив PHP, особенно с папкой ext/standart.
И раз уж Zend предоставил нам такие удобные инструменты, почему бы ими не воспользоваться? Например для оптимизации каких-то процессов, сокрытия своего когда в коммерческих приложениях и встраивания механизма лицензий, реализации многопоточности или для чего-то еще…
Есть два варианта расширений: сборка PHP с нашим кодом или динамически загружаемые модули. Оба варианта отличаются только одной строчкой — наличием макроса ZEND_GET_MODULE в последнем. Разницы между ними особой нет, но в процессе разработки и дальнейшей поддержки приложения гораздо удобнее работать с отдельными модулями.
Hello World
Давайте рассмотрим немного измененный для простоты пример из официальной документации.
Все что делает наша первая программа — это возвращает «1», и дает нам представление о
стандартной структуре расширения. По сути этот минимальный набор макросов и API — скелет, который подойдет в большинстве случаев.
#include "php.h" /* declaration of functions to be exported */ ZEND_FUNCTION( my_function ); /* compiled function list so Zend knows what's in this module */ zend_function_entry firstmod_functions[] = { ZEND_FE(my_function, NULL) {NULL, NULL, NULL} }; /* compiled module information */ zend_module_entry firstmod_module_entry = { STANDARD_MODULE_HEADER, "First Module", firstmod_functions, NULL, NULL, NULL, NULL, NULL, NO_VERSION_YET, STANDARD_MODULE_PROPERTIES }; ZEND_GET_MODULE(firstmod) /* implement function that is meant to be made available to PHP */ ZEND_FUNCTION( my_function ) { RETURN_LONG(1); }
Компиляция
cc -fpic -Wall -I/opt/php-5.2.6/include/php/Zend -I/opt/php-5.2.6/include/php/main -I/opt/php-5.2.6/include/php/TSRM -I/opt/php-5.2.6/include/php -c -o test.o test.c cc -shared -rdynamic -Wall -O3 -o test.so test.o
Что бы было понятно откуда что берется, я указал полные пути к заголовочным файлам.
Но если вы собираетесь работать над большим проектом, то стоит обратить внимание на
рекомендуемые и предустановленные средства, и воспользоваться утилитой ext_skel
(php-dist/ext/ext_skel --extname=mymod), которая сгенерит директорию с конфигами для нового
модуля. Тут мы не будем рассматривать это подробно, глобальной разницы нет, но для понимания
ручная сборка удобнее.
Теперь копируем test.so в 'extension_dir'. extension_dir мы должны или прописать в php.ini,
или же использовать имеющийся. Все зависит от предыдущей настройки Вашей системы.
Сразу после этого наш модуль готов к работе, остается только загрузить его в PHP-скрипте.
Создаем test.php
<?php dl("test.so"); // Загружаем наш модуль $return = my_function(); print("We got '$return'"); ?>
Получение, обработка аргументов и возвращение результата.
Для начала давайте напишем модуль, который будет получать три аргумента и просто выводить их.
Заголовок остается тот же, меняем только my_function
ZEND_FUNCTION( my_function ) { char *str; int str_len; long l; zval *array_arg; zval **tmp; HashTable *target; HashPosition pos; if(ZEND_NUM_ARGS() != 3) WRONG_PARAM_COUNT; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zsl", &array_arg, &str, &str_len, &l) == FAILURE) { return; } /* z - zval* s - string, char*,int - обратите внимание, что за указателем, следует int переменная, в которую пишется длинна. [..., &str, &str_len,...] Не забывайте это, если Вы собираетесь использовать string в этом их понимании. l - Long, long список остальных типов и кодов, досупных в этой функции b - boolean, zend_bool d - double, double r - resource, zval* a - array, zval* o - object, zval* */ /* Обрабатываем массив */ target = Z_ARRVAL_P(array_arg); if(!target){ php_error_docref(NULL TSRMLS_CC, E_WARNING, "The argument 'array_arg' should be an array"); RETURN_FALSE; } zend_hash_internal_pointer_reset_ex(target, &pos); zend_printf("Array size: %d <br>\r\n", zend_hash_num_elements(target)); while (zend_hash_get_current_data_ex(target, (void **)&tmp, &pos) == SUCCESS){ convert_to_string(*tmp); zend_printf("%s <br> \r\n", Z_STRVAL_PP(tmp)); zend_hash_move_forward_ex(target, &pos); } /* С обычными переменными все проще */ zend_printf("%s <br> \r\n", str); zend_printf("%ld <br> \r\n", l); RETURN_LONG(1); }
Тут я хотел бы остановиться на нескольких моментах. Так как в PHP нет типов переменных в том
виде, котором их понимает C, и содержать они могут все что угодно, то нужно определить для себя
в каком месте подготавливать передаваемые данные.
Это можно сделать в PHP-скрипте, и отдать их в функцию, как это сделано в примере или же
проверить все в модуле, в этом случае все передается как «z». Делать это в двух местах может
быть и правильно, но не очень разумно, мы же стремимся к оптимизации.
И если передаваемые аргументы за нас обрабатывает zend_parse_parameters, то с массивами
придется делать это самим.
— в примере ко всем элементам массива применяется
convert_to_string(*tmp); Z_STRVAL_PP(tmp);
в данном случае это подходит — нам ведь нужно просто это напечатать.
Обработку и предподготовку аргументов в PHP оставим на ваше усмотрение, кто как умеет,
так и делает, Zend API же предлагает нам прекрасный набор макросов, выглядеть это может,
например, так:
while (zend_hash_get_current_data_ex(target, (void **)&tmp, &pos) == SUCCESS){ switch (Z_TYPE_PP(tmp)) { case IS_NULL: php_printf("NULL <br>"); break; case IS_BOOL: convert_to_boolean(*tmp); php_printf("Boolean: %s <br>", Z_LVAL_PP(tmp) ? "TRUE" : "FALSE"); break; case IS_LONG: convert_to_long(*tmp); php_printf("Long: %ld <br>", Z_LVAL_PP(tmp)); break; case IS_DOUBLE: convert_to_double(*tmp); php_printf("Double: %f <br>", Z_DVAL_PP(tmp)); break; case IS_STRING: convert_to_string(*tmp); php_printf("String: %s, len(%d) <br>", Z_STRVAL_PP(tmp), Z_STRLEN_PP(tmp)); break; case IS_RESOURCE: php_printf("Resource<br>"); break; case IS_ARRAY: php_printf("Array<br>"); break; case IS_OBJECT: php_printf("Object<br>"); break; default: php_printf("Unknown<br>"); } zend_hash_move_forward_ex(target, &pos); }
Возвращаем результат
Теперь давайте немного поговорим о том, что и как можно возвращать из нашей программы.
Как и следовало ожидать все типы кроме массивов возвращаются без особых усилий с нашей
стороны:
RETURN_BOOL(bool) RETURN_NULL() RETURN_LONG(long) RETURN_DOUBLE(double) RETURN_STRING(string, duplicate) RETURN_EMPTY_STRING() RETURN_FALSE RETURN_TRUE
(С полным списков макросов можно познакомиться в официальной документации)
Для массивов же есть определенная процедура действий, которые мы должны выполнить:
array_init(return_value) — инициализируем массив
далее добавляем в него элементы следующими способами
add_next_index_* для индексного массива
или
add_index_* для индексного массива по ключу
или
add_assoc_* для ассоциативного массива по ключу
* это
bool, long, string, null, zval, double
Вот небольшой пример того как вернуть массив элементами которого являются другие
массивы, содержащие в свою очередь результат выполнения SELECT.
array_init(return_value); /* Инициализируем главный массив */ rcnt = PQntuples(result); /* Кол-во строк (postgresql api) */ fcnt = PQnfields(result); /* Кол-во столбцов (postgresql api) */ for(i = 0; i < rcnt; i++){ MAKE_STD_ZVAL(row); /* Создаем zval контейнер для будущего массива*/ /* Инициализируем второй массив, в котором и будут хранится данные */ array_init(row); for(x = 0; x < fcnt; x++) add_index_stringl(row, x, (char *)PQgetvalue(result, i, x), strlen((char *)PQgetvalue(result, i, x)), 1); /* Добавляем row как элемент массива return_value */ add_index_zval(return_value, i, row); }
Многопоточность… ?
В официальной документации я не нашел никакой информации относительно особенностей
использования программных потоков при написании расширений для PHP, так что можно сделать
вывод, что они работают так же как и любая другая библиотека включенная в наш код. Не так
давно на хабре была ссылка на статью про многопоточность средствами самого PHP. На мой взгляд,
если уж Вы решились на такой шаг, то лучше использовать pthread и модули написанные на C.
Давайте проверим, что все это работает. Напишем небольшой tcp сервер, который одновременно
будет открывать 3 входящих порта. Таким образом, мы точно сможем убедиться, что все работает корректно, просто запустив netstat -ln или открыв одновременно 3 соединения.
Заголовок оставляем прежним (как в первом примере), меняем только my_func и добавляем функцию
thr.
void *thr(void *ptr) { struct sockaddr_in cliaddr, servaddr; int servsock, sock, on, Port = -1, i=0; socklen_t clilen; char p, line[128]; pthread_detach(pthread_self()); if((servsock = socket(AF_INET, SOCK_STREAM, 0)) < 0){ zend_printf("can't create servsocket <br>\n"); return(0); } if(setsockopt(servsock, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)) < 0){ zend_printf("SO_REUSEADDR error <br>\n"); return(0); } Port = *((int *)ptr); free(ptr); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(Port); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(servsock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){ zend_printf("can't bind socket <br>\n"); return(0); } listen(servsock, 5); clilen = sizeof(cliaddr); while(1){ sock = accept(servsock, (struct sockaddr *)&cliaddr, &clilen); if(sock < 0){ zend_printf("[-] accept error <br>\n"); return(0); } zend_printf("[+] %s: connected<br>\n", inet_ntoa(cliaddr.sin_addr)); memset(&line, 0x00, sizeof(line)); while( read(sock, (char *)&p, 1) > 0 ){ /* Не делайте так! Здесь это только для простоты кода. */ if(i >= 127){ memset(&line, 0x00, sizeof(line)); i=0; } if(p == '\n') { line[i] = '\0'; zend_printf("port[%d] line: %s <br> \r\n", Port, line); memset(&line, 0x00, sizeof(line)); i=0; } line[i] = p; i++; } } return(0); } ZEND_FUNCTION( my_function ) { pthread_t tid; int *port_arg; int i=0; for(i = 1; i < 4; i++){ port_arg = (int *)malloc(sizeof(int)); *port_arg = 1024+i; if(pthread_create(&tid, NULL, &thr, (void *)port_arg) != 0){ zend_printf("thread creating error <br>\n"); RETURN_LONG(-1); } zend_printf("Thread [%d] created, port_arg=%d <br>\r\n", i, *port_arg); } sleep(30); /* Через 30 секунд все наши процессы умрут. */ zend_printf("done <br> \n"); RETURN_LONG(1); }
Немного меняем наш test.php
<?php dl("test.so"); ob_implicit_flush(true); ob_end_flush(); my_function(); return; ?>
Дополнительная информация
Официальная документация
Статьи на Зенде, посвещенные расширениям
А так же очень рекомендую Вам ознакомиться с исходным кодом расширений включенных в
дистрибутив PHP, особенно с папкой ext/standart.