ugrás a tartalomhoz

PHP kiterjesztések megvalósítása

darevish · 2012. Nov. 10. (Szo), 17.17
PHP kiterjesztések megvalósítása

Megtörténhet, hogy bővíteni szeretnénk a PHP funkcionalitását. Egyértelmű a kérdés: mégis hogyan? Miután a téma magyar nyelvű irodalma nem túl terebélyes, így gondoltam elkezdem, hátha lesz folytatása.

Előre bocsájtanám: rég volt már nekem is, hogy ezzel foglalkoztam, és most volt rá újra időm, viszont a lelkesedésem ez idő alatt alábbhagyott, ellenben kár lett volna veszni hagyni a maradékot. Többek között ebből adódóan a cikk nem teljeskörű leírás, inkább csak példák bemutatása, afféle tutorial és linkgyűjtemény. Bármiféle hasonlóság egyéb szerzők írásaival csak a véletlen műve.

A cikk legalább minimális C tudást feltételez. Ha sejted mi az a mutató, a header és a dinamikus memóriafoglalás, nagy baj már nem érhet.

A kiterjesztésekről

Miért írunk saját kiterjesztést?

  • Külső könyvtárat vagy az operációs rendszert szeretnénk elérni PHP-ból
  • Szeretnénk a PHP viselkedését megváltoztatni
  • Növelni szeretnénk a PHP kódunk sebességét vagy csökkenteni az erőforrásigényét
  • El szeretnénk adni a megvalósított funkciót anélkül, hogy kikerülne a kód

Mik azok a kiterjesztések?

Tulajdonképp PHP-ból meghívható, C nyelven írt függvények gyűjteményei. A legáltalánosabb közöttük a standard névre hallgat, ez tartalmazza a legalapvetőbb funkcionalitást (sztring, tömb stb. függvények), de jól ismerjük az adatbázisokkal kapcsolatos csomagokat és sok másikat is. Forráskódjuk nagyrészt az ext/ mappában található.

Hogy használja őket a PHP?

Amit PHP néven emlegetünk, alapvetően két részből áll: a Zend Engine-ből és a PHP-ból. A Zend Engine egy virtuális gép, ő felel többek közt a kód értelmezéséért, futtatásáért és a memóriakezelésért. A PHP felelős a SAPI-val való kommunikációért, a stream API kezeléséért (fájl és hálózati I/O) stb.

Mikor a SAPI elindítja a PHP-t, az végigmegy az összes bővítményen, ami a php.ini-ben be van jegyezve, és meghívja azok Module Initialization (MINIT) függvényét. Itt a moduloknak lehetősége van inicializálni magukat, illetve beregisztrálni a függvényeiket a Zend Engine-be. Ezután a PHP várakozik egy kérésre. Amikor ez megérkezik, meghívódik a modulok Request Initialization (RINIT) függvénye, ahol inicializálhatják a lekérés kiszolgálásához szükséges erőforrásaikat. Ezután a Zend Engine értelmezi és lefuttatja a szükséges kódot, és ha végzett, meghívódik a modulok Request Shutdown (RSHUTDOWN) függvénye. Itt van lehetőségük a bővítményeknek eltakarítaniuk maguk után. Amikor ezzel végeztek, lefut a garbage collection, ami a maradék szemetet is eltakarítja, és a PHP várakozik egy újabb kérésre vagy egy jelre, hogy leállhat. Ha utóbbi bekövetkezik, meghívódik a modulok Module Shutdown (MSHUTDOWN) függvénye, majd leáll maga a PHP is.

Hogy telepítsünk PHP-t kiterjesztés írásához?

Pont úgy, ahogy egyébként telepítenénk forrásból, csak a configure-nak adjunk meg két opciót:

--enable-debug --enable-maintainer-zts

Az első engedélyezi a debug szimbólumok használatát, így könnyebben deríthetjük ki mi a gond a kóddal, míg a második a szálbiztosságot, így a programunk minden környezetben hibátlanul futhat. Ha ezzel megvolnánk, neki is állhatunk.

Az első példa

Az első példában egy külső könyvtárhoz írunk burkolót. Sokáig kerestem, hogy melyik is legyen ez, és a választás végül Salvatore Sanfilippo SMAZ nevű projektjére esett.

GPL alatt közzétett könyvtárhoz nem készíthető kiterjesztés, mert az nem kompatibilis a PHP licenccel.

A SMAZ rövid sztringek tömörítésére szolgál. Nem biztos, hogy a lehető leghasznosabb tudás, viszont ami kifejezetten vonzóvá tette, hogy összesen két függvényt tartalmaz:

int smaz_compress(char *in, int inlen, char *out, int outlen);
int smaz_decompress(char *in, int inlen, char *out, int outlen);

Ennek a PHP-s megfelelője legyen:

string smapper_compress_to_string(string in [, int in_length = 0]);
string smapper_decompress_from_string(string in);

Előkészületek

Első körben töltsük le a SMAZ kódját valahova, ahol később könnyen elérjük. Utána válasszunk egy nevet a kiterjesztésünknek. Miután mind a PHP, mind a SMAZ licence megkér, hogy a ne használd az alkalmazásodban a nevüket, így én a SMAZ wrapper után a smapper nevet adtam neki.

Ezután válasszunk egy mappát, ahol a kódunkat tároljuk majd. Ez tradícionálisan a PHP ext/ mappája, de nem kötelező itt tartani (én sem tettem). Ha eddig még nem tettük, nyitunk egy terminált, belépünk az imént kiválasztott mappába, és kiadjuk a

