Wrapping C++ Classes in a PHP Extension

One of the most common reasons people write extensions for PHP is to link against an external library and make available its API to PHP userspace scripts. Often the external library is written in C or C++. If you are tasked with writing such an extension that links against an object oriented C++ library, you will probably want to wrap the C++ classes and make them available to PHP userspace scripts. This is pretty easy to do once you get over a few initial hurdles.

In this tutorial I am going to walk you through creating a PHP extension called “vehicles” which will expose a single class called “Car” (obviously in the real-world, your extensions will expose many classes, but I’m trying to keep things simple). The extension will be built for PHP 5. I am only going to give instructions for building the extension in a UNIX-like environment, although most of what I cover should apply to Windows extension development as well. Most of what I detail here will be an expanded or simplified version of material covered in the somewhat abandoned Using C++ With PHP. I am not going to cover the basics of PHP extension writing, for that I recommend Sara Goleman’s Extension Writing series or her book Extending and Embedding PHP.

File Layout

The extension you will be creating consists of several files which you should place in a directory called vehicles:

  • car.h – Contains the definition of the C++ Car class.
  • car.cc – Contains the implementation of the C++ Car class.
  • php_vehicles.h – Contains PHP specific includes, external variables, etc.
  • vehicles.cc – The main source file of the extension. This will contain the PHP Car class.
  • config.m4 – PHP Build system / autoconf configuration file.

Configuring the PHP Build System

In order to use C++ in your PHP extension, you must instruct the PHP build system to use a C++ compiler. This is done by calling the PHP_REQUIRE_CXX() macro in your config.m4 file. PHP_REQUIRE_CXX() is defined in aclocal.m4 in the root directory of the PHP source distribution and wraps the autoconf macro AC_PROG_CXX(), which is documented here. You must also link the C++ standard library (libstdc++ on most systems), which is done with the PHP_ADD_LIBRARY() macro. Put the following in your config.m4 file:

PHP_ARG_ENABLE(vehicles,
    [Whether to enable the "vehicles" extension],
    [  --enable-vehicles      Enable "vehicles" extension support])

if test $PHP_VEHICLES != "no"; then
    PHP_REQUIRE_CXX()
    PHP_SUBST(VEHICLES_SHARED_LIBADD)
    PHP_ADD_LIBRARY(stdc++, 1, VEHICLES_SHARED_LIBADD)
    PHP_NEW_EXTENSION(vehicles, vehicles.cc car.cc, $ext_shared)
fi

Depending on your compiler and how you compiled PHP, you may have to wrap your includes and certain macros in a linkage specification. If you’re not sure what that means, this article explains it nicely. So, together with all the usual skeleton code for a PHP extension, your php_vehicles.h header file should look something like this:

#ifndef PHP_VEHICLES_H
#define PHP_VEHICLES_H

#define PHP_VEHICLES_EXTNAME  "vehicles"
#define PHP_VEHICLES_EXTVER   "0.1"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif 

extern "C" {
#include "php.h"
}

extern zend_module_entry vehicles_module_entry;
#define phpext_vehicles_ptr &vehicles_module_entry;

#endif /* PHP_VEHICLES_H */

Additionally, you’ll probably need to wrap the call to the ZEND_GET_MODULE() macro in vehicles.cc in a similar linkage specification. With that in mind, your vehicles.cc file should look like the following:

#include "php_vehicles.h"

PHP_MINIT_FUNCTION(vehicles)
{
    return SUCCESS;
}

zend_module_entry vehicles_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    PHP_VEHICLES_EXTNAME,
    NULL,                  /* Functions */
    PHP_MINIT(vehicles),
    NULL,                  /* MSHUTDOWN */
    NULL,                  /* RINIT */
    NULL,                  /* RSHUTDOWN */
    NULL,                  /* MINFO */
#if ZEND_MODULE_API_NO >= 20010901
    PHP_VEHICLES_EXTVER,
#endif
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_VEHICLES
extern "C" {
ZEND_GET_MODULE(vehicles)
}
#endif

Side Note: There is a set of macros provided with the PHP / Zend API called BEGIN_EXTERN_C / END_EXTERN_C but I don’t use them. They’re defined in Zend/zend.h which is included from php.h, so they’re not available when you need them in php_vehicles.h. In my opinion, the inconsistency of using macros in some places and not in others just isn’t worth the debatable aesthetic benefit you might obtain from using them.

At this point you can do a quick sanity check and make sure that your extension is compiling properly. Possible Gotchya: Even though we haven’t done anything with it yet, make sure that car.cc exists since we referred to it in config.m4 earlier. If the file does not exist, you will run into problems with this step.

