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

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

      +2
      Восхитительно!
      Спасибо +
        +2
        Несколько лет назад занимался разработкой моста между PHP и запущенным в продакшен программным комплексом на C. Как жаль, что тогда не было под рукой этой документации. Сейчас это кажется самим собой разумеющимся, а вот тогда… =) Но так или иначе, статья ушла в избранное. Спасибо!
          0
          ман был всегда,
          сам набивал шишки по ману :)

          обращение к топикстартеру — ждем продолжений ;)
          модно писать классы, статические методы

        • UFO just landed and posted this here
              0
              и написал один мой хороший знакомый :)
              приятно видеть что на Хабре вспоминают про пхпКлаб

              жаль что не хватило кармы проголосовать за статью
              0
              Так последний пример нормально отработал или нет?
                0
                Отлично работает,
                если бы там были какие-то проблемы, я бы про это написал.
                  0
                  Просто как то кончается оборванно.
                0
                Ещё про разработку расширений есть несколько глав в книжке Шлосснейгла
                www.ozon.ru/context/detail/id/2527057/
                  0
                  о! я искал! я нашел!
                    0
                    о! я искал! я нашел!

                    Only users with full accounts can post comments. Log in, please.