$ <php_forrás>/ext/ext_skel --extname=smapper

utasítást. Ennek hatására létrejön egy smapper nevű mappa. Lépjünk be, mostantól itt fogunk ügyködni. Láthatjuk, hogy létrejött többek közt egy config.m4 nevű file. Létrehozunk ezen kívül egy smapper.c és egy php_smapper.h nevű fájlt is.

Kezdődhet a móka

Először megírjuk a kiterjesztések „Hello World”-jét, úgyszólván a szükséges tölteléket, majd kibővítjük a hasznos kóddal.

A config.m4 tartalmazza a szükséges konfigurációt, amiből a configure szkript lesz, később pedig a makefile. Kezdetben a sorok legtöbbje a dnl karaktersorral kezdődik, ez jelzi, hogy az adott sor komment. Egyelőre ennyit hagyunk belőle:

PHP_ARG_WITH(smapper, for smapper support, [--with-smapper[=DIR] Include smapper support])

if test "$PHP_SMAPPER" != "no"; then
  PHP_SUBST(SMAPPER_SHARED_LIBADD)
  PHP_NEW_EXTENSION(smapper, smapper.c, $ext_shared)
fi

Ezzel létrehozzuk a --with-smapper opciót a configure-nak, majd megmondjuk, hogy a smapper.c file-ban lesz a kódunk.

A php_smapper.h-ba kerülnek függvény deklarációk és minden más, ami egy header fájlba való: a modul általános adatai és a smapper_hello függvény deklarációja. A függvények nevei konvencionálisan a kiterjesztés nevével kezdődnek:

#ifndef PHP_SMAPPER_H
#define PHP_SMAPPER_H 1
#define PHP_SMAPPER_VERSION "1.0"
#define PHP_SMAPPER_EXTNAME "smapper"

PHP_FUNCTION(smapper_hello);

extern zend_module_entry smapper_module_entry;
#define phpext_smapper_ptr &smapper_module_entry

#endif

A smapper.c fájl tartalmazza a lényegi kódot:

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_smapper.h"

static function_entry smapper_functions[] = {
    PHP_FE(smapper_hello, NULL)
    {NULL, NULL, NULL}
};

zend_module_entry smapper_module_entry = {
    #if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
    #endif
    
    PHP_SMAPPER_EXTNAME,
    smapper_functions,
    NULL, /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
    
    #if ZEND_MODULE_API_NO >= 20010901
    PHP_SMAPPER_VERSION,
    #endif
    
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_SMAPPER
ZEND_GET_MODULE(smapper)
#endif

PHP_FUNCTION(smapper_hello) {
    php_printf("Hello World!\n");
}

Ez a fájl általános szerkezete. A sok NULL egymás alatt – mint ahogy a kommentek is mutatják – a már feljebb említett MINIT, RINIT stb. helye lesz, a smapper_functions-ben a modul függvényei lesznek összegyűjtve, alul pedig a kifejtett függvények következnek.

Mentsük el a fájlokat, és az adott mappában adjuk ki a következő utasításokat:

$ phpize
$ ./configure --with-smapper
$ make
$ sudo make install

A phpize parancs létrehozza a még szükséges fájlokat, a ./configure és a make lefordítja a smapper.c-t a mappánkon belüli modules/smapper.so fájlba, a make install pedig átmásolja ezt a PHP kiterjesztés mappájába (ennek alapértelmezett helye a /usr/local/lib/php/extensions).

Ha ezzel megvagyunk, nyissuk meg a php.ini-t, és illesszük be az

extension = smapper.so

sort, majd mentsük el.

Itt az idő, hogy kipróbáljuk, mit alkottunk:

$ php -r ‘smapper_hello();’

A zval típus

Ahhoz, hogy implementáljuk a paraméterátadást és a visszatérési értéket, érdemes tisztában lennünk azzal, hogy miként is tárolódnak a változóink. A PHP a változókat egy zval nevű struktúrában tárolja, aminek a szerkezete:

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount;
    zend_uchar type;
    zend_uchar is_ref;
} zval;

Ahol a zend_u… az unsigned … megfelelője. A refcount a másolatok száma, az is_ref mutatja, hogy a változó referencia-e, a type a típust tartalmazza, a value pedig a típusnak megfelelő értéket az alábbi unióban:

typedef union _zvalue_value {
    long   lval;           /* Egész érték*/
    double dval;           /* Lebegőpontos érték */
    
    struct {               /* Sztring érték */
        char *val;
        int   len;
    } str;
    
    HashTable *ht;         /* Hashtábla érték */
    zend_object_value obj; /* Objektumra mutató leíró */
} zvalue_value;

A visszatérési érték

Első ránézésre logikusabb lenne a paraméterátadásnál kezdeni, viszont abban a szerencsés helyzetben vagyunk, hogy minden függvény meghívásakor automatikusan létrejön egy zval* típúsú változó, a return_value, amivel a függvény visszatér. Erről meg is bizonyosodhatunk:

$ php -r ‘var_dump(smapper_hello());’
NULL

Először ezen fogunk kísérletezni egy kicsit, hogy megismerjük a típusokat a gyakorlatban, és annak a nagy halom makrónak egy részét, amit használhatunk.

A változó típusát a Z_TYPE_P(*zval) makróval határozhatjuk meg, amivel ekvivalensek a Z_TYPE(zval) és a Z_TYPE_PP(**zval). A lehetséges típusok a következők: IS_NULL, IS_BOOL, IS_LONG, IS_DOUBLE, IS_STRING, IS_RESOURCE, IS_ARRAY és IS_OBJECT.

