On writing php 7 extension, or How do I write a php extension
Our company has a long history of moving technical stack from here to there. At first everything was built on PHP, with Memcached, MySQL. Now we’re moving to Java, use diferent technologies for backend, and frontend application: new webserver, new database system, some is inhouse, some is opensource database (redis, for example).
The hard thing about maitaining both old and new architecture is we have to keep both well functional, at its best performance, with updated features, everything cannot be migrated at once, and the result is we have to strike to solved many performance issues of the old php application.
Some A/B testing shows PHP7 makes the huge performance gain, compare with php5, with less code migration. Problem is some of our extensions work well with php5, are not compatiple with php7, most of them was built using swig , which is slowly developed to adapt php7 changes.
I tried to port old extension to work with php7. PHP extension is poor, old, and absolute documentation. Then I found php-cpp, a C++ library for developing php extension, it also supports php7.
PHP-CPP document is well updated, easy to understand, and support most php extension features. I can quickly port one extension to php7 using its old swig defination file, also keep the old php user interface, our developer do not have to change anything in their php code. After some tests we discover php-cpp has memory leak problem, and it does not support php built with threadsafe (zts) enabled.
All problems above lead us to the option: writing native php extension.
I have experience on writing Apache Httpd and nginx module, and those API are also poorly document, same with PHP. The best way to learn how to write nginx, or apache httpd module is reading others module, and follow their style, trial and error. So I came across php-src and read their extensions. Although finally (1) i can write native extension for php7, I am still not happy to write php extension again.
In this post you will find my experience on how to write a php extension.
Some magic
PHP Zend API introduces many magic things, from how to declare global values, to how to return a value to user space. For example, to declare a global value, you will have to do this:
ZEND_BEGIN_MODULE_GLOBALS(example)
int counter;
ZEND_END_MODULE_GLOBALS(example)
Behind the scene, ZEND_BEGIN_MODULE_GLOBALS is a macro which transform your module name example
to a struct name _zend_example_globals
, and ZEND_END_MODULE_GLOBALS is another macro which end your defination struct, and give your struct a new name: zend_example_globals
#define ZEND_BEGIN_MODULE_GLOBALS(module_name) \
typedef struct _zend_##module_name##_globals {
#define ZEND_END_MODULE_GLOBALS(module_name) \
} zend_##module_name##_globals;
all these code will be translated to this:
typedef struct _zend_example_globals {
int counter;
} zend_example_globals;
Why not just make thing explicit? Guess how we return a string value to user space, when you can example->getValue()? Here is how it is implemented:
ZVAL_STRING(return_value, (char*) value);
return_value
is internal ZEND API variable, you give it a value, that value will be returned to user space, magic!
Writing our extension
Rest of this post I will decribe how to write a php extension, with class defination, and its method. Hope someone will find it useful. In php, you usually create new class instance by doing this:
$c = new myClass();
introduction
To declare myClass
in php extension, you will have to do these following steps:
- define a customize struct
- map it to a zend_class_entry, write your own struct constructor and destructor, PHP will call these methods to create and destroy your class instance
- define a method to convert from zend_class_entry to your original struct
- and finally write a
__construct
method and register that method to your php extension
implementation
here is how you create a class example in php extension (comment included in below lines of code)
// define customize struct
typedef struct {
char* a_value;
zend_object std; //your struct must contain zend standard object
} example_t;
// a method to convert zend_class_entry to your original object
static inline example_t* example_obj_fetch(zend_object* obj) {
return (example_t*) ((char*) (obj) - XtOffsetOf(example_t, std));
}
// later you can use Z_EXAMPLE_P(zend_object) to retrieve your original object
#define Z_EXAMPLE_P(zv) example_obj_fetch(Z_OBJ_P((zv)))
// object constructor and destructor
static inline zend_object* example_obj_new(zend_class_entry *ce) {
example_t* example;
example = ecalloc(1, sizeof (example_t) + zend_object_properties_size(ce));
zend_object_std_init(&example->std, ce);
object_properties_init(&example->std, ce);
example->std.handlers = &example_obj_handlers;
return &example->std;
}
static void example_obj_free(zend_object *object) {
example_t* example;
example = example_obj_fetch(object);
if (!example) {
return;
}
zend_object_std_dtor(&example->std TSRMLS_CC);
efree(example);
}
// declare a standard zend object handler
static zend_object_handlers example_obj_handlers;
// map your class to zend_class_entry, have to do it within PHP_MINIT_FUNCTION
static zend_class_entry *example_ce;
PHP_MINIT_FUNCTION(example) {
.....
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "example", example_methods);
example_ce = zend_register_internal_class(&ce TSRMLS_CC);
example_ce->create_object = example_obj_new;
memcpy(&example_obj_handlers, zend_get_std_object_handlers(), sizeof (example_obj_handlers));
example_obj_handlers.offset = XtOffsetOf(example_t, std);
example_obj_handlers.free_obj = example_obj_free;
.....
}
// construct method
PHP_METHOD(example, __construct) {
char* value;
strlen_t len = 0;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &value, &len) == FAILURE) {
RETURN_FALSE;
}
return_value = getThis();
example_t* example;
example = Z_EXAMLE_P(return_value);
example->a_value = value;
}
// register construct method to extension
ZEND_BEGIN_ARG_INFO_EX(arginfo_example_none, 0, 0, 0)
ZEND_END_ARG_INFO()
zend_function_entry example_methods[] = {
PHP_ME(example, __construct, arginfo_example_none, ZEND_ACC_CTOR | ZEND_ACC_PUBLIC)
}
static method
In php you can call a static method, by doing example::some_static_method()
, you also can define static method in php extension, below is the function to set a_value
for class example, but it’s static method:
PHP_METHOD(example, set_value) {
char* value;
strlen_t len = 0;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &value, &len) == FAILURE) {
RETURN_FALSE;
}
object_init_ex(return_value, example_ce);
example_t* example;
example = Z_EXAMLE_P(return_value);
example->a_value = value;
}
because we do not construct method by calling new example()
, but example::set_value("something")
, we must construct object ourself inside our method, this is where we call object_init_ex
with return_value and example class entry we defined before, register this static method so we can call it from php user space:
zend_function_entry example_methods[] = {
PHP_ME(example, __construct, arginfo_example_one, ZEND_ACC_CTOR | ZEND_ACC_PUBLIC)
PHP_ME(example, set_value, arginfo_example_one, ZEND_ACC_STATIC | ZEND_ACC_PUBLIC)
PHP_FE_END
}
Now we can use $e = example::set_value("something");
from php code, we will get the same result as $e = new example("something");
retrieve value
To get value you assigned when initialised class example by new example("A value")
, we need to define a get method
PHP_METHOD(example, get) {
example_t* example;
if (zend_parse_parameters_none() == FAILURE) {
RETURN_FALSE;
}
example = Z_EXAMLE_P(getThis());
ZVAL_STRING(return_value, (char*) example->a_value);
}
and register it
zend_function_entry example_methods[] = {
PHP_ME(example, __construct, arginfo_example_one, ZEND_ACC_CTOR | ZEND_ACC_PUBLIC)
PHP_ME(example, get, arginfo_example_none, ZEND_ACC_PUBLIC)
PHP_FE_END
};
initialize & shutdown function
A standard php extension has to implement (at least) 2 global function: PHP_MINIT_FUNCTION, PHP_MSHUTDOWN_FUNCTION, and here is it:
PHP_MINIT_FUNCTION(example) {
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "example", example_methods);
example_ce = zend_register_internal_class(&ce TSRMLS_CC);
example_ce->create_object = example_obj_new;
memcpy(&example_obj_handlers, zend_get_std_object_handlers(), sizeof (example_obj_handlers));
example_obj_handlers.offset = XtOffsetOf(example_t, std);
example_obj_handlers.free_obj = example_obj_free;
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(example) {
return SUCCESS;
}
PHP_MINIT_FUNCTION to register our class entry, and how to handle it. it’s where we do step 2, map zend standard class entry to our struct, and define its object handler.
Finally, register own extension, and everything will be ready to go:
register php module
zend_module_entry example_module_entry = {
STANDARD_MODULE_HEADER,
"example",
NULL,
PHP_MINIT(example),
PHP_MSHUTDOWN(example),
PHP_RINIT(example),
NULL,
PHP_MINFO(example),
EXAMPLE_VERSION,
STANDARD_MODULE_PROPERTIES
};
ZEND_GET_MODULE(example)
configuration/metadata
To install php extension, php require a metadata file which provides extension name, and extension dependencies, where to get include header, which files need to be compiled along with our main source file. Because our extension is pretty simple, we just need to provide simpile metadata, put lines below in config.m4:
dnl $Id$
dnl config.m4 for extension example
dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.
dnl If your extension references something external, use with:
PHP_ARG_WITH(example, for example support,
dnl Make sure that the comment is aligned:
[ --with-example Include example support])
dnl Otherwise use enable:
dnl PHP_ARG_ENABLE(example, whether to enable example support,
dnl Make sure that the comment is aligned:
dnl [ --enable-example Enable example support])
if test "$PHP_EXAMPLE" != "no"; then
dnl Write more examples of tests here...
dnl # --with-example -> check with-path
SEARCH_PATH="/usr/local /usr" # you might want to change this
EXAMPLE_DIR=$PHP_EXAMPLE
dnl
if test -z "$EXAMPLE_DIR"; then
EXAMPLE_CFLAGS= "$EXAMPLE_CFLAGS"
fi
dnl
PHP_SUBST(EXAMPLE_CFLAGS)
PHP_SUBST(EXAMPLE_SHARED_LIBADD)
PHP_NEW_EXTENSION(example, example.c, $ext_shared)
fi
installation
To install example extension, do the following steps:
phpize
./configure
make && make install
Let php know we need example extension, add extension=example.so
in php.ini
usage
Now we can use example extension likes this:
<?php
$c = new example("Hello");
echo $c->get(), "\n";
and done.
source code
I put example extension used in this post in this repository.
Conclusion:
Actually I can not understand why php zend api has so much complicated macro, note that those was mentioned in this post is just some of many magical things in ZEND API. Maybe with a better document, things will be easier and developer will write more php extension for php community.
(1) Here is my extension for 51degrees device detection library(2)
(2): device detection