I'm using SWIG to generate a PHP extension over GLib which uses callbacks. To allow using PHP user-space functions as callbacks, i'm using something like:
The Wrapper (registers a unique callback dispatcher to handle all signal emissions):
/* {{{ proto void my_signal_connect(resource $instance, string $signal, mixed $callback, mixed $additional_args) }}}*/
ZEND_NAMED_FUNCTION(_wrap_my_signal_connect) {
GstObject *instance = (GstObject *) 0 ;
gchar *signal = (gchar *) 0 ;
zval *zcallback = (zval *) 0 ;
zval *zargs = (zval *) 0 ;
zval **args[4];
gulong result;
struct the_callback_struct *cb;
GType itype;
guint signal_id;
GSignalQuery *signal_info;
char *callback_name;
/* parse arguments */
SWIG_ResetError();
if(ZEND_NUM_ARGS() != 4 || zend_get_parameters_array_ex(4, args) != SUCCESS) {
WRONG_PARAM_COUNT;
}
{
if(SWIG_ConvertPtr(*args[0], (void **) &instance, 0, 0) < 0) {
if((*args[0])->type==IS_NULL) instance = 0;
else SWIG_PHP_Error(E_ERROR, "Wrapper: Type error in argument 1. Expected SWIGTYPE_p_p_void");
}
}
if((*args[1])->type == IS_NULL) {
signal = (gchar *) 0;
} else {
convert_to_string_ex(args[1]);
signal = (gchar *) Z_STRVAL_PP(args[1]);
}
MAKE_STD_ZVAL(zcallback);
*zcallback = **args[2];
zval_copy_ctor(zcallback);
MAKE_STD_ZVAL(zargs);
*zargs = **args[3];
zval_copy_ctor(zargs);
/* query the signal system for in-depth info about the signal */
{
itype = G_TYPE_FROM_INSTANCE((GObject *) instance);
signal_id = g_signal_lookup((const gchar *) signal, itype);
if(signal_id == 0) {
SWIG_PHP_Error(E_ERROR, "The object does not emit the given signal");
}
signal_info = (GSignalQuery *) emalloc(sizeof(*signal_info));
g_signal_query(signal_id, signal_info);
}
/* get the function name or object + method name */
cb = (struct callback_struct *)emalloc(sizeof(*cb));
if(zcallback->type == IS_NULL) {
SWIG_PHP_Error(E_ERROR, "Wrapper: Type error in callback argument.");
}
if(zcallback->type == IS_ARRAY) {
HashTable *ht = Z_ARRVAL_P(zcallback);
int n = zend_hash_num_elements(ht);
if(n == 2) {
if(zend_hash_index_find(ht, 0, (void **)&cb->target) == SUCCESS && Z_TYPE_PP(cb->target) == IS_OBJECT) {
if(zend_hash_index_find(ht, 1, (void **)&tmp2) == SUCCESS && Z_TYPE_PP(tmp2) == IS_STRING) {
MAKE_STD_ZVAL(cb->fx);
*cb->fx = **tmp2;
zval_copy_ctor(cb->fx);
}
}
}
} else if(zcallback->type == IS_STRING) {
cb->target = NULL;
MAKE_STD_ZVAL(cb->fx);
*cb->fx = *zcallback;
zval_copy_ctor(cb->fx);
} else {
SWIG_PHP_Error(E_ERROR, "Wrapper: Type error in callback argument.");
}
/* Validate callback */
if(zend_is_callable(cb->fx, 0, &callback_name) == FAILURE) {
efree(callback_name);
SWIG_PHP_Error(E_ERROR, "Invalid callback");
}
/* copy the args into the structure */
MAKE_STD_ZVAL(cb->args);
*cb->args = *zargs;
zval_copy_ctor(cb->args);
cb->signal_id = signal_info->signal_id;
cb->signal_name = signal_info->signal_name;
cb->signal_flags = signal_info->signal_flags;
cb->itype = signal_info->itype;
cb->return_type = signal_info->return_type;
cb->n_params = signal_info->n_params;
cb->param_types = signal_info->param_types;
/* connect the signal handler */
result = (gulong)g_signal_connect(instance, signal, G_CALLBACK(my_signal_dispatcher), (gpointer) cb);
{
ZVAL_LONG(return_value,result);
}
return;
fail:
zend_error(SWIG_ErrorCode(),"%s",SWIG_ErrorMsg());
}
The callback struct:
struct callback_struct {
zval **target;
zval *fx;
zval *args;
GType itype; /* The type of object/instance which emitted the signal */
guint signal_id; /* The signal id (or 0 if the signal is unknown) */
const gchar *signal_name; /* The signal name */
GSignalFlags signal_flags; /* The signal flags (as declared when creating the signal) */
GType return_type; /* The return type for the callback */
guint n_params; /* The number of parameters of the callback */
const GType *param_types; /* The parameter types for callback arguments */
};
The signal dispatcher maps the signal handling to a PHP user space function:
static void my_signal_dispatcher(gpointer instance, ...) {
int i = 0, addr;
gpointer arg, ref;
zval retval;
zval *arglist[3];
struct callback_struct *cb;
/* add emitter instance to arg list */
SWIG_SetPointerZval(arglist[i++], (void *) instance, SWIGTYPE_p__GObject, 1);
va_list ap;
va_start(ap, instance);
/* fetch the variable list of arguments */
while((addr = va_arg(ap, int)) > 2) {
arg = (gpointer) addr;
if(G_IS_OBJECT(arg)) {
SWIG_SetPointerZval(arglist[i++], (void *) arg, SWIGTYPE_p__GObject, 1);
} else {
cb = (struct callback_struct *) arg;
MAKE_STD_ZVAL(arglist[i]);
*arglist[i] = *cb->args;
zval_copy_ctor(arglist[i]);
i++;
break;
}
}
va_end(ap);
if(cb->target == NULL) {
if(call_user_function(EG(function_table), NULL, cb->fx, &retval, i, arglist TSRMLS_CC) == SUCCESS) {
zval_dtor(&retval);
}
} else {
if(call_user_function(NULL, cb->target, cb->fx, &retval, i, arglist TSRMLS_CC) == SUCCESS) {
zval_dtor(&retval);
}
}
zval_ptr_dtor(cb->target);
zval_dtor(cb->fx);
zval_dtor(cb->args);
efree(cb);
}
I'am able to build the extension, and to connect a PHP signal handler (callback) to a given signal, for example:
<?php
//...
function cb() {
$s = array();
foreach(func_get_args() as $arg) {
$s[] = gettype($arg) == 'resource' ? 'Resource '.get_resource_type($arg) : (gettype($arg) == 'object' ? 'Object '.get_class($arg) : gettype($arg));
}
$s = implode(', ', $s);
echo " { PHP user-space: cb($s) } ";
return 1;
}
//...
myextension::my_signal_connect($instance, "child-added", array('one' => 1));
?>
so, when $instance emits the "child-added" signal i got the output from the cb() PHP function, and the following error:
{ PHP user-space: cb(Resource _p__GObject, Resource _p__GObject, array) }
*** glibc detected *** php: free(): invalid pointer: 0x095080c8 ***
======= Backtrace: =========
/lib/tls/i686/cmov/libc.so.6(+0x6b591)[0xb95591]
/lib/tls/i686/cmov/libc.so.6(+0x6cde8)[0xb96de8]
/lib/tls/i686/cmov/libc.so.6(cfree+0x6d)[0xb99ecd]
/usr/lib/php5/20090626+lfs/myextension.so(+0x2a477)[0x7510477]
php[0x831c024]
php(zend_hash_del_key_or_index+0x112)[0x831af82]
php(_zend_list_delete+0x8c)[0x831c2ec]
php(_zval_dtor_func+0xb2)[0x830b872]
php(_zval_ptr_dtor+0x4d)[0x82ff00d]
php[0x82ff0c9]
php(zend_call_function+0x764)[0x8301694]
php(call_user_function_ex+0x64)[0x83023b4]
php(call_user_function+0x6b)[0x830242b]
/usr/lib/php5/20090626+lfs/gstreamer.so(+0x93c2d)[0x7579c2d]
/usr/lib/libgobject-2.0.so.0(g_cclosure_marshal_VOID__OBJECT+0x88)[0xd262d8]
======= Memory map: ========
00110000-0026e000 r-xp 00000000 08:04 440863 /usr/lib/libdb-4.8.so
0026e000-00270000 r--p 0015d000 08:04 440863 /usr/lib/libdb-4.8.so
00270000-00271000 rw-p 0015f000 08:04 440863 /usr/lib/libdb-4.8.so
...
I've tried to ref the GObject instances using g_object_ref() when connecting the signal before adding to the arguments list, without success
Any help?
Related
I'm write simple extension with class definition
extension.h
zend_class_entry * ExampleClass_class;
zend_class_entry * get_ExampleClass_class();
extension.c
#include "php.h"
#include "extension.h"
...
zend_class_entry * get_ExampleClass_class(){
return ExampleClass_class;
}
....
PHP_METHOD(ExampleClass, getInstance){
ZEND_PARSE_PARAMETERS_START(0, 0)
Z_PARAM_OPTIONAL
ZEND_PARSE_PARAMETERS_END();
RETURN_OBJ(
// ----------- fun objectToZval(obj: PhpObject) = obj.zval //CPointer<zval>
example_symbols()->kotlin.root.php.extension.proxy.objectToZval(
example_symbols()->kotlin.root.exampleclass.getInstance(
// ------- Unused parameter
example_symbols()
->kotlin.root.php.extension.proxy.phpObj(
ExampleClass_class, getThis()
)
// ------- Unused parameter end
)
)
)
}
Also I write and compile static library with logic realization (Kotlin Native)
.def
static inline zval* zend_helper_new_ExampleClass() {
zval *obj = malloc(sizeof(zval));
object_init_ex(obj, get_ExampleClass_class());
return obj;
}
.kt
fun newExampleClass() = zend_helper_new_ExampleClass()!!
//PhpObject is wrapper for two fields CPointer<zend_class_entry> and CPointer<zval>
class PhpObject(val context: CPointer<zend_class_entry>, val zval: PhpMixed) {
companion object {
fun fromMixed(zval: PhpMixed) = PhpObject(zval.pointed!!.value.obj!!.pointed!!.ce!!, zval)
}
....
}
val PhpMixed.phpObject get() = PhpObject.fromMixed(this)
fun getInstance(obj: PhpObject) = newExampleClass().phpObject
Finally I run PHP code
var_dump(ExampleClass::getInstance());
And receive this
# /opt/rh/rh-php71/root/usr/bin/php -dextension=`ls ./phpmodule/modules/*.so` -r "var_dump(ExampleClass::getInstance());"
*RECURSION*
#
Where I mistaken?
UPD
static inline zval* zend_helper_new_{className}() {
zval *obj = malloc(sizeof(zval));
object_init_ex(obj, get_{className}_class());
php_printf("Just created FLAGS %u\n", GC_FLAGS(obj->value.obj));
return obj;
}
Just created object have GC_FLAGS equals 0
*RECURSIVE* apears in function php_var_dump by code
case IS_OBJECT:
if (Z_IS_RECURSIVE_P(struc)) {
PUTS("*RECURSION*\n");
return;
}
Macro->macro->macro->Oh god!->macro->macro...
Z_IS_RECURSIVE_P(struc) = (GC_FLAGS((*(zval)).value.counted) & GC_PROTECTED)
Okay...
php_printf("%d\n", GC_FLAGS((*(obj)).value.counted));
Returns 0
Must not trigger *RECURSIVE*, but... Why!?
First
For compilation I used PHP 7.1.8, but coding based on latest sources.
Recursion protection has been changed 06.10.2017
Actual var_dump code for 7.1.8
case IS_OBJECT:
if (Z_OBJ_APPLY_COUNT_P(struc) > 0) {
PUTS("*RECURSION*\n");
return;
}
But it doesn't matter
Second
RETURN_OBJ(
example_symbols()->kotlin.root.php.extension.proxy.objectToZval(
example_symbols()->kotlin.root.exampleclass.getInstance(/*unused*/)
)
)
Let's expand the macro RETURN_OBJ (r)
RETURN_OBJ(r)
{ RETVAL_OBJ(r); return; }
{ ZVAL_OBJ(return_value, r); return; }
.
{ do {
zval *__z = (return_value);
Z_OBJ_P(__z) = (r);
Z_TYPE_INFO_P(__z) = IS_OBJECT_EX;
} while (0); return; }
.
{ do {
zval *__z = (return_value);
Z_OBJ(*(__z)) = (r);
Z_TYPE_INFO(*(__z)) = (IS_OBJECT | (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT));
} while (0); return; }
.
{ do {
zval *__z = (return_value);
(*(__z)).value.obj = (r);
(*(__z)).u1.type_info = (8 | ((1<<0) << 8));
} while (0); return; }
You see? :)
Yea, this macro must receive zend_object but not zval
Just change return expression to
example_symbols()->kotlin.root.php.extension.proxy.zendObject(
example_symbols()->kotlin.root.exampleclass.getInstance(/*unused*/)
)
where
fun zendObject(obj: PhpObject) = obj.zval.pointed!!.value.obj!!
Bingo!
PS Special thanks for php developers community for incredible documented macro hell
I want to build an PHP-Extension which internally calls C++-Functions returning C++-Objects. I know I have to wrap these C++-Objects in zval containers to provide them to the PHP-level.
//if returnval is long
if (color != NULL) {
RETURN_LONG(color->GetRGBRed());
}
//if returnval is bool
if (color != NULL) {
RETURN_BOOL(color->GetRGBRed());
}
//if returnval is double
if (color != NULL) {
RETURN_DOUBLE(color->GetRGBRed());
}
//if returnval is string without length
if (color != NULL) {
RETURN_STRING(color->GetRGBRed(),1; //1 or 0
}
//if returnval is string with length
if (color != NULL) {
RETURN_STRINGL(color->GetRGBRed(),15,1);
}
//if returnval is an object
if (color != NULL) {
zval *new_object;
MAKE_STD_ZVAL(new_object);
if(object_init(new_object) != SUCCESS)
{
zend_error(E_WARNING, "Objekt konnte bei Return nicht erzeugt werden.");
}
new_object->type=IS_OBJECT;
zend_hash_update(
EG(active_symbol_table),
"new_variable_name",
strlen("new_variable_name") + 1,
&new_variable,
sizeof(zval *),
NULL
);
}
As you see, it is easy to return a numeric value or a boolean or a string into the PHP-Space. But at the end of my code you see my attempt to return a custom C++-Object in a zval.
I have trouble with a custom extension in php.
I am extening Php::ArrayAccess for a self made object and I am able to use my object in PHP juste like a native array. BUT I can not chain the operators [] even though I am returning a reference to my object in the implementation of offsetGet. I get this error :
PHP Fatal error: Cannot use object of type Jq as array in ... (myfile.php on line 0)
#include <phpcpp.h>
#include <iostream>
#include <sstream>
#include <algorithm>
class Jq : public Php::Base, public Php::ArrayAccess
{
public:
Jq()
{
}
virtual ~Jq()
{
}
void __construct(Php::Parameters& params)
{
std::string localParam1 = params[0];
std::string localParam2 = params[1];
_pathToJq = localParam1;
_pathToCacheFile = localParam2;
}
Php::Value __toString()
{
return asString();
}
Php::Value asString()
{
std::ostringstream os;
os << _pathToJq << ' ' << _pathToCacheFile << " : " << _filters.str();
return os.str();
}
virtual bool offsetExists(const Php::Value &key) override
{
return true;
}
virtual Php::Value offsetGet(const Php::Value& key) override
{
return &((*this)[key]);
}
virtual void offsetSet(const Php::Value &key, const Php::Value &value) override
{
}
virtual void offsetUnset(const Php::Value &key) override
{
}
Jq& operator[] (const std::string& key)
{
const std::string offset = key;
if (is_number(offset)) {
if (_filters.tellp() > 0) {
_filters << '[' << offset << ']';
} else {
_filters << ".[" << offset << ']';
}
} else {
_filters << '.' << offset;
}
return *this;
}
private:
std::string _pathToJq;
std::string _pathToCacheFile;
std::ostringstream _filters;
std::ostringstream _chainedOutput;
bool is_number(const std::string& s)
{
return !s.empty() && std::find_if(s.begin(), s.end(), [](char c) { return !std::isdigit(c); }) == s.end();
}
};
/**
* tell the compiler that the get_module is a pure C function
*/
extern "C" {
/**
* Function that is called by PHP right after the PHP process
* has started, and that returns an address of an internal PHP
* strucure with all the details and features of your extension
*
* #return void* a pointer to an address that is understood by PHP
*/
PHPCPP_EXPORT void *get_module()
{
// static(!) Php::Extension object that should stay in memory
// for the entire duration of the process (that's why it's static)
static Php::Extension extension("jq", "0.0.1");
// #todo add your own functions, classes, namespaces to the extension
Php::Class<Jq> jq("Jq");
jq.method("__construct", &Jq::__construct);
jq.method("__toString", &Jq::__toString);
jq.method("asString", &Jq::asString);
//jq.method("offsetGet", &Jq::offsetGet);
// add the class to the extension
extension.add(std::move(jq));
// return the extension
return extension;
}
}
and the php code to be executed:
<?php
$jqa = new Jq("pathJQ", "pathCache");
// This is fine !
echo $jqa['test'] . PHP_EOL;
// This is fine too !
echo $jqa . PHP_EOL;
// But This is not !
echo $jqa['coc']['players'][0]['name'] . PHP_EOL;
Thank's for your help !
I had a query in my application that looked like this:
$stmt = db::db()->prepare('INSERT INTO t(a,b) VALUES :a,:b)');
$stmt->execute(array(1,2));
Later, I had an unrelated error, and when reviewing this script, I asked myself why it ever worked previously. I would have expected I should have used
$stmt->execute(array('a'=>1,'b'=>2));
But, it seems to work?
Is it acceptable to use an non-associated array with a prepared statement where the placeholders are array keys and not question marks?
Not that I will get into this practice, but this had me stumped and I just need to know.
Thank you
Yes PDO does care, In this scenario prepare would not fail because the client sends a partial query to the database server,
this query has to be completely minus the data so it can be pre-processed.
The client then sends the data, either once or multiple times, the server then executes the pre-processed query with the data.
Howerver the following exception should have been raised when executing:
{ "HY093", "Invalid parameter number" }
If you read the source code of PDO for the execute function you will see the following piece of code that throws this error:
if (HASH_KEY_IS_STRING == zend_hash_get_current_key_ex(Z_ARRVAL_P(input_params),
¶m.name, &str_length, &num_index, 0, NULL)) {
/* yes this is correct. we don't want to count the null byte. ask wez */
param.namelen = str_length - 1;
param.paramno = -1;
} else {
/* we're okay to be zero based here */
if (num_index < 0) {
pdo_raise_impl_error(stmt->dbh, stmt, "HY093", NULL TSRMLS_CC);
RETURN_FALSE;
}
param.paramno = num_index;
}
I am not sure how it worked for you so I would make sure PDO errormode attribute is set.
You still have to be careful because you must take care of proper order of the elements in the array that we are passing to the PDOStatement::execute() method.
execute() full function from source code
/* {{{ proto bool PDOStatement::execute([array $bound_input_params])
Execute a prepared statement, optionally binding parameters */
static PHP_METHOD(PDOStatement, execute)
{
zval *input_params = NULL;
int ret = 1;
PHP_STMT_GET_OBJ;
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|a!", &input_params)) {
RETURN_FALSE;
}
PDO_STMT_CLEAR_ERR();
if (input_params) {
struct pdo_bound_param_data param;
zval **tmp;
uint str_length;
ulong num_index;
if (stmt->bound_params) {
zend_hash_destroy(stmt->bound_params);
FREE_HASHTABLE(stmt->bound_params);
stmt->bound_params = NULL;
}
zend_hash_internal_pointer_reset(Z_ARRVAL_P(input_params));
while (SUCCESS == zend_hash_get_current_data(Z_ARRVAL_P(input_params), (void*)&tmp)) {
memset(¶m, 0, sizeof(param));
if (HASH_KEY_IS_STRING == zend_hash_get_current_key_ex(Z_ARRVAL_P(input_params),
¶m.name, &str_length, &num_index, 0, NULL)) {
/* yes this is correct. we don't want to count the null byte. ask wez */
param.namelen = str_length - 1;
param.paramno = -1;
} else {
/* we're okay to be zero based here */
if (num_index < 0) {
pdo_raise_impl_error(stmt->dbh, stmt, "HY093", NULL TSRMLS_CC);
RETURN_FALSE;
}
param.paramno = num_index;
}
param.param_type = PDO_PARAM_STR;
MAKE_STD_ZVAL(param.parameter);
MAKE_COPY_ZVAL(tmp, param.parameter);
if (!really_register_bound_param(¶m, stmt, 1 TSRMLS_CC)) {
if (param.parameter) {
zval_ptr_dtor(¶m.parameter);
}
RETURN_FALSE;
}
zend_hash_move_forward(Z_ARRVAL_P(input_params));
}
}
if (PDO_PLACEHOLDER_NONE == stmt->supports_placeholders) {
/* handle the emulated parameter binding,
* stmt->active_query_string holds the query with binds expanded and
* quoted.
*/
ret = pdo_parse_params(stmt, stmt->query_string, stmt->query_stringlen,
&stmt->active_query_string, &stmt->active_query_stringlen TSRMLS_CC);
if (ret == 0) {
/* no changes were made */
stmt->active_query_string = stmt->query_string;
stmt->active_query_stringlen = stmt->query_stringlen;
ret = 1;
} else if (ret == -1) {
/* something broke */
PDO_HANDLE_STMT_ERR();
RETURN_FALSE;
}
} else if (!dispatch_param_event(stmt, PDO_PARAM_EVT_EXEC_PRE TSRMLS_CC)) {
PDO_HANDLE_STMT_ERR();
RETURN_FALSE;
}
if (stmt->methods->executer(stmt TSRMLS_CC)) {
if (stmt->active_query_string && stmt->active_query_string != stmt->query_string) {
efree(stmt->active_query_string);
}
stmt->active_query_string = NULL;
if (!stmt->executed) {
/* this is the first execute */
if (stmt->dbh->alloc_own_columns && !stmt->columns) {
/* for "big boy" drivers, we need to allocate memory to fetch
* the results into, so lets do that now */
ret = pdo_stmt_describe_columns(stmt TSRMLS_CC);
}
stmt->executed = 1;
}
if (ret && !dispatch_param_event(stmt, PDO_PARAM_EVT_EXEC_POST TSRMLS_CC)) {
RETURN_FALSE;
}
RETURN_BOOL(ret);
}
if (stmt->active_query_string && stmt->active_query_string != stmt->query_string) {
efree(stmt->active_query_string);
}
stmt->active_query_string = NULL;
PDO_HANDLE_STMT_ERR();
RETURN_FALSE;
}
/* }}} */
Is there any method to access to list of registered shutdown functions?
You can write an extension and look at BG(user_shutdown_function_names). Probably easier is to make a wrapper for register_shutdown_function that saves the shutdown functions to some array and call it instead.
(Untested)
#include "ext/standard/basic_functions.h"
//usual include suspects here
typedef struct _php_shutdown_function_entry {
zval **arguments;
int arg_count;
} php_shutdown_function_entry;
static void _shutdown_function_dtor(php_shutdown_function_entry *shutdown_function_entry) /* {{{ */
{
int i;
for (i = 0; i < shutdown_function_entry->arg_count; i++) {
zval_ptr_dtor(&shutdown_function_entry->arguments[i]);
}
efree(shutdown_function_entry->arguments);
}
static int _build_shutdown_array(php_shutdown_function_entry *entry, zval *arr TSRMLS_DC)
{
zval *inner;
zval *args;
int i;
array_init(inner);
array_init(args);
Z_ADDREF_P(entry->arguments[0]);
add_assoc_zval(inner, "callback", entry->arguments[0]);
for (i = 1; i < entry->arg_count; i++) {
Z_ADDREF_P(entry->arguments[i]);
add_next_index_zval(args, entry->arguments[i]);
}
add_assoc_zval(inner, "arguments", args);
add_next_index_zval(arr, inner);
}
PHP_FUNCTION(list_shutdown_functions)
{
if (zend_parse_parameters_none() == FAILURE)
return;
if (!BG(user_shutdown_function_names)) {
ALLOC_HASHTABLE(BG(user_shutdown_function_names));
zend_hash_init(BG(user_shutdown_function_names), 0, NULL,
(void (*)(void *)) _shutdown_function_dtor, 0);
}
array_init(return_value);
zend_hash_apply_with_argument(BG(user_shutdown_function_names),
(apply_func_arg_t) _build_shutdown_array, return_value TSRMLS_CC);
}
Other than keeping track yourself, no. The list of registered function names is not exposed to your PHP scripts. If you're open to extending PHP itself (this would be a simple task) then see Artefacto's answer.