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.
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}
};
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:
- 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..