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-ztsAz 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.
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=smapperutasí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)
fiEzzel 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 installA 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.sosort, 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());’
NULLElő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());’
…
NULLIllesszü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}
};
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)
fiA 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 installPró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=mcaA 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)
fiMá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
valamiTé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.000000iTová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:
- szintén Sara Golemontól egy három részes tutorial első, második és harmadik része
- Kristina Chodorow négyrészes tutorialja
- Dokumentáció a PHP oldalán
- Egy diasor PHP5 kiterjesztésekről
- C++ osztályok használata PHP kiterjesztésekben
- Egy tutorial, amiben van benchmark Fibonacci sebességről
É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ó.
■



Ez igen!
Ha van egy projekt, amiben nagy osztályokkal dolgozunk, megéri azokat kiszervezni PHP kiterjesztésbe, hogy gyorsabb legyen a futásuk?
Nem hiszem.
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 :)
HipHop
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:
PHP kiterjesztés:
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"...
HipHop? PHC!
Korábban érdekes ötletnek találtam a PHC-t, de elég kínlódás volt működésre bírni.
Én is szeretném megragadni az
Nagyon komoly..