Pull to refresh

Пишем PHP extension

Reading time 8 min
Views 33K
А давайте сегодня взглянем на 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
Tags:
Hubs:
+113
Comments 16
Comments Comments 16

Articles