Современному 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.