Вызов C функций из PHP: обёртки, расширения и встроенные средства

Раздел: Программирование -> Интеграция с C

Способы вызова 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 &params) {
    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.

C API на PHP - comments

En
C api php (php)