Dzisiaj temat, o którym od dawna chciałem się zainteresować, ale jakoś ciężko było mi się za niego zabrać. Jak powszechnie wiadomo, język PHP do najszybszych nie należy. Wynika to po części z jego natury - bycia językiem skryptowym. Jednak "pod maską" PHP znajduje się kod napisany w języku C i w tym języku także można pisać rozszerzenia, które mogą być ładowane za pomocą konfiguracji php.ini.

W tym wpisie stworzymy proste rozszerzenie składające się z funkcji i zainstalujemy odpowiednie oprogramowanie, aby móc je skompilować i przetestować!

Na początek uwaga. Nie jestem specjalistą od języka C i raczej nigdy nie będę. Poniższy przykład pokaże, jak stworzyć pierwsze rozszerzenie, ale żeby napisać jakieś sensowne, to będziesz musiał poświęcić dużo więcej czasu na zgłębieniu tego tematu. Po więcej szczegółów zapraszam na stronę z dokumentacją.

Tworzymy kod rozszerzenia

Najpierw stwórzmy pusty katalog, np. src. Tutaj będziemy trzymać pliki źródłowe i konfigurację rozszerzenia. Umieśćmy w nim od razu 3 puste pliki:

  • config.m4 - zawiera informacje, jakie opcje są dostępne dla polecenia configure w rozszerzeniu,
  • custom_sum.c - plik źródłowy z kodem w języku C,
  • custom_sum.h - plik nagłówka w języku C.

W pliku config.m4 umieszczamy zawartość:

PHP_ARG_ENABLE(custom_sum, Sum of two numbers, [ --enable-custom_sum Enable customSum extension])
PHP_NEW_EXTENSION(custom_sum, custom_sum.c, $ext_shared)

Nasze rozszerzenie będzie nazywało się custom_sum, ale sama funkcja, którą będziemy wywoływać to customSum(arg1, arg2).

Teraz w pliku nagłówkowym (custom_sum.h) dodajemy zawartość:

#define EXTENSION_NAME      "custom_sum"
#define EXTENSION_VERSION   "1.0.0"

PHP_FUNCTION(customSum);

Możemy tutaj zdefiniować wersję rozszerzenia, co w tym przykładzie uczyniłem (stała EXTENSION_VERSION ustawiona na wartość 1.0.0).

Teraz dodamy plik źródłowy; tutaj będzie działo się najwięcej! Najpierw musimy załączyć odpowiednie pliki nagłówkowe:

#include <php.h>
#include "custom_sum.h"

Następnie zadeklarujemy "type hints" przyjmowanych przez funkcję argumentów. Jest tu dużo niskopoziomowego kodu, ale na nasze szczęście twórcy języka przygotowali dla nas makro, które to upraszcza:

ZEND_BEGIN_ARG_INFO_EX(arginfo_customSum, 0, 0, 2)
    ZEND_ARG_INFO(0, firstNumber)
    ZEND_ARG_INFO(0, secondNumber)
ZEND_END_ARG_INFO()

Jeżeli chcielibyśmy dodać tutaj kolejny argument, to wystarczy dodać kolejną linijkę z ZEND_ARG_INFO i podbić wartość parametru w makrze ZEND_BEGIN_ARG_INFO_EX.

Teraz wskażemy, że w module znajduje się ta funkcja:

zend_function_entry extension_functions[] = {
    PHP_FE(customSum, arginfo_customSum)
    {NULL, NULL, NULL}
};

zend_module_entry custom_sum_module_entry = {
    STANDARD_MODULE_HEADER,
    EXTENSION_NAME,
    extension_functions,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    EXTENSION_VERSION,
    STANDARD_MODULE_PROPERTIES
};

ZEND_GET_MODULE(custom_sum)

Teraz przygotujemy implementację naszej funkcji - będzie to funkcja sumująca. Jej implementacja będzie bardzo prosta:

static double customSum(double firstNumber, double secondNumber)
{
    return firstNumber + secondNumber;
}

Teraz pozostał ostatni krok - trzeba implementację funkcji w języku C połączyć z wywołaniem z modułu i przekazać odpowiednio parametry. Tutaj także możemy skorzystać z makra, aby trochę ułatwić sobie sprawę:

PHP_FUNCTION(customSum) {
    double firstNumber;
    double secondNumber;

    ZEND_PARSE_PARAMETERS_START(2,2)
        Z_PARAM_DOUBLE(firstNumber);
        Z_PARAM_DOUBLE(secondNumber);
    ZEND_PARSE_PARAMETERS_END();

    RETURN_DOUBLE(customSum(firstNumber, secondNumber));
}

Co się tutaj dzieje? Tak na prawdę deklarujemy sobie zmienne lokalne, następnie parsujemy zmienne otrzymane z wywołania rozszerzenia i przekazujemy je do funkcji napisanej już w języku C. Tutaj określamy faktycznie przyjmowaną liczbę parametrów - parametry makra ZEND_PARSE_PARAMETERS_START wskazują, że funkcja przyjmuje od użytkownika minimalnie 2 parametry i 2 parametry maksymalnie. Oba parametry są typu double.

Jeżeli chodzi o kod rozszerzenia to tyle! Napisaliśmy właśnie nasze pierwsze rozszerzenie do PHP! Teraz pora przygotować sobie środowisko, aby móc skompilować i przetestować to rozszerzenie.

Przygotowanie środowiska

Środowisko przygotujemy sobie - a jakże inaczej - z wykorzystaniem Dockera. Stwórzmy w katalogu projektu (obok katalogu src) plik Dockerfile z zawartością:

FROM ubuntu:20.04

RUN apt-get update \
    && apt-get install -y software-properties-common \
    && apt-add-repository ppa:ondrej/php
RUN apt-get update \
    && apt-get install -y \
        build-essential \
        php8.0 \
        php8.0-dev

COPY ./build.sh /var/data/build.sh

WORKDIR /var/data

Zbudowanie środowiska składa się z trzech kroków:

  • Dodajemy do systemu Ubuntu repozytoria z kodem PHP,
  • Instalujemy PHP oraz narzędzia potrzebne do zbudowania rozszerzenia,
  • Dodajemy skrypt budujący rozszerzenie.

Sam skrypt (plik build.sh) będzie miał zawartość:

phpize
./configure --enable-custom_sum
make
make install

Jak stworzyliśmy już te pliki, to teraz pora na fajerwerki. Uruchamiamy budowanie obrazu za pomocą polecenia:

docker build -tag=php-extension .

Samo zbudowanie obrazu potrwa kilka minut. Teraz pozostało nam stworzenie katalogu build (tutaj pojawi się plik rozszerzenia PHP z rozszerzeniem pliku .so) i uruchomić kompilację:

docker run -v "${PWD}/src/config.m4:/var/data/config.m4" -v "${PWD}/src/custom_sum.c:/var/data/custom_sum.c" -v "${PWD}/src/custom_sum.h:/var/data/custom_sum.h" -v "${PWD}/build:/var/data/modules" php-extension bash /var/data/build.sh

Powinien w tym momencie pojawić się w katalogu build plik rozszerzenia! Teraz możemy spróbować je uruchomić i przetestować działanie:

docker run -v "${PWD}/build:/var/data/modules" php-extension bash -c 'cp /var/data/modules/custom_sum.so $(php-config --extension-dir)/custom_sum.so && php -d extension=custom_sum.so -r \"echo customSum(6.1, 3.8);\"'

W wyniku otrzymamy na ekranie wartość 9.9. Dzieje się tak dlatego, że w powyższym poleceniu kopiujemy plik rozszerzenia do katalogu, gdzie są trzymane wszystkie moduły PHP (sprawdzamy to za pomocą komendy php-config), a następnie uruchamiamy zahardkodowany skrypt ładując stworzony wcześniej moduł.

To koniec tego wpisu, ale dopiero początek przygody z kompilacją własnych rozszerzeń dla PHP. Zapraszam do zapoznania się z artykułem na stronie PHP Internals Book, gdzie również jest wpis poświęcony tworzeniu funkcji, tłumaczący bardziej szczegółowo, co się tutaj dzieje. Dodatkowo podsyłam link do ciekawej dyskusji na StackOverflow.

Wszystkie utworzone dzisiaj pliki dostępne są do ściągnięcia tutaj.