Írjuk át a smapper_hello() függvényünket:

PHP_FUNCTION(smapper_hello) {
    switch(Z_TYPE_P(return_value)) {
        case IS_NULL:
            php_printf("Teremtom, hozzad kepest NULL vagyok\n");
            break;
        
        case IS_LONG:
            php_printf("Be nem all a SZAM, haha...\n");
            break;
        
        default:
            php_printf("Hello World!\n");
            break;
    }
}

Majd fordítsuk, és futtassuk újra:

$ make
$ sudo make install
$ php -r ‘var_dump(smapper_hello());’
…
NULL

Illesszük be a függvény elejére a következő sort:

Z_TYPE_P(return_value) = IS_LONG;

Láthatjuk, hogy a változó típusa immár int, értéke 0.

Most illesszük be a függvény végére az alábbi sorokat:

Z_TYPE_P(return_value) = IS_BOOL;
Z_LVAL_P(return_value) = 1;

Majd próbáljuk ki így is (boolean true). Láthatjuk, hogy így tudjuk változtatni a típust. A Z_…VAL_P(*zval) – illetve ugyanúgy a Z_…VAL(zval) és Z_…VAL_PP(**zval) – makrókkal (pl. double-höz Z_DVAL_P()) tudjuk az adott változó értékét beállítani, illetve lekérdezni.

Végül helyezzük a függvény végére a következő sort:

RETURN_DOUBLE(sqrt(3) / 2);

A visszatérési érték így float lesz. Visszatérési makrókból van még jópár: RETURN_LONG(long), RETURN_BOOL(zend_bool), RETURN_TRUE, RETURN_FALSE, RETURN_STRING(char*, int) stb. Hogy a RETURN_STRING miért vár egy második int paramétert (ami ha 1, az annyit jelent, hogy le kell másolni visszatérés előtt), arra később – a memóriakezelésnél – visszatérünk.

Függvényparaméterek

Kimenetünk már van, vessünk hát egy pillantást a bemenetre is. A megszokottól eltérő módon a paramétereket nem a függvény fejében deklaráljuk. A függvény meghívásakor kap egy referenciát a paraméterek listájáról, és azokat a függvény törzsében szedjük elő az

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "<paraméterlista>", ...) == FAILURE) {
    RETURN_NULL();
}

blokkban. A paraméterlista jelzi, hogy mennyi és milyen típusú paramétert várunk (lásd a táblázatot), míg az utána lévő változók ezek helyei lesznek. A ZEND_NUM_ARGS() a paraméterek száma, a TSRMLS_CC pedig a szálbiztosságot adja.

Amennyiben a lehetségesnél több vagy a kötelezőnél kevesebb paramétert kap, a visszatérési érték FAILURE lesz, ilyenkor keletkezik egy warning és a függvény azonnal visszatér.

Típus Jelölés Megvalósítás
integer l long*
float d double*
string s char**, int*
boolean b zend_bool*
resource r zval**
array a zval**
object o zval**
object (adott típussal) O zval**, típus
zval z zval**

Ezen kívül vannak különböző operátorok, amik a paraméterek parzolását befolyásolják:

Operátor Jelentés
| Az utána következő paraméterek opcionálisak
! A megelőző paraméter lehet NULL is
/ A megelőző paraméter elkülönítése, ha másolatainak száma nagyobb mint egy. Csak a, o, O, r és z típusra használható.

Nézzünk is rögtön egy példát. Írjuk meg a smapper_mynameis() függvényt. Előszor vegyük fel php_smapper.h-ban, majd a smapper_functions-ben, végül a megvalósítását írjuk a fájl végére:

static function_entry smapper_functions[] = {
    PHP_FE(smapper_hello, NULL)
    PHP_FE(smapper_mynameis, NULL)
    {NULL, NULL, NULL}
};
A smapper_functions-ben nincs vessző.
PHP_FUNCTION(smapper_mynameis) {
    char *name;
    int   name_length;
    
    long age;
    
    zend_bool show_age = 1;
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sl|b", &name, &name_length, &age, &show_age) == FAILURE) {
        RETURN_NULL();
    }
    
    php_printf("My name is %s", name);
    
    if (show_age) {
        php_printf(", I'm %d", age);
    }
    
    php_printf(".\n");
}

A függvény két kötelező, és egy opcionális paramétert vár, string, long, bool sorrendben. A zend_bool a boolean típus C-beli implementációja, 0, ha false, és konvencionálisan 1, ha true.

Az opcionális paramétereknek ne felejtsünk el alapértelmezett értéket adni. Ismét make, sudo make install és próba.

$ php -r ‘smapper_mynameis(“abc”, 123);’
My name is abc, I’m 123.
$ php -r ‘smapper_mynameis(“abc”, 123, false);’
My name is abc.

Új változót az ALLOC_INIT_ZVAL(zval*) vagy a MAKE_STD_ZVAL(zval*) makróval hozhatunk létre. Erre láthatunk majd példát a tömbök tárgyalásánál.

Közeledünk a megoldáshoz

Most, hogy ezzel is megvolnánk, kanyarodjunk vissza kicsit példánkhoz. Készítsük el a könyvtárat a letöltött smaz.c fájlból. Megtehetnénk persze, hogy felhasználjuk a kódot egy az egyben, de az sokkal kevésbé lenne tanulságos, mint így. Ezután a smaz.h-t másoljuk be a smapper/include mappába, a libsmaz.so-t pedig a smapper/lib-be.

