PHP 7 Extensions
PHP 7 internals have some major changes that break compatibility with all extensions build for prior versions of PHP. I was searching to find a nice and working tutorial on how to build PHP 7 extensions for Linux and Windows and almost everything is outdated and for older PHP versions. My main goal is to use C++ classes and extract PHP classes that mostly depend on those C++ objects.
We’ll create an extension with the name lytrax
. It will export a class Test
under the namespace Lytrax
. The class will have some methods to alter our C++ Test class object and it will export some methods to demonstrate how to use PHP callback functions.
Extension file structure
ext/
lytrax/
config.m4
config.w32
lytrax.cc
php_lytrax.h
test.cc
test.h
config.m4
PHP_ARG_ENABLE(lytrax,
[Whether to enable the "Lytrax" extension],
[ --enable-lytrax Enable "Lytrax" extension support])
if test $PHP_LYTRAX != "no"; then
PHP_REQUIRE_CXX()
PHP_SUBST(LYTRAX_SHARED_LIBADD)
PHP_ADD_LIBRARY(stdc++, 1, LYTRAX_SHARED_LIBADD)
PHP_NEW_EXTENSION(lytrax, lytrax.cc test.cc, $ext_shared)
fi
config.w32
ARG_ENABLE("lytrax", "Whether to enable the Lytrax extension", "no");
if (PHP_LYX != "no") {
EXTENSION("lytrax", "lytrax.cc test.cc", true);
}
php_lytrax.h
#ifndef PHP_LYTRAX_H
#define PHP_LYTRAX_H
#define PHP_LYTRAX_EXTNAME "lytrax"
#define PHP_LYTRAX_EXTVER "0.1"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
extern "C" {
#include "php.h"
}
extern zend_module_entry lytrax_module_entry;
#define lytrax_module_ptr &lytrax_module_entry
#define phpext_lytrax_ptr lytrax_module_ptr
#endif /* PHP_LYTRAX_H */
lytrax.cc
#include "php_lytrax.h"
#include "test.h"
zend_object_handlers test_object_handlers;
typedef struct _test_object {
Test *test;
zend_object std;
} test_object;
static inline test_object *php_test_obj_from_obj(zend_object *obj) {
return (test_object*)((char*)(obj) - XtOffsetOf(test_object, std));
}
#define Z_TSTOBJ_P(zv) php_test_obj_from_obj(Z_OBJ_P((zv)))
zend_class_entry *test_ce;
PHP_METHOD(Test, __construct)
{
long maxGear;
zval *id = getThis();
test_object *intern;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &maxGear) == FAILURE) {
RETURN_NULL();
}
intern = Z_TSTOBJ_P(id);
if(intern != NULL) {
intern->test = new Test(maxGear);
}
}
PHP_METHOD(Test, shift)
{
long gear;
zval *id = getThis();
test_object *intern;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &gear) == FAILURE) {
RETURN_NULL();
}
intern = Z_TSTOBJ_P(id);
if(intern != NULL) {
intern->test->shift(gear);
}
}
PHP_METHOD(Test, doTest)
{
zval *args = NULL;
int argc, i;
zval retval;
zend_fcall_info fci;
zend_fcall_info_cache fci_cache;
zval *id = getThis();
test_object *intern;
intern = Z_TSTOBJ_P(id);
if(intern != NULL) {
memcpy(&fci, &intern->test->fci_onTest, sizeof(fci));
memcpy(&fci_cache, &intern->test->fcc_onTest, sizeof(fci_cache));
fci.retval = &retval;
ZEND_PARSE_PARAMETERS_START(0, -1)
Z_PARAM_VARIADIC('*', args, argc)
ZEND_PARSE_PARAMETERS_END();
if(argc > 0) {
fci.params = args;
fci.param_count = argc;
}
if (zend_call_function(&fci, &fci_cache) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
if (Z_ISREF(retval)) {
zend_unwrap_reference(&retval);
}
ZVAL_COPY_VALUE(return_value, &retval);
}
}
}
PHP_METHOD(Test, onTest)
{
zval *args = NULL;
int argc, i;
zval *id = getThis();
test_object *intern;
intern = Z_TSTOBJ_P(id);
if(intern != NULL) {
ZEND_PARSE_PARAMETERS_START(1, -1)
Z_PARAM_FUNC(intern->test->fci_onTest, intern->test->fcc_onTest)
Z_PARAM_VARIADIC('*', args, argc)
ZEND_PARSE_PARAMETERS_END();
}
intern->test->fci_onTest.param_count = argc;
if(argc > 0) {
intern->test->fci_onTest.params = (zval*)safe_emalloc(intern->test->fci_onTest.param_count, sizeof(zval), 0);
for(i = 0; i < argc; i++) {
zval *arg = args + i;
ZVAL_COPY_VALUE(&intern->test->fci_onTest.params[i], arg);
}
}
}
PHP_METHOD(Test, testCallback)
{
zval retval;
zend_fcall_info fci;
zend_fcall_info_cache fci_cache;
ZEND_PARSE_PARAMETERS_START(1, -1)
Z_PARAM_FUNC(fci, fci_cache)
Z_PARAM_VARIADIC('*', fci.params, fci.param_count)
ZEND_PARSE_PARAMETERS_END();
fci.retval = &retval;
if (zend_call_function(&fci, &fci_cache) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
if (Z_ISREF(retval)) {
zend_unwrap_reference(&retval);
}
ZVAL_COPY_VALUE(return_value, &retval);
}
}
PHP_METHOD(Test, getCurrentGear)
{
zval *id = getThis();
test_object *intern;
intern = Z_TSTOBJ_P(id);
if(intern != NULL) {
RETURN_LONG(intern->test->getCurrentGear());
}
RETURN_NULL();
}
ZEND_BEGIN_ARG_INFO_EX(arginfo_testcallback, 0, 0, 1)
ZEND_ARG_CALLABLE_INFO(0, cbfn, 0)
ZEND_END_ARG_INFO();
ZEND_BEGIN_ARG_INFO_EX(arginfo_ontest, 0, 0, 1)
ZEND_ARG_CALLABLE_INFO(0, cbfn, 0)
ZEND_END_ARG_INFO();
const zend_function_entry test_methods[] = {
PHP_ME(Test, __construct, NULL, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
PHP_ME(Test, shift, NULL, ZEND_ACC_PUBLIC)
PHP_ME(Test, testCallback, arginfo_testcallback, ZEND_ACC_PUBLIC)
PHP_ME(Test, onTest, arginfo_ontest, ZEND_ACC_PUBLIC)
PHP_ME(Test, doTest, NULL, ZEND_ACC_PUBLIC)
PHP_ME(Test, getCurrentGear, NULL, ZEND_ACC_PUBLIC)
PHP_FE_END
};
zend_object *test_object_new(zend_class_entry *ce TSRMLS_DC)
{
test_object *intern = (test_object*)ecalloc(1,
sizeof(test_object) +
zend_object_properties_size(ce));
zend_object_std_init(&intern->std, ce TSRMLS_CC);
object_properties_init(&intern->std, ce);
intern->std.handlers = &test_object_handlers;
return &intern->std;
}
static void test_object_destroy(zend_object *object)
{
test_object *my_obj;
my_obj = (test_object*)((char *)object - XtOffsetOf(test_object, std));
/* Now we could do something with my_obj->my_custom_buffer, like sending it
on a socket, or flush it to a file, or whatever, but not free it here */
zend_objects_destroy_object(object); /* call __destruct() from userland */
}
static void test_object_free(zend_object *object)
{
test_object *my_obj;
my_obj = (test_object *)((char *)object - XtOffsetOf(test_object, std));
delete my_obj->test;
zend_object_std_dtor(object); /* call Zend's free handler, which will free object properties */
}
PHP_MINIT_FUNCTION(lytrax)
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "Lytrax\\Test", test_methods);
test_ce = zend_register_internal_class(&ce TSRMLS_CC);
test_ce->create_object = test_object_new;
memcpy(&test_object_handlers, zend_get_std_object_handlers(), sizeof(test_object_handlers));
test_object_handlers.free_obj = test_object_free; /* This is the free handler */
test_object_handlers.dtor_obj = test_object_destroy; /* This is the dtor handler */
test_object_handlers.offset = XtOffsetOf(test_object, std); /* Here, we declare the offset to the engine */
return SUCCESS;
}
zend_module_entry lytrax_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
PHP_LYTRAX_EXTNAME,
NULL, /* Functions */
PHP_MINIT(lytrax),
NULL, /* MSHUTDOWN */
NULL, /* RINIT */
NULL, /* RSHUTDOWN */
NULL, /* MINFO */
#if ZEND_MODULE_API_NO >= 20010901
PHP_LYTRAX_EXTVER,
#endif
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_LYTRAX
extern "C" {
ZEND_GET_MODULE(lytrax)
}
#endif
test.h
#ifndef LYTRAX_TEST_H
#define LYTRAX_TEST_H
extern "C" {
#include "php.h"
}
// A very simple test class
class Test {
public:
Test(int maxGear);
void shift(int gear);
void accelerate();
void brake();
int getCurrentSpeed();
int getCurrentGear();
public:
int maxGear;
int currentGear;
int speed;
public:
// Here we store our callback function
zend_fcall_info fci_onTest;
zend_fcall_info_cache fcc_onTest;
};
#endif /* LYTRAX_TEST_H */
test.cc
#include "test.h"
Test::Test(int maxGear) {
this->maxGear = maxGear;
this->currentGear = 1;
this->speed = 0;
}
void Test::shift(int gear) {
if (gear < 1 || gear > maxGear) {
return;
}
currentGear = gear;
}
void Test::accelerate() {
speed += (5 * this->getCurrentGear());
}
void Test::brake() {
speed -= (5 * this->getCurrentGear());
}
int Test::getCurrentSpeed() {
return speed;
}
int Test::getCurrentGear() {
return currentGear;
}
Compiling PHP with extension
You need to have a system ready for compiling PHP from sources. That means that there must be all of the dependencies extensions on Linux systems and Visual Studio installed for Windows.
Linux
An already compiled PHP can be used. You’ll have to use [phpdir]/bin/phpize
and then use ./configure --with-php-config=[phpdir]/bin/php-config
inside your extension directory.
For Linux, you need an updated GCC/G++ compiler along with dependencies packages. For CentOS you can instsall most of the dependencies with:
yum install git gcc gcc-c++ libxml2-devel pkgconfig openssl-devel bzip2-devel curl-devel libpng-devel libjpeg-devel libXpm-devel freetype-devel gmp-devel libmcrypt-devel mysql-devel aspell-devel recode-devel autoconf bison re2c libicu-devel
For Ubuntu:
sudo apt-get install build-essential libxml2-dev libcurl4-openssl-dev libjpeg-dev libpng-dev libxpm-dev libmysqlclient-dev libpq-dev libicu-dev libfreetype6-dev libldap2-dev libxslt-dev libbz2-dev libc-client-dev libkrb5-dev libreadline-dev unixodbc-dev
Go to your src directory /url/local/src/
or $HOME/src/
and clone the PHP source from GitHub:
git clone https://github.com/php/php-src.git && cd php-src
Then checkout to the latest PHP version, or to any version you want:
git checkout PHP-7.2
Build configure
file[1]:
./buildconf --force
Run the ./configure --help
file to check if our extension is recognized:
./configure --help
...
--enable-lytrax Enable "Lytrax" extension support
...
If you can’t see our extension enable option, then there is something wrong with out extension config.m4
file. Check that and retry ./buildconf --force
After we confirm that PHP configure can see our extension, we run ./configure --enable-lytrax
and enabling our extension:
./configure --enable-lytrax
Finally we execute the make
command to build PHP and the extension binaries:
./make
If PHP was not compiled, then we’ll have to wait for the whole PHP to compile for the first time; after the PHP is compiled, then the compiler will detect changes to our extension and will compile just it and not PHP entirely.
Windows
For Windows you need Visual Studio. You can download and install Visual Studio Community, which it has Visual C++ compiler that PHP sources rely on for compiling:
- Visual C++ 9.0 (Visual Studio 2008 or Visual C++ 2008) for PHP 5.4
- Visual C++ 11.0 (Visual Studio 2012) for PHP 5.5 or 5.6
- Visual C++ 14.0 (Visual Studio 2015) for PHP 7.0 or PHP 7.1
- Visual C++ 15.0 (Visual Studio 2017) for PHP 7.2
There is a step by step guide for PHP <= 7.1 and for PHP >= 7.2 at https://wiki.php.net.
Troubleshooting
You have to upgrade autoconf
to the latest version. On CentOS you can compile it like this:
wget http://ftp.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz
tar xzf autoconf-2.69.tar.gz
cd autoconf-2.69
./configure
make && make install
On Ubuntu just run this:
sudo apt-get install autoconf
In both Linux and Windows, I get a compiler error when using ZEND_PARSE_PARAMETERS_START
macro. I have to edit Zend/zend_API.h
manual to fix this error. Under ZEND_PARSE_PARAMETERS_START_EX
macro, find the line (line 723 for PHP 7.2):
zend_expected_type _expected_type = IS_UNDEF; \
and change it to:
zend_expected_type _expected_type = (zend_expected_type)IS_UNDEF; \