Вызов C функций из PHP: обёртки, расширения и встроенные средства
Способы вызова C-кода из PHP
Интеграция PHP с языком C позволяет повысить производительность критических участков, использовать существующие библиотеки и напрямую работать с системными ресурсами. Основных подходов три: написание собственного расширения с использованием C API PHP, применение встроенного расширения FFI (Foreign Function Interface) и генерация обёрток через SWIG или PHP-CPP. Каждый вариант подходит для разных сценариев.
Как написать простое расширение PHP на C?
Создание расширения даёт полный контроль над производительностью и возможностью расширять синтаксис. Типичные случаи: реализация высоконагруженных алгоритмов (например, обработка видео, криптография) или интеграция с уникальным оборудованием.
Для минимального расширения требуется файл с функциями-заглушками и файл конфигурации config.m4. Пример:
// php_hello.c
#include <php.h>
PHP_FUNCTION(hello_world) {
php_printf("Hello from C!\n");
}
const zend_function_entry hello_functions[] = {
PHP_FE(hello_world, NULL)
PHP_FE_END
};
zend_module_entry hello_module_entry = {
STANDARD_MODULE_HEADER,
"hello",
hello_functions,
NULL, NULL, NULL, NULL, NULL,
"0.1",
STANDARD_MODULE_PROPERTIES
};
ZEND_GET_MODULE(hello)
C api php (c api на php)
// config.m4
PHP_ARG_ENABLE(hello, whether to enable hello support,
[ --enable-hello Enable hello support])
if test "$PHP_HELLO" != "no"; then
PHP_NEW_EXTENSION(hello, php_hello.c, $ext_shared)
fi
Типичные ошибки:
- Забыть включить
php.h- компилятор выдаёт неопределённые типы. - Неправильное имя функции:
PHP_FUNCTIONдолжно совпадать с именем вPHP_FE. - Пропуск
ZEND_GET_MODULE- расширение не загружается.
Как передать параметры из PHP в C-функцию расширения?
Используйте макросы zend_parse_parameters() для извлечения аргументов. Это обеспечивает безопасность типов и автоматическое преобразование.
PHP_FUNCTION(add_two) {
zend_long a, b;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ll", &a, &b) == FAILURE) {
RETURN_THROWS();
}
RETURN_LONG(a + b);
}
Здесь строка формата "ll" указывает на два целых числа. Поддерживаются и другие типы: строки ("s"), числа с плавающей точкой ("d"), массивы ("a") и объекты ("o").
Частая проблема:
Если реальный аргумент не соответствует спецификатору, PHP выдаёт предупреждение. Рекомендуется всегда проверять результат zend_parse_parameters().
Когда использовать FFI вместо расширения?
FFI (доступен с PHP 7.4) позволяет вызывать функции из динамических библиотек без написания C-кода. Это удобно для быстрого прототипирования или интеграции с библиотеками, где не нужна максимальная производительность.
// example.php
$ffi = FFI::cdef(
"int printf(const char *format, ...);",
"libc.so.6"
);
$ffi->printf("Привет из C!\n");
Однако FFI накладывает накладные расходы на вызовы и не поддерживает сложные структуры без дополнительной работы.
Проблема:
Различные платформы могут иметь разные имена библиотек (libc.so.6 на Linux, libc.dylib на macOS). Используйте константы или проверки среды.
Как автоматически сгенерировать обёртку для C-библиотеки?
Инструменты SWIG и PHP-CPP упрощают создание расширений. SWIG генерирует код на основе заголовочных файлов C, а PHP-CPP предоставляет C++-подобный API.
Пример использования SWIG:
/* example.i */
%module example
%{
#include "example.h"
%}
int add(int a, int b);
$ swig -php example.i
$ gcc -c example.c example_wrap.c -I/usr/include/php/... -fpic
$ gcc -shared example.o example_wrap.o -o example.so
После этого в PHP можно вызывать example_add().
Ошибки:
- Пути к заголовкам PHP должны точно соответствовать версии и конфигурации сборки.
- При использовании SWIG возможен конфликт имён, если функция уже определена в другом модуле.
Наиболее эффективное решение: написание расширения на C с использованием PHP API
Этот метод обеспечивает наилучшую производительность, полную интеграцию с механизмами памяти и исключениями PHP, а также доступ к внутренним структурам (например, zval). Рекомендуется для production-систем, где важна скорость и надёжность.
Пример полноценного расширения, принимающего строку и возвращающее её длину с проверкой типов:
// string_utils.c
#include <php.h>
PHP_FUNCTION(string_length) {
zend_string *str;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &str) == FAILURE) {
RETURN_THROWS();
}
RETURN_LONG(ZSTR_LEN(str));
}
const zend_function_entry utils_functions[] = {
PHP_FE(string_length, NULL)
PHP_FE_END
};
zend_module_entry utils_module_entry = {
STANDARD_MODULE_HEADER,
"string_utils",
utils_functions,
NULL, NULL, NULL, NULL, NULL,
"1.0",
STANDARD_MODULE_PROPERTIES
};
ZEND_GET_MODULE(string_utils)
// config.m4
PHP_ARG_ENABLE(string_utils, whether to enable string_utils support,
[ --enable-string-utils Enable string_utils support])
if test "$PHP_STRING_UTILS" != "no"; then
PHP_NEW_EXTENSION(string_utils, string_utils.c, $ext_shared)
fi
После компиляции и установки расширения в PHP вызов string_length("test") вернёт 4.
Возможные проблемы:
- Игнорирование
memory management: работа с zval без корректного увеличения refcount приводит к утечкам. - Неверное указание строки формата:
"S"принимает именноzend_string*, а не простую строку. Если нужно принимать строку с возможным NULL, используют"s". - Для компиляции требуется установленный PHP с dev-заголовками (пакет php-dev).
Расширенные примеры работы с C API PHP
Пример 1: Возврат массива из C расширения
Создание функции, которая возвращает массив чисел Фибоначчи до заданного предела.
// fibonacci.c (фрагмент)
PHP_FUNCTION(fibonacci) {
zend_long limit;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &limit) == FAILURE) {
RETURN_THROWS();
}
array_init(return_value);
zend_long a = 0, b = 1;
while (a <= limit) {
add_next_index_long(return_value, a);
zend_long next = a + b;
a = b;
b = next;
}
}
Результат в PHP:
$arr = fibonacci(50); print_r($arr); // Array ( [0] => 0 [1] => 1 [2] => 1 [3] => 2 [4] => 3 [5] => 5 [6] => 8 [7] => 13 [8] => 21 [9] => 34 )
Пример 2: Работа с пользовательскими структурами через FFI
Вызов библиотеки libcurl через FFI без расширения. Требуется только наличие libcurl.so.
$ffi = FFI::cdef(
"
void *curl_easy_init();
int curl_easy_setopt(void *curl, int option, ...);
int curl_easy_perform(void *curl);
void curl_easy_cleanup(void *curl);
",
"libcurl.so"
);
$curl = $ffi->curl_easy_init();
if ($curl === null) {
echo "Ошибка инициализации curl";
exit;
}
$ffi->curl_easy_setopt($curl, 10002, "https://example.com"); // CURLOPT_URL
$ffi->curl_easy_perform($curl);
$ffi->curl_easy_cleanup($curl);
Результат: на экран выводится содержимое страницы. Обратите внимание, что константы (CURLOPT_URL) нужно задавать численно.
Трудности:
- Необходимо знать точные числовые значения опций.
- Обработка ошибок через возвращаемые коды требует чтения документации.
Пример 3: Использование PHP-CPP для быстрого создания расширения
PHP-CPP позволяет писать расширения на C++ с более простым синтаксисом. Установка через Composer не требуется, нужны только исходники библиотеки.
// mymath.cpp
#include <phpcpp.h>
Php::Value multiply(Php::Parameters ¶ms) {
int a = params[0];
int b = params[1];
return a * b;
}
extern "C" {
PHPCPP_EXPORT void *get_module() {
static Php::Extension extension("mymath", "1.0");
extension.add<multiply>("multiply", {
Php::ByVal("a", Php::Type::Numeric),
Php::ByVal("b", Php::Type::Numeric)
});
return extension;
}
}
Компиляция: g++ -std=c++11 -fpic -c mymath.cpp -I/usr/include/phpcpp и последующая сборка в .so.
Результат в PHP:
echo multiply(5, 6); // 30
Пример 4: Обработка исключений в расширении
Генерация исключения PHP из C-кода с помощью zend_throw_exception().
PHP_FUNCTION(divide) {
zend_long a, b;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ll", &a, &b) == FAILURE) {
RETURN_THROWS();
}
if (b == 0) {
zend_throw_exception(zend_ce_exception, "Деление на ноль", 0);
RETURN_THROWS();
}
RETURN_DOUBLE((double)a / b);
}
Поведение: при вызове divide(10,0) PHP выбросит исключение и завершит выполнение, если оно не перехвачено.
Пример 5: Работа с глобальными переменными PHP из C
Доступ к глобальной переменной $GLOBALS через хеш-таблицу EG(symbol_table).
PHP_FUNCTION(get_global_var) {
zend_string *name;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &name) == FAILURE) {
RETURN_THROWS();
}
zval *value = zend_hash_find(&EG(symbol_table), name);
if (value) {
RETURN_ZVAL(value);
} else {
RETURN_NULL();
}
}
Применение: позволяет получить доступ к любой глобальной переменной из C. Следует быть осторожным с изменением refcount.