[paul@helm vehicles]$ phpize
PHP Api Version:         20041225
Zend Module Api No:      20060613
Zend Extension Api No:   220060519
[paul@helm vehicles]$ ./configure --enable-vehicles
checking for grep that handles long lines and -e... /bin/grep
checking for egrep... /bin/grep -E
checking for a sed that does not truncate output... /bin/sed
checking for gcc... gcc
checking for C compiler default output file name... a.out
...
[paul@helm vehicles]$ make
...
[paul@helm vehicles]$ sudo make install
...
[paul@helm vehicles]$ php -d"extension=vehicles.so" -m
[PHP Modules]
bcmath
bz2
calendar
ctype
tokenizer
vehicles
wddx

[Zend Modules]

[paul@helm vehicles]$

I trimmed the output, but you get the idea. You should see the extension listed when you run php -m. Also replace sudo with whatever is appropriate for your system / setup. Maybe you’re doing this as a user who has permission to write to the php extension_dir or maybe you use “su -c” or something similar.

The C++ Class

Now that you have the basic skeleton of your extension and the PHP build system knows to use a C++ compiler to compile it, you can work on the class that you’re going to expose to PHP. Notice how I’ve kept the C++ class definition and implementation separate from any extension specific code? This isn’t strictly necessary, but it decreases coupling and is generally a good idea (if you want to reuse your C++ class, you can do so without pulling over any PHP extension cruft).

car.h:

#ifndef VEHICLES_CAR_H
#define VEHICLES_CAR_H

// A very simple car class
class Car {
public:
    Car(int maxGear);
    void shift(int gear);
    void accelerate();
    void brake();
    int getCurrentSpeed();
    int getCurrentGear();
private:
    int maxGear;
    int currentGear;
    int speed;
};

#endif /* VEHICLES_CAR_H */

car.cc:

#include "car.h"

Car::Car(int maxGear) {
    this->maxGear = maxGear;
    this->currentGear = 1;
    this->speed = 0;
}

void Car::shift(int gear) {
    if (gear < 1 || gear > maxGear) {
        return;
    }
    currentGear = gear;
}

void Car::accelerate() {
    speed += (5 * this->getCurrentGear());
}

void Car::brake() {
    speed -= (5 * this->getCurrentGear());
}

int Car::getCurrentSpeed() {
    return speed;
}

int Car::getCurrentGear() {
    return currentGear;
}

In order to expose this class to PHP userspace scripts, you’ll first need to define a PHP class called Car with a function_entry table that contains a method for each of the C++ class methods you wish to expose. I should point out that the PHP class does not need to have the same name as the C++ class, and it is completely feasible to include methods in your PHP class that do not exist in your C++ class and vice versa. Again, I’m striving for simplicity here so I’ve kept the class names the same and I’ve maintained a 1-to-1 mapping of C++ and PHP methods. Update vehicles.cc to contain the following:

#include "php_vehicles.h"

zend_class_entry *car_ce;

PHP_METHOD(Car, __construct)
{
}
PHP_METHOD(Car, shift)
{
}
PHP_METHOD(Car, accelerate)
{
}
PHP_METHOD(Car, brake)
{
}
PHP_METHOD(Car, getCurrentSpeed)
{
}
PHP_METHOD(Car, getCurrentGear)
{
}

function_entry car_methods[] = {
    PHP_ME(Car,  __construct,     NULL, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
    PHP_ME(Car,  shift,           NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Car,  accelerate,      NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Car,  brake,           NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Car,  getCurrentSpeed, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Car,  getCurrentGear,  NULL, ZEND_ACC_PUBLIC)
    {NULL, NULL, NULL}
};

PHP_MINIT_FUNCTION(vehicles)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Car", car_methods);
    car_ce = zend_register_internal_class(&ce TSRMLS_CC);
    return SUCCESS;
}

zend_module_entry vehicles_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    PHP_VEHICLES_EXTNAME,
    NULL,        /* Functions */
    PHP_MINIT(vehicles),        /* MINIT */
    NULL,        /* MSHUTDOWN */
    NULL,        /* RINIT */
    NULL,        /* RSHUTDOWN */
    NULL,        /* MINFO */
#if ZEND_MODULE_API_NO >= 20010901
    PHP_VEHICLES_EXTVER,
#endif
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_VEHICLES
extern "C" {
ZEND_GET_MODULE(vehicles)
}
#endif

If this is looking a little foreign to you, I recommend reading Chapters 10 and 11 of “Extending PHP”.

Connecting the Dots

