Pull to refresh

Пишем расширения для PHP на C (Си)

Reading time 7 min
Views 3.4K
Современному PHP разработчику это знание может понадобиться скорее для расширения сознания, чем непосредственное руководство к действию, но несмотря на то, что в PHP уже встроено практически все необходимое, а в разнообразных PEAR и PECL репозитариях можно найти пакет дополнений на любой вкус, многим думаю будет интересно, а некоторым и полезно узнать как и что работает внутри PHP.

И раз уж 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.
Tags:
Hubs:
+27
Comments 12
Comments Comments 12

Articles