Creating PHP 7 extensions for Linux and Windows

An updated tutorial on how to make extensions for PHP 7

September 10, 2017
In category Makeitwork

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

  1. Autoconf error
    error: Autoconf version 2.69 or higher is required
    or
    buildconf: autoconf not found.

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
  1. Compiler invalid conversion
    error: invalid conversion from ‘int’ to ‘zend_expected_type’

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; \
0 0

comments powered by Disqus