At this point you have a C++ class and a PHP class with identical (or similar) definitions. Now you need to connect the two. Each instance of the PHP class must have access to exactly one instance of the C++ class. One way to do this is to use a struct to keep track of the C++ and the PHP instances. Doing so requires that you implement your own object handlers. Remember that in PHP5, an object is just a handle (a unique id that allows the Zend Engine to locate your class), a method table, and a set of handlers. You can override handlers to perform the various tasks that need to be performed at various stages of the object’s lifecycle. Add the following to the top your vehicles.cc file:

#include "car.h"

zend_object_handlers car_object_handlers;

struct car_object {
    zend_object std;
    Car *car;
};

The car_object structure will be used to keep track of our C++ instances, associating them to a zend_object. Add the following functions to your vehicles.cc file, above the PHP_METHOD declarations:

void car_free_storage(void *object TSRMLS_DC)
{
    car_object *obj = (car_object *)object;
    delete obj->car; 

    zend_hash_destroy(obj->std.properties);
    FREE_HASHTABLE(obj->std.properties);

    efree(obj);
}

zend_object_value car_create_handler(zend_class_entry *type TSRMLS_DC)
{
    zval *tmp;
    zend_object_value retval;

    car_object *obj = (car_object *)emalloc(sizeof(car_object));
    memset(obj, 0, sizeof(car_object));
    obj->std.ce = type;

    ALLOC_HASHTABLE(obj->std.properties);
    zend_hash_init(obj->std.properties, 0, NULL, ZVAL_PTR_DTOR, 0);
    zend_hash_copy(obj->std.properties, &type->default_properties,
        (copy_ctor_func_t)zval_add_ref, (void *)&tmp, sizeof(zval *));

    retval.handle = zend_objects_store_put(obj, NULL,
        car_free_storage, NULL TSRMLS_CC);
    retval.handlers = &car_object_handlers;

    return retval;
}

And update your PHP_MINIT_FUNCTION in vehicles.cc:

PHP_MINIT_FUNCTION(vehicles)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Car", car_methods);
    car_ce = zend_register_internal_class(&ce TSRMLS_CC);
    car_ce->create_object = car_create_handler;
    memcpy(&car_object_handlers,
        zend_get_std_object_handlers(), sizeof(zend_object_handlers));
    car_object_handlers.clone_obj = NULL;
    return SUCCESS;
}

There’s quite a bit going on here. First, once we have a dedicated zend_class_entry pointer in the MINIT_FUNCTION, we can assign a create_object handler to it. The create_object handler allocates memory for the object, sets up the zend_object and generates a handle for the object by storing the car_object pointer. You’ll also notice that the zend_objects_store_put function accepts a function pointer to what is basically a c-level destructor for the object being handled.

Finally, in your PHP class constructor, you’ll parse the user parameters like usual and pass them on to the C++ constructor. Once the C++ class instance is created, you can fetch the car_object pointer from the zend object store and set the car field in the struct. This makes the C++ class instance available to other instance methods of your PHP class.

PHP_METHOD(Car, __construct)
{
    long maxGear;
    Car *car = NULL;
    zval *object = getThis();

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &maxGear) == FAILURE) {
        RETURN_NULL();
    }

    car = new Car(maxGear);
    car_object *obj = (car_object *)zend_object_store_get_object(object TSRMLS_CC);
    obj->car = car;
}

Now, in order to obtain an instance of the C++ class, instance methods of your PHP class just need to call zend_object_store_get_object:

PHP_METHOD(accelerate)
{
    Car *car;
    car_object *obj = (car_object *)zend_object_store_get_object(
        getThis() TSRMLS_CC);
    car = obj->car;
    if (car != NULL) {
        car->accelerate();
    }
}

PHP_METHOD(getCurrentSpeed)
{
    Car *car;
    car_object *obj = (car_object *)zend_object_store_get_object(
        getThis() TSRMLS_CC);
    car = obj->car;
    if (car != NULL) {
        RETURN_LONG(car->getCurrentSpeed());
    }
    RETURN_NULL();
}

The remaining methods all work in basically the same way, I’ll leave implementing them as an exercise.

That’s it, we’re done! Now let’s take our new extension for a test drive. Don’t forget to re-compile, install and of course enable it in php.ini (or continue to use the -dextension=vehicles.so command line argument).

// create a 5 gear car
$car = new Car(5);
print $car->getCurrentSpeed();  // prints '0'
$car->accelerate();
print $car->getCurrentSpeed(); // prints '5'

If you can run this script, congratulations, you’ve just created a PHP extension that wraps a C++ class.