Tehát elkészült a könyvtár, és csak arra vár, hogy összekössük a kódunkkal. Általános esetben a makefile-ban linkelnénk, viszont mi ezt a config.m4 alapján generáltatjuk. Nyissuk hát meg, és írjuk át erre:

PHP_ARG_WITH(smapper, for smapper support, [--with-smapper[=DIR] Include smapper support])

if test "$PHP_SMAPPER" != "no"; then
  SEARCH_PATH="/usr/local /usr" # you might want to change this
  SEARCH_FOR="/include/smaz.h"  # you most likely want to change this

  if test -r $PHP_SMAPPER/$SEARCH_FOR; then # path given as parameter
    SMAPPER_DIR=$PHP_SMAPPER
  else # search default path list
    AC_MSG_CHECKING([for smapper files in default path])
    
    for i in $SEARCH_PATH; do
      if test -r $i/$SEARCH_FOR; then
        SMAPPER_DIR=$i
        AC_MSG_RESULT(found in $i)
      fi
    done
  fi
  
  if test -z "$SMAPPER_DIR"; then
    AC_MSG_RESULT([not found])
    AC_MSG_ERROR([Please reinstall the smapper distribution])
  fi
  
  PHP_ADD_INCLUDE($SMAPPER_DIR/include)
  
  LIBNAME=smaz            # you may want to change this
  LIBSYMBOL=smaz_compress # you most likely want to change this
  
  PHP_CHECK_LIBRARY(
    $LIBNAME,
    $LIBSYMBOL,
    
    [
      PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $SMAPPER_DIR/lib, SMAPPER_SHARED_LIBADD)
      AC_DEFINE(HAVE_SMAPPERLIB, 1, [])
    ],
    
    [AC_MSG_ERROR([wrong smapper lib version or lib not found])],
    [-L$SMAPPER_DIR/lib -lm]
  )
  
  PHP_SUBST(SMAPPER_SHARED_LIBADD)

  PHP_NEW_EXTENSION(smapper, smapper.c, $ext_shared)
fi

A SEARCH_PATH-tól a PHP_ADD_INCLUDE-ig a smaz.h fájlt keressük – először a második sorban opcionálisan megadható mappában, majd ha ott nincs, akkor a SEARCH_PATH-ban megadott mappákban – és adjuk hozzá a PHP_ADD_INCLUDE-dal, utána pedig a könyvtárat a PHP_ADD_LIBRARY_WITH_PATH-al.

A smapper_hello() függvényünk törzsének a helyére másoljuk be a letöltött smaz_test.c main() függvényének tartalmát, majd a printf()-eket cseréljük le php_printf()-re, és a times változó értékét csökkentsük le 100-ra, mert úgy lényegesen gyorsabb lesz. Ezután a konzolban:

$ phpize
$ configure --with-smapper=<elérési út>/smapper/
$ make
$ sudo make install

Próbáljuk ki, hogy működik-e:

$ php -r ‘smapper_hello();’

Akár TEST PASSED, akár TEST NOT PASSED az eredmény, mi sikerrel jártunk, mert már elérjük a könyvtárat PHP kódból.

Memóriakezelés

C-ben nekünk kell arról gondoskodnunk, hogy a lefoglalt memóriát fel is szabadítsuk, mikor már nincs rá szükség. Ez a PHP kiterjesztések esetében sincs másként, viszont itt a Zend Engine ad némi segítséget, vagy alkalomadtán akár helyettünk is dolgozik.

Kiterjesztések esetében megkülönböztetünk tartós (persistent) és átmeneti (non-persistent) memóriafoglalást. Tartósnak az minősül, aminek adott kérés kiszolgálása után is meg kell maradnia, átmeneti az, amit a kérés végeztével felszabadíthatunk, és amit a Zend Engine ilyenkor fel is szabadít. Ehhez rendelkezésünkre áll néhány függvény, melyeket a következő táblázat tartalmaz:

C megfelelő átmeneti tartós
malloc(<méret>) emalloc(<méret>) pemalloc(<méret>, 1)
calloc(<elemszám>, <méret>) ecalloc(<elemszám>, <méret>) pecalloc(<elemszám>, <méret>, 1)
realloc(<mutató>, <méret>) erealloc(<mutató>, <méret>) perealloc(<mutató>, <méret>, 1)
free(<mutató>) efree(<mutató>) pefree(<mutató>, 1)
strdup(<sztring>) estrdup(<sztring>) pestrdup(<sztring>, 1)
strndup(<sztring>, <méret>) estrndup(<sztring>, <méret>) pestrndup(<sztring>, <méret>, 1)

A tartós függvények utolsó 1 paramétere egy flag, hogy tartós, tehát emalloc(size) == pemalloc(size, 0).

A visszatérési érték tárgyalásakor említettem, hogy megindoklom a RETURN_STRING(char*, int) makró második paraméterét. Miután a függvény futása véget ért, a Zend Engine törli az ott létrehozott változókat, majd a return_value által képviselt memóriaterületet is felszabadítja. Így statikus sztring esetén kétszer próbálná felszabadítani ugyanazt a területet, ami jó eséllyel szegmentálási hibához vezet. Ennek elkerülése végett jelezzük a RETURN_STRING()-nek (egy a második paraméterként átadott 1-gyel), hogy készítse el a saját másolatát.

Utolsó lépések

Most, hogy már ezen is túl vagyunk, minden ismeretünk megvan ahhoz, hogy megírjuk a függvényeket:

string smapper_compress_to_string(string in [, int in_length = 0]);

