Пишем PHP extension

    А давайте сегодня взглянем на PHP немного с другой точки зрения, и напишем к нему расширение. Так как на эту тему уже были публикации на Хабре (здесь и здесь), то не будем углубляться в причины того, для чего это может оказаться полезным и для чего может быть использовано на практике. Эта статья расскажет, как собирать простые расширения под Windows с использованием Visual C++ и под Debian с использованием GCC. Также я постараюсь немного осветить работу с PHP-массивами внутри расширений и провести сравнение производительности алгоритма, написанного на native PHP и использующего код, написанный на C.


    Компиляция под Win32


    Итак, начнем с Windows. Как известно, разработчики PHP используют Visual C++ 9 или Visual Studio 2008 для компиляции своего детища под Windows. Поэтому мы будем использовать Visual Studio 2008, бесплатная Express версия тоже подойдет, как впрочем, наверное, и более поздние и ранние версии студии.

    Что нам потребуется:Для начала создадим проект типа Win32 Console Application и выберем DLL в Application type. Теперь нам придется настроить все зависимости и пути для линковщика:
    • Щелкните правой кнопкой мыши в Solution Explorer'e и выберите Properties > C/C++ > General > Additional Include Directories. Сюда мы добавим директории, в которых лежат распакованные исходники и заголовочные файлы PHP. Конкретно нужны будут:
      php-5.3.6
      php-5.3.6\main
      php-5.3.6\TSRM
      php-5.3.6\Zend
      

    • Теперь добавим preprocessor definitions, которые нужны для корректного выбора платформы и компиляции модуля. Выбираем Configuration Properties > C/C++ > Preprocessor > Preprocessor Definitions, и добавляем туда следующее:
      PHP_WIN32
      ZEND_WIN32
      ZTS=1
      ZEND_DEBUG=0
      

    • Затем укажем линковщику где можно найти необходимые библиотеки. Выбираем Configuration Properties > Linker > General > Additional Library Directories. Там выбираем директорию \div из бинарников PHP. Должно получиться что-то такое: «D:\Program Files\php-5.3.6-Win32-VC9-x86\dev».

    • Теперь укажем конкретную либу для линковщика. Идем в Configuration Properties > Linker > Input > Additional Dependencies, и вписываем туда php5ts.lib, которая находится в той самой \dev директории, которую мы указали в предыдущем шаге.

    • Для избегания некоторых проблем компиляции, добавим директиву /FORCE:MULTIPLE в Configuration Properties > Linker > Command Line. Подробнее о ней можно прочитать на сайте MSDN.

    • И, наконец, можно указать, куда сохранять скомпилированную dll. Для этого перейдем в Configuration Properties > Linker > General > Output Filename и укажем там путь к папке \ext установленного PHP. Должно получиться что-нибудь такое: «D:\Program Files\php-5.3.6-Win32-VC9-x86\ext\$(ProjectName).dll».
    Найдем в проекте файл stdafx.h и заменим его содержимое на следующее:
    #ifndef STDAFX
    
    #define STDAFX
    #define PHP_COMPILER_ID "VC9"    // эту опцию мы указываем для совместимости с PHP, скомпилированным Visual C++ 9.0
    #include "zend_config.w32.h" 
    #include "php.h"
    
    #endif

    Если вы попытаетесь скомпилировать проект на данном этапе, вы получите ошибку, говорящую о том, что отсутствует main\config.w32.h. Его можно получить либо запустив скрипт main\configure.bat, либо можно выдернуть его из исходников, например версии PHP 5.2. При этом не забываем отредактировать в этом файле все пути и раскомментировать директиву "#define HAVE_SOCKLEN_T". Теперь проект должен скомпилироваться без ошибок.

    Теперь давайте напишем hello world, добавим в наш cpp файл следующее:

    PHP_FUNCTION(test);
    
    const zend_function_entry test_functions[] = {
    	PHP_FE(test, NULL)
    	{NULL, NULL, NULL}
    };
    
    zend_module_entry test_module_entry = {
    	STANDARD_MODULE_HEADER,       // #if ZEND_MODULE_API_NO >= 20010901
    	"test",                       // название модуля
    	test_functions,               // указываем экспортируемые функции
    	NULL,                         // PHP_MINIT(test), Module Initialization
    	NULL,                         // PHP_MSHUTDOWN(test), Module Shutdown
    	NULL,                         // PHP_RINIT(test), Request Initialization
    	NULL,                         // PHP_RSHUTDOWN(test), Request Shutdown
    	NULL,                         // PHP_MINFO(test), Module Info (для phpinfo())
    	"0.1",                        // версия нашего модуля
    	STANDARD_MODULE_PROPERTIES
    };
    
    ZEND_GET_MODULE(test)
    
    PHP_FUNCTION(test)
    {
    	RETURN_STRING("hello habr", 1);  // возвращаем PHP-строку, второй параметр указывает, нужно ли копировать строку в памяти или нет
    }

    Теперь подключим этот модуль в PHP и попробуем запустить что-нибудь такое:
    php -r "test();"
    На что мы должны получить ответ «hello habr».


    Компиляция под *nix


    В *nix'ах все оказалось как всегда проще. Я покажу на примере Debian, думаю, что под другими системами процесс не будет отличаться.
    Нам потребуется:
    • Иметь установленный PHP на машине,
    • Иметь установленный PHP-dev. Для этого нужно выполнить всего одну команду:
      apt-get install php5-dev
      
    Давайте создадим где-нибудь директорию для нашего расширения. Ну например /test. Там создадим два пустых файла:
    config.m4
    test.c
    

    Первый нужен для магической компиляции расширения, а во втором будет его исходный код. В config.m4 напишем следующее:
    PHP_ARG_ENABLE(test, Enable test support)
    
    if test "$PHP_TEST" = "yes"; then
       AC_DEFINE(HAVE_TEST, 1, [You have test extension])
       PHP_NEW_EXTENSION(test, test.c, $ext_shared)
    fi

    Внутри test.c добавьте
    #include "php.h"

    И после этой сроки скопируйте содержимое cpp-файла из Windows-версии.
    Теперь идем в консоль и:
    # phpize          // команда сгенерирует необходимые файлы для следующего шага
    # ./configure     // сгенерируется makefile
    # make            // компилируем
    # make install    // устанавливаем .so в директорию с PHP расширениями

    На этом все. Теперь можно открыть php.ini, добавить там свое расширение:
    extension=test.so
    

    И проверить его работоспособность командой
    php -r "test();"


    Обработка аргументов и возвращаемые значения



    Для начала посмотрим, как можно принимать аргументы:
    char* text;
    int text_length;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &text, &text_lenght) == FAILURE) { 
       return;
    }

    Третий параметр указывает ожидаемый тип (здесь можно просмотреть все варианты), в данном случае это char* или int. Также по ссылке можно найти варианты комбинирования типов и указания количества аргументов. Все следующие параметры являются переменными, в которые будут записаны переданные значения. При передаче строки передается сама строка и ее длина.
    Если количество аргументов, переданных в вашу функцию, не совпадает, будет выброшен E_WARNING, при этом вы можете возвратить какое-либо значение, например, сообщение об ошибке.

    Возвращать можно как простые типы, так и сложные. Давайте познакомимся с формированием возвращаемого массива. Для указания того, что будет возвращен массив, его нужно проинициализировать:
    array_init(result);
    

    Для добавления значений в массив необходимо использовать функции, зависящие от того, какой индекс и значение добавляется в массив. Например:
    add_next_index_long(result, 42);    // $result[] = 42;
    add_assoc_bool(result, "foo", 1);   // $result['foo'] = true;
    add_next_index_null(result);        // $result[] = NULL;
    

    Полный список функций можно найти здесь

    Если кого-то заинтересует, я могу в следующей статье рассмотреть пример работы с объектами (классический пример расширения на объектах — mysqli). Тут есть очень хорошая статья на эту тему.


    Производительность


    Для проверки производительности я выбрал несколько синтетический пример: подсчет вхождения каждого символа в строку. Другими словами, мы должны получить функцию, которая принимает строку в качестве параметра, и отдает массив, в котором указано количество употреблений каждого символа в данной строке. Этот пример продемонстрирует работу с большими строками.

    У меня получилась такая реализация, сильно не пинайте за код, я все-таки больше пишу на PHP, чем на C:

    PHP_FUNCTION(calculate_chars) {
    	char* text;
    	int text_length;
     
    	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &text, &text_length) == FAILURE) { 
    		return;
    	}
       
    	array_init(return_array);
    	int table[256] = { 0 };
    	for (int i = 0; i < text_length; i++) {
    		table[((unsigned char*)text)[i]]++;
    	}   
       
    	char str[2];
    	str[1] = '\0';
    	for (int i = 0; i < 256; i++) {
    		if (table[i]) {
    			str[0] = (char)i;
    			add_assoc_long(return_array, str, table[i]);
    		}
    	}
    }

    Этот код выдает следующий результат:
    user> php -r "print_r( calculate_chars('example') );"
    Array
    (
        [a] => 1
        [e] => 2
        [l] => 1
        [m] => 1
        [p] => 1
        [x] => 1
    }

    А теперь давайте сравним скорость выполнения этого кода и аналогичного на native PHP:

    $map = array();
    for ($i = 0; $i < $length; $i++) {
       $char = $text[$i];
       if (isset($map[$char])) {
          $map[$char]++;
       } else {
          $map[$char] = 1;
       }
    }

    Сравнивать я буду время выполнения обоих решений с помощью функции microtime. Возьмем строку в 100 символов, строку в 5000 символов, и строку в 69000 символов (я взял книгу A Message from the Sea, написанную Чарльзом Диккенсом, надеюсь, что он мне это простит), и для каждого варианта прогоним оба решения по несколько тысяч раз. Результаты приведены в таблице ниже. Тестирование проводилось на моем не самом сильном домашнем ноутбуке и VDS с Debian на борту, и да, я отчетливо понимаю, что результаты могут зависеть от конфигурации, от версии операционной системы, PHP, атмосферного давления и направления ветра, но я хотел показать лишь примерные цифры.
    Полный код тестового скрипта можно скачать здесь. Исходники и бинарники самих расширений можно скачать здесь (win) и здесь (nix).
    Кол-во итераций PHP code / Win32 PHP code / Debian PHP extension / Win32 PHP extension / Debian Win32 выигрыш Debian выигрыш
    1. Строка 100 символов 1000000 84.7566 сек 72.5617 сек 8.4750 сек 4.4175 сек в 10 раз в 16.43 раз
    2. Строка 5000 символов 10000 39.1012 сек 31.7541 сек 0.5001 сек 0.134 сек в 78.19 раз в 236.98 раз
    3. Строка 69000 символов 1000 52.3378 сек 44.0647 сек 0.4875 сек 0.0763 сек в 107.36 раз в 577.51 раз

    Выводы


    Если судить о производительности модуля по сравнению с интерпретируемым кодом, то мы видим, что ощутимые результаты можно получить на больших объемах данных и на малых количествах итераций. То есть, для часто использующихся, однако, не очень ресурсоемких алгоритмов не имеет смысла вынесение их в компилируемый код. Но для алгоритмов, работающих с большими объемами данных, это может иметь практический смысл. Также, опираясь на мои измерения, можно заметить, что результаты работы PHP-кода сравнимы на разных системах (напомню, что это были две разные машины), а вот результаты работы расширения очень сильно отличаются. Из этого лично я делаю вывод, что существуют какие-то особенности компиляции, которые мне не известны. Впрочем, я сильно сомневаюсь, что кто-то использует Windows-сервера для PHP-проектов. Хотя я также очень сомневаюсь, что кто-то прямо сейчас побежит переписывать что-то на С, эта статья все-таки больше just for fun, чем руководство к действию. Просто я хотел показать, что написать PHP extension очень просто, и иногда может быть очень полезно.

    UPD1. Сравнение с count_chars
    В комментах задали интересный вопрос: что если сравнить с производительностью функции count_chars?
    Я увеличил количество итераций в сто раз, и прогнал тот же самый тест, но уже с использованием этой функции. Можно увидеть, что на Debian результаты почти сравнялись, а под Windows наблюдается интересная ситуация: чем больше объем данных, тем больше мой модуль сливает в производительности. Напомню, что идея теста была не в том, чтобы написать велосипед, а в том, чтобы взять алгоритм для работы с большими объемами данных.
    Кол-во итераций count_chars / Win32 count_chars / Debian extension / Win32 extension / Debian Win32 выигрыш Debian выигрыш
    1. Строка 100 символов 10000000 67.5245 сек 47.8104 сек 81.8185 сек 43.8091 сек в 0.83 раз в 1.09 раз
    2. Строка 5000 символов 1000000 22.4693 сек 12.8959 сек 47.2514 сек 12.9577 сек в 0.48 раз в 0.99 раз
    3. Строка 69000 символов 100000 15.0681 сек 7.661 сек 46.9598 сек 7.7387 сек в 0.32 раз в 0.99 раз


    Материалы

    • PHP at the Core: A Hacker's Guide to the Zend Engine, php.net
    • Compiling shared PECL extensions with phpize, php.net
    • Creating a PHP Extension for Windows using Microsoft Visual C++ 2008, talkphp.com
    • Extension Writing Part I: Introduction to PHP and Zend, devzone.zend.com
    • Extension Writing Part II: Parameters, Arrays, and ZVALs, devzone.zend.com
    • Wrapping C++ Classes in a PHP Extension, devzone.zend.com
    Поделиться публикацией

    Комментарии 15

      +12
      А взяли бы SWIG, и повода для статьи не было.

      Впрочем, за раздел с производительностью спасибо. Обычно на сайтах типа stackoverflow любой вопрос про производительность заканчивается заплюсованным ответом «А не взять ли тебе профайлер и не посмотреть самому?». Возможно оно и правильно, но меня злит невероятно.
        +1
        Спасибо за ссылку, выглядит очень мощно. Правда не реализует весь функционал Zend API — например не нашел в документации ни слова про работу с массивам, хотя примеры работы с объектами впечатляют. Возможно, тоже когда-нибудь напишу об этом
        +1
        а если использовать count_chars? почти все велосипеды придуманы до нас, а те, которые не придуманы, не нуждаются в них )
          0
          Вы задали хороший вопрос, обновил топик :)
          +9
          В коде присутствует переполнение буфера, даже потенциально эксплоитируемое.
          Переполнение здесь: table[(int)text[i]]++;, text это указатель на знаковый тип char, который может принимать отрицательные значения. Если text содержит отрицательные числа, то у нас будет доступ к памяти перед массивом table, а т.е. table лежит в стеке — мы имеем классическое переполнение стекового буфера с возможностью перезаписи адреса возврата.
          Правильный код: table[((unsigned char*)text)[i]]++;
            0
            Пожалуй вы правы, мой C не очень хорош. Поправлю код. И конечно же я не призываю использовать этот код для production, хотя бы просто потому, что он не сможет работать с unicode строками, ну и потому, что не все хорошо с безопасностью, как оказывается
            0
            Вопрос немного не в тему, но может кто-нибудь знает. Есть апач в префорк режиме и модуль пхп. Возможно ли сделать так чтобы инициализация пхп модуля просиходила только один раз (трудоёмкие вычисления), а не в каждом инстансе. Или апач их всё же каждый раз с диска пускает, а не форком? Про mmap знаю ;)
              +1
              Я думаю время в последнем тесте отличается из-за того, что все же автор не мастер в С, но если его подкрутить, время должно быть почти равным.
              А вообще конечно верно выносить тяжелые обработки на языки более высокого уровня.
                0
                Наверное, имелось в виду «верно выносить тяжелые обработки на языки более низкого уровня»
                  0
                  Да, немного оговорился :)
                  Ну Вы понимаете, четверг, солнце за окном, а ты сидишь на своем чертовом стуле и пишешь код)))
                0
                Спасибо большое за статью.

                Непонятно, конечно, почему надо еще какие-то файлы выдергивать из 5.2 и там править define. Выглядит как-то неубедительно.

                Но в общем — очень хорошо, что все по полочкам разложено — то что надо начинающим )
                  0
                  Вот тут есть небольшое обсуждение вопроса по конфигурационному файлу. Он был в исходниках вплоть до 5.2.9, а потом был удален, и теперь разработчики рекомендуют генерировать его. Соответственно дефайны должны быть сгенерированы там тоже автоматически. Но самый простой путь — это скопировать его из предыдущих версий :)
                  0
                  Я тоже писал топик на тему расширений для PHP :)
                  Как раз описывал в том числе и работу с массивами, где-то выше спрашивали в комментах :)
                  habrahabr.ru/blogs/php/75696/
                    0
                    Каюсь, не нашел ваш топик через поиск. Но они вообщем-то дополняют друг друга по содержанию
                    0
                    Я попробовал собрать расширение для php 5.4.7 для Win32 (xampp) по этим инструкциям — и у меня получилось! Автору — спасибо!!! :-)

                    Только по дороге пришлось споткнуться о несколько камушков, хочу поделиться для автора и тех, кто захочет повторить мой эксперимент:

                    1. батника configure.bat изначально нет, он собирается другим батником — buildconf.bat, который требует наличия утилиты bison.exe (которая вроде бы должна находиться в папке %WINDIR%\system32\, но мне пришлось скачать ее отсюда: www.freewareweb.com/download/?f=bison.exe)

                    2. Директива #define PHP_COMPILER_ID «VC9» не помогает, так как после нее стоит директива #include «zend_config.w32.h», переопределяющая макрос PHP_COMPILER_ID в значение «VC10» (у меня используется именно такой компилятор) — мне пришлось еще закомментировать эту директиву в сгенерированном файле config.w32.h

                    3. проверять работоспособность расширения надо не командой php -r «test();» как рекомендует автор, а командой php -r «echo test();» — авторский вариант ничего никуда не выводит

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

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