Creating PHP 7 extensions for Linux and Windows

An updated tutorial on how to make extensions for PHP 7

September 10, 2017 · 14 comments
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; \

14 comments

  1. MnemonicCarrier

    Thank you sooooooo much for taking the time and effort to write about this! It's exactly what I'm looking for (I'm just starting to learn how to write PHP extensions for PHP 7.2).

    I have managed to follow your tutorial, compiled everything, and can execute:

    $obj = new Lytrax\Test(123);
    $obj->shift(4);
    echo "Current gear: " . $obj->getCurrentGear() . "\n";

    I just don't understand how to call the doTest() method:

    HP_METHOD(Test, doTest)
    {
    zval *args = NULL;
    int argc, i;

    What parameters are required?

    1. Christos Lytras Author

      Hello,

      Thank you. I was so excited when I was making this. I think it's very nice stuff.

      I should have included a full PHP file for testing and documented the function arguments.
      The `doTest` function is for executing a call back function that is defined with the `onTest` function, just to demontrate the case/use of CB functions with parameters; so, to test this, you first need to assign a callback function to the `onTest` method, the execute the `doTest` with the parameters you want to pass to the CB function:

      $test = new lytrax\Test(5);

      $name = "Chris";

      $cb = function($sname) use ($name) {
      return "This is a call back Test with name '{$name}' and sname '{$sname}'!";
      };

      $test->onTest($cb, 'Lytr'); // First param is CB function, second is the default parameter for CB function

      echo "'".$test->doTest()."'".PHP_EOL;
      // Outputs: 'This is a call back Test with name 'Chris' and sname 'Lytr'!!!'

      echo "'".$test->doTest("New SName")."'".PHP_EOL;
      // Outputs: 'This is a call back Test with name 'Chris' and sname 'New SName'!!!'

      1. MnemonicCarrier

        Thank you so much! This is really appreciated!! I'll give it a try shortly and read through the code more carefully.

        I went to a PHP conference (meetup?) a couple of years back where there was a talk on writing PHP extensions. Unfortunately, it was very unhelpful, as the speakers basically blanket answered most technical questions with "...read the source code..." (they were talking about the PHP source code). Such advice easily scares off would-be extension developers who are trying to get started in this daunting (to the newcomer) endeavor.

        Once again - thank you so much for the tutorial. I'll spend some time looking at your other articles :)

  2. xenovia

    Hi. can u tell me which functions is only needed for dll? i want to remove the tests function but there are many dependents.

    1. Christos Lytras Author

      The whole project of this article is based on a Test class and exports the class and test mothods that use that class. You may create your class you want to export, change the exports and the functions that use that class. You need to change the test_object the exported test_methods and setup all of your new classes as I have exported the Test class methods by creating all the exported methods by using PHP_METHOD. It's not that hard if you follow the exported objects.

      1. Yes thanks!

  3. There is a lot of unknown identifier in lytrax.cc when compiling using php 5.6. can u help me?

    1. anyway. I think I have get a good grasp in making extension in php 7. So I think ill just follow the source codes in php extension 5.6.

      1. Christos Lytras Author

        Hello @disqus_Yte4qv21ri:disqus . Why do you want to create extensions for PHP 5.6? PHP 5.6 is not actively supported anymore and it will stop receiving security updates at the end of 2018. It's a waste of time really to learn how to make an extension for a deprecated PHP version. See supported versions here: http://php.net/supported-ve...

        1. Hi. I am able to do it with php 7.2. Where are .so files generated after compiling with make? There Is no Release folder generated just like In windows.

  4. Karlo Bonayon

    I fixed my problem about make by installing build-essential. but when I compile I get 'ZEND_ACC_CTOR' was not declared in this scope. I tried to do about editing zend_api.h but didnt work.

    1. Christos Lytras Author

      Hello @karlobonayon:disqus. You first have to generate the config file by either using phpize from the PHP sources you have installed, or by using `./buildconf --force`. Then you execute `./configure --enable-lytrax` with the name of your extensions of course, and only then you can run the make command. Running make without generating a config file and configuring the whole things won't produce anything.

      1. Karlo Bonayon

        Yes I already did that. Im already in the stage of compiling the source using make. But I keep getting the 'ZEND_ACC_CTOR' was not declared in the scope error. also, will the configure command genrate make? im using make that i get from build-essential because there is no make file in the directory of php source.

  5. Jason Toh

    Can this work on php 7.1.26? on Windows... I got the .dll after compile but unable to load it into php..

Manage a comment you wrote