Mint ahogy eddig is, vegyük fel a függvényeket a php_smapper.h-ba, illetve a smapper.c-ben a smapper_functions-be, majd legalul adjuk meg a függvény törzsét:

PHP_FUNCTION(smapper_compress_to_string) {
    char *in, *out;
    
    long  in_length = 0;
    int  _in_length;
    
    int out_length;
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &in, &_in_length, &in_length) == FAILURE) {
        RETURN_NULL();
    }
    
    if (!_in_length || in_length > _in_length)
    {
        in_length = _in_length;
    }
    
    out = (char*) emalloc(sizeof (char) * in_length);
    out_length = smaz_compress(in, in_length, out, in_length);
    
    RETURN_STRINGL(out, out_length, 0);
}

Már csak egy valami lehet ismeretlen: a RETURN_STRINGL(char*, int length, int) makró. Ez nem más, mint a RETURN_STRING(char*, int) bináris változata, miután a tömörített sztring inkább fogható fel bájt tömbként, mint szövegként.

string smapper_decompress_from_string(string in);
PHP_FUNCTION(smapper_decompress_from_string) {
    char *in, *out;
    
    int  in_length;
    int out_length;
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &in, &in_length) == FAILURE) {
        RETURN_NULL();
    }
    
    out = (char*) emalloc(sizeof(char) * in_length * 5);
    out_length = smaz_decompress(in, in_length, out, in_length * 5);
    
    RETURN_STRINGL(out, out_length, 0);
}

Azért az ötös szorzó, mert feltételeztem, hogy a SMAZ nem képes 80%-ot tömöríteni a sztringen.

Ezután a már jól megszokott make, sudo make install, majd a próba (sokkal látványosabb, ha a tömörítés visszatérése előtt kiíratjuk az out_length értékét):

$ php -r 'var_dump(smapper_decompress_from_string(smapper_compress_to_string("This is the end of smapper extension.")));'
out_length: 18
string(37) "This is the end of smapper extension."

A második példa

Ebben a példában komplex számok tömbjét fogjuk megvalósítani, így megismerkedve először a tömb majd az erőforrás típussal.

Tömbök

A tömb egyéb változók csoportjának tárolására való típus, belső reprezentációja a zvalue_value unióban látott HashTable struktúra. Ugyan magához a hash táblához is hozzáférhetünk, a műveletek egy részét az alábbi függvényekkel egszerűen elvégezhetjük (az array változó mindenhol zval* típusú):

PHP C
$array = array(); array_init(array);
$array[] = NULL; add_next_index_null(array);
$array[] = 42; add_next_index_long(array, 42);
$array[] = true; add_next_index_bool(array, 1);
$array[] = 3.14; add_next_index_double(array, 3.14);
$array[] = 'foo'; add_next_index_string(array, "foo", 1);
$array[] = $variable; add_next_index_zval(array, variable);
$array[0] = NULL; add_index_null(array, 0);
$array[1] = 42; add_index_long(array, 1, 42);
$array[2] = true; add_index_bool(array, 2, 1);
$array[3] = 3.14; add_index_double(array, 3, 3.14);
$array[4] = 'foo'; add_index_string(array, 4, "foo", 1);
$array[5] = $variable; add_index_zval(array, 5, variable);
$array['abc'] = NULL; add_assoc_null(array, "abc");
$array['def'] = 711; add_assoc_long(array, "def", 711);
$array['ghi'] = true; add_assoc_bool(array, "ghi", 1);
$array['jkl'] = 1.44; add_assoc_double(array, "jkl", 1.44);
$array['mno'] = 'baz'; add_assoc_string(array, "mno", "baz", 1);
$array['pqr'] = $variable; add_assoc_zval(array, "pqr", variable);

Ha paraméterként kapjuk a tömböt, a hash táblájához a Z_ARRVAL_P(array), vagy a HASH_OF(array) makróval férhetünk hozzá. A paraméterként megkapott tömböt a

for (
    zend_hash_internal_pointer_reset_ex(HashTable*, HashPosition*);
    zend_hash_get_current_data_ex(HashTable*, void**, HashPosition*) == SUCCESS;
    zend_hash_move_forward_ex(HashTable*, HashPosition*)
) {
    …
}

ciklussal járhatjuk be.

Ha tömbbel szeretnénk visszatérni, akkor a fenti függvényeknek a return_value változót kell átadni.

Mindez a gyakorlatban

Hozzunk létre egy új kiterjesztést mca (My Complex Array) néven:

$ <php forrás>/ext/ext_skel --extname=mca

A config.m4-ben most az enable opciót hagyjuk meg, mivel nem használunk külső könyvtárat:

PHP_ARG_ENABLE(mca, whether to enable mca support, [--enable-mca Enable mca support])

if test "$PHP_MCA" != "no"; then
  PHP_SUBST(MCA_SHARED_LIBADD)
  PHP_NEW_EXTENSION(mca, mca.c, $ext_shared)
fi

Másoljuk le az előző példa elején létrehozott Hello Worldöt mca.c és php_mca.h néven, és cseréljük ki a smapper szavakat mca-ra, kis-nagybetű érzékenyen.

array mca_create_complex_array(array real [, array imaginary]);
PHP_FUNCTION(mca_create_complex_array) {
    zval      *real_array;
    zval *imaginary_array;
    
    zval **data;
    
    HashTable      *real_hash;
    HashTable *imaginary_hash;
    
    HashPosition pointer;
    
    int      real_count;
    int imaginary_count;
  
    double *native_data;
    
    int i;
    
    MAKE_STD_ZVAL(imaginary_array);
    array_init(imaginary_array);
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a|a", &real_array, &imaginary_array) == FAILURE) {
        RETURN_NULL();
    }
    
    real_hash  = Z_ARRVAL_P(real_array);
    real_count = zend_hash_num_elements(real_hash);
    
    imaginary_hash  = HASH_OF(imaginary_array);
    imaginary_count = zend_hash_num_elements(imaginary_hash);
    
    if (real_count <= 0) {
        php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Real array can't be empty");
        RETURN_BOOL(0);
    }
    
    if (imaginary_count != 0 && imaginary_count != real_count) {
        php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Imaginary array must be empty, or have the same number of elements as real array");
        RETURN_BOOL(0);
    }
    
    array_init(return_value);
    
    native_data = (double*) ecalloc(real_count * 2, sizeof (double));
    
    if (imaginary_count > 0) {
        i = real_count;
        
        for (
            zend_hash_internal_pointer_reset_ex(imaginary_hash, &pointer);
            zend_hash_get_current_data_ex(imaginary_hash, (void**) &data, &pointer) == SUCCESS;
            zend_hash_move_forward_ex(imaginary_hash, &pointer)
        ) {
            zval temporary = **data;
            
            // zval_copy_ctor(&temporary);
            
            convert_to_double(&temporary);
            native_data = Z_DVAL(temporary);
            
            // zval_dtor(&temporary);
            
            i++;
        }
    }
    
    i = 0;
    
    for (
        zend_hash_internal_pointer_reset_ex(real_hash, &pointer);
        zend_hash_get_current_data_ex(real_hash, (void**) &data, &pointer) == SUCCESS;
        zend_hash_move_forward_ex(real_hash, &pointer)
    ) {
        zval temporary = **data;
        
        convert_to_double(&temporary);
        native_data[i] = Z_DVAL(temporary);
        
        i++;
    }
    
    for (i = 0; i < real_count; i++) {
        zval *element;
        
        MAKE_STD_ZVAL(element);
        array_init(element);
        
        add_assoc_double(element, "real",      native_data[i]);
        add_assoc_double(element, "imaginary", native_data[i + real_count]);
        
        add_next_index_zval(return_value, element);
    }
    
    efree(native_data);
}

Előszor kiszedjük a paraméterként kapott tömbök tartalmát egy natív double tömbbe, majd ebből felépítjük az asszociatív tömböt, amivel visszatérünk. Több, egyelőre ismeretlen részletet is láthatunk a kódban. A legegyértelműbb ezek közül a php_error_docref() makró, ami hibák dobására szolgál. A hiba szövegébe (printf()-szerűen) változók értékeit is beszúrhatjuk. Először látjuk a convert_to_double(zval*) függvényt is. Ami feltűnhet, hogy a konverzió előtt lemásoljuk a változót, és a másolat típusát változtatjuk. Enélkül a paraméterben megkapott tömb is változna. Amennyiben a zval mutatót tartalmaz (pl. sztring), úgy nem elég magát a változót lemásolni, használni kell a zval_copy_ctor(zval*) függvényt, majd mikor már nincs szükségünk a másolatra, a zval_dtor(zval*) függvénnyel szabadíthatjuk fel az általa foglalt memóriát. Ezen kívül új elem még a zend_hash_num_elements(HashTable*) függvény, ami – nem meglepő módon – egy hash tábla elemszámát mondja meg.

Erőforrások

Láthattuk, hogy hogy tárolja a zval a változókat és a megfeleltetest a belső, illetve a PHP-beli típusok közt. Amennyiben egy C-beli típusnak nincs megfeleltethető párja (legyen ez mutató, struktúra vagy akármi más), úgy használhatjuk az erőforrás típust.

Amennyiben új típust szeretnénk beregisztrálni, azt a kiterjesztésünk MINIT() függvényében kell megtennünk. Vegyük fel a php_mca.h-ban – az eddigi fejlécek és a #define-ok közé – az új fejlécet:

PHP_MINIT_FUNCTION(mca);

Majd az mca.c-ben található mca_module_entry-be is jegyezzük be:

zend_module_entry mca_module_entry = {
    #if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
    #endif
    
    PHP_MCA_EXTNAME,
    mca_functions,
    PHP_MINIT(mca), /* MINIT */
    NULL,           /* MSHUTDOWN */
    NULL,           /* RINIT */
    NULL,           /* RSHUTDOWN */
    NULL,           /* MINFO */
    
    #if ZEND_MODULE_API_NO >= 20010901
    PHP_MCA_VERSION,
    #endif
    
    STANDARD_MODULE_PROPERTIES
};

Ezután valósítsunk is meg benne valamilyen funkcionalitást:

PHP_MINIT_FUNCTION(mca) {
    php_printf("mca MINIT\n");
}

Végül teszteljük. Korábban volt róla szó, hogy ez a függvény mindenképp meghívódik, ha kiterjesztésünk be van jegyezve a php.ini-be, így mindegy, hogy milyen kódot futtatunk:

$ php -r 'echo "valami\n";'
mca MINIT
valami

Térjünk vissza az erőforrásra. Legyen a megvalósítandó függvény prototípusa:

resource mca_create_resource(array real [, array imaginary]);

Vegyünk fel a php_mca.h-ba egy típust (nálam a MINIT() fejléce fölé került):

typedef struct _mca_array {
    double **complex_pointer;
    int pointer_size;
} mca_array;

#define MCA_ARRAY_RES_NAME "mca array"

A complex_pointer-ben fogjuk tárolni a számokat, a pointer_size nyilván ennek a mérete, míg a RES_NAME a típust fogja megjeleníteni, például egy var_dump()-nál.

Térjünk át az mca.c fájlra, és deklaráljunk egy globális int típusú változót az mca_functions fölött:

int le_mca_array;

Ehhez fogjuk hozzárendelni az adott erőforrás típus nevét és destruktor függvényét.

Írjuk át a MINIT() függvényünket:

PHP_MINIT_FUNCTION(mca) {
    le_mca_array = zend_register_list_destructors_ex(mca_array_destructor, NULL, MCA_ARRAY_RES_NAME, module_number);
}

Az mca_array_destructor lesz a destruktor függvény, amennyiben az erőforrásunk átmeneti, az utána lévő NULL a perzisztens erőforrások destruktorának a helye, a RES_NAME-ről már volt szó, a module_number pedig egy a PHP által generált szám, ami a kiterjesztésünket azonosítja.

Hozzuk létre a destruktort, a MINIT() függvény felett:

static void mca_array_destructor(zend_rsrc_list_entry *rsrc TSRMLS_DC) {
    int i;
    
    mca_array *array = (mca_array*) rsrc->ptr;
    
    if (array) {
        if (array->complex_pointer) {
            
            for(i = 0; i < array->pointer_size; i++) {
                if (array->complex_pointer[i]) {
                    efree(array->complex_pointer[i]);
                }
            }
        
            efree(array->complex_pointer);
        }
    
        efree(array);
    }
}

Itt felszabadítunk minden, az erőforrás létrehozásakor lefoglalt memóriát. Ezután az mca_create_resource() törzsében már nincs más dolgunk, mint a paraméterként kapott tömb(ök)ből feltölteni a double** mutatót, majd a ZEND_REGISTER_RESOURCE(zval*, void*, int) függvénnyel hozzárendelni a return_value változóhoz.

PHP_FUNCTION(mca_create_resource) {
    zval      *real_array;
    zval *imaginary_array;
    
    zval **data;
    
    mca_array *array;
    
    HashTable      *real_hash;
    HashTable *imaginary_hash;
    
    HashPosition pointer;
    
    int      real_count;
    int imaginary_count;
    
    int i;
    
    MAKE_STD_ZVAL(imaginary_array);
    array_init(imaginary_array);
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a|a", &real_array, &imaginary_array) == FAILURE) {
        RETURN_NULL();
    }
    
    real_hash  = Z_ARRVAL_P(real_array);
    real_count = zend_hash_num_elements(real_hash);
    
    imaginary_hash  = HASH_OF(imaginary_array);
    imaginary_count = zend_hash_num_elements(imaginary_hash);
    
    array = (mca_array*) emalloc(sizeof (mca_array));
    
    array->complex_pointer = (double**) emalloc(sizeof (double*) * real_count);
    array->pointer_size    = real_count;
    
    for (i = 0; i < real_count; i++) {
        array->complex_pointer[i] = (double*) ecalloc(2, sizeof (double));
    }
    
    if (imaginary_count > 0) {
        i = 0;
        
        for (
            zend_hash_internal_pointer_reset_ex(imaginary_hash, &pointer);
            zend_hash_get_current_data_ex(imaginary_hash, (void**) &data, &pointer) == SUCCESS;
            zend_hash_move_forward_ex(imaginary_hash, &pointer)
        ) {
            zval temporary = **data;
            
            convert_to_double(&temporary);
            array->complex_pointer[i][1] = Z_DVAL(temporary);
            
            i++;
        }
    }
    
    i = 0;
    
    for (
        zend_hash_internal_pointer_reset_ex(real_hash, &pointer);
        zend_hash_get_current_data_ex(real_hash, (void**) &data, &pointer) == SUCCESS;
        zend_hash_move_forward_ex(real_hash, &pointer)
    ) {
        zval temporary = **data;
        
        convert_to_double(&temporary);
        array->complex_pointer[i][0] = Z_DVAL(temporary);
        
        i++;
    }
    
    ZEND_REGISTER_RESOURCE(return_value, array, le_mca_array);
}

Gyártani már tudunk, itt az ideje, hogy hasznosítsuk is. Valósítsuk meg a void mca_print_resource(resource) függvényt, ami kiíratja a paraméterként kapott mutató tartalmát:

PHP_FUNCTION(mca_print_resource) {
    mca_array *array;
    zval *zarray;
    
    int i;
    
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &zarray) == FAILURE) {
        RETURN_FALSE;
    }
    
    ZEND_FETCH_RESOURCE(array, mca_array*, &zarray, -1, MCA_ARRAY_RES_NAME, le_mca_array);
    
    for (i = 0; i < array->pointer_size; i++) {
        php_printf("%f", array->complex_pointer[i][0]);
        
        if (array->complex_pointer[i][1] < 0) {
            php_printf(" - %fi", array->complex_pointer[i][1] * (-1));
        } else {
            php_printf(" + %fi", array->complex_pointer[i][1]);
        }
        
        php_printf("\n");
    }
}

Fordítsuk le és futtassuk:

$ php -r '$a = array(1.2, 2, 3); $b = array(4, -5, 6); $c = mca_create_resource($a, $b); mca_print_resource($c);'
1.200000 + 4.000000i
2.000000 - 5.000000i
3.000000 + 6.000000i

További irodalom

A témával foglalkozik (legjobb tudomásom szerint) két könyv:

  • Sara Golemon: Extending and Embedding PHP
  • George Schlossnagle: Advanced PHP Programming (ennek van magyar fordítása is)

És pár cikk, illetve tutorial:

És persze erre a témára is igaz, hogy jó eséllyel találod meg a kérdésedet válaszokkal együtt a StackOverflow-n.

A bélyegképen Sylvain Mayer fényképe látható.

 
darevish arcképe
darevish
~
1

Ez igen!

Hidvégi Gábor · 2012. Nov. 12. (H), 11.02
Köszönjük ezt a részletes cikket, szép munka!

Ha van egy projekt, amiben nagy osztályokkal dolgozunk, megéri azokat kiszervezni PHP kiterjesztésbe, hogy gyorsabb legyen a futásuk?
3

Nem hiszem.

darevish · 2012. Nov. 12. (H), 19.18
Bár saját tesztjeim erre nincsenek, az általános nézet az, hogy fejlesztési időben nem éri meg. Tényleg sokkal tovább tart, és ha kellőképp komplex, akkor C kóder kell mind a megíráshoz, mind a karbantartáshoz. Nem mellesleg, a témakör nem mondható jól dokumentáltnak, elég sok idő kell a szükséges ismeretanyag összekukázásához.
Amennyiben csak nagyon kevés, de számításigényes feladatot kell elvégezni, úgy talán érdemes lehet, de a PHP-ban készült alkalmazások - amennyire én látom - ritkán tartoznak ebbe a kategóriába.
Opció még, hogy a memóriahasználatot szeretnéd visszavenni, én ebben az esetben is inkább hw-t bővítenék.
Amire tényleg érdemes használni, az a wrapperek írása, ha bármi PHP-ból nem, de C-ből kezelhető dolgot szeretnél bevonni.

És köszönöm a visszajelzéseket :)
5

HipHop

bbalint2 · 2012. Nov. 15. (Cs), 14.01
(szvsz) akármilyen opcache (PHP "binárist" gyorsítótárazó megoldás) használatával már jelentős gyorsulás érhető el - ha ez a cél.

ha naggyon gyors PHP kódot szeretnél, akkor ott a Facebook által használt, fejlesztett HipHop for PHP (Google keresés: Facebook HipHop), mely a gyakorlatban C++ kódot generál a .php filékből s webszerver meg PHP helyett valódi binárisokat fogsz kapni.

kiterjesztésbe átteni kódot meg "hülyeség", mivel bármilyen módosításhoz kb. újra kell indítani a webszervert; a hibakeresés, debug-olás is jóval körülményesebb, pl.:
.php kódék:
  1. beírom a forrásba, hogy
    
    <?php
     if(isset($_REQUEST['debugSQL'])){
      echo mysql_error();
      echo $query;
      
      exit;
     }
    ?>
  2. lekérem újra a fájlt a debugSQL paraméterrel
  3. ???
  4. DEBUG!

PHP kiterjesztés:
  1. beírom a forrásba, hogy
    
     char *requestKulcs = "_REQUEST";
     zval **_REQUEST;
     char *kulcs = "debugSQL";
     zval **talalat;
     
     zend_is_auto_global(requestKulcs, ZEND_STRL(requestKulcs) TSRMLS_CC);
     
     if( zend_hash_find( &EG(symbol_table), requestKulcs, strlen(requestKulcs) + 1, (void**) _REQUEST) == SUCCESS
     && zend_hash_find( _REQUEST, kulcs, strlen(kulcs) + 1, (void**) talalat) == SUCCESS){
      /* 
       * na, mégse írom le, mert itt elfogyott a türelmem (a példa összeszedéséhez):
       * a fenti két index-keresést meg kéne ismételni, a globális változótérben (a $query változó), aztán
       * pl. php_printf()-fel a kimenetre tenni a talált értéket
       *
       * ésakkor még abba se mentem bele, hogy a mysql_error()-t, illetve
       * annak a C megfelelőjét meg kéne hívni; ehhez mellékesen még kell
       * a MySQL kapcsolat, illetve az azt tároló változó, mutató, de az
       * egy másik kiterjesztéshez (mysql) tartozik és ...
       */
     }
    
  2. kiterjesztés újrafordítása (make && sudo make install)
  3. webszerver újraindítása
  4. ???
  5. DEBUG!

ebben a verzióban amúgy bárhol, ha hiba van, akkor 1-től 3-ig meg kell ismételni a műveleteket.

remélem eme rövid példával sikerül(t) meggyőződni, hogy nem olyan egyszerű az a "PHP kiterjesztésbe szervezés"...
6

HipHop? PHC!

joed · 2012. Nov. 17. (Szo), 20.14
A modulokkal egy kontextusban a HipHop for PHP kicsit sántít. A HipHop ugyanis tudtommal nem a Zend Engine-re épít, nem lehet modulozni, hanem inkább egy nagy monolitikus binárist kapsz a fordítás végén. Erre kínál egy megoldást a HHVM, ami egyfajta opcode cache virtual machine. De ha már opcode optimizer, akkor szerintem Zend Optimizer és társai (ezt akár vitaindítónak is tekinthetitek ;-)
Korábban érdekes ötletnek találtam a PHC-t, de elég kínlódás volt működésre bírni.
2

Én is szeretném megragadni az

dropout · 2012. Nov. 12. (H), 13.32
Én is szeretném megragadni az alkalmat, hogy köszönetet mondjak: tanulságos olvasmány volt.
4

Nagyon komoly..

aboy · 2012. Nov. 15. (Cs), 10.05
..az anyag. Köszi :)