Writing PHP 7 Extensions with C++

Why would you want to write a PHP extension? Although there are many answers to this question, most of the time the answer is: “You wouldn’t!”. Writing and (more importantly) maintaining PHP extensions can be costly in terms of time and effort. It is highly likely any extensions you have written and come to rely upon today will probably break with the next major PHP release. Even minor PHP releases can break extensions.

There are still many reasons why someone would want to write PHP extensions: for performance benefits, to do low level stuff (e.g. finding all bluetooth devices in range), to obfuscate and hide things away in a binary (like private keys used to decrypt sensitive data), to scratch an itch (etc, etc, etc…).

Much has been written on the Interwebs abouts writing PHP extensions. I would recommend PHP Internals as a good starting point:

http://www.phpinternalsbook.com/index.html

It is not the intention of this blog post to deep-dive in to the nitty-gritty guts of PHP (and PHP extensions). Instead, this post is intended to cater more for those who can’t be bothered RTFM’ing.

In this blog post we’re going to write a PHP extension that uses IOCTL to fetch the MAC address of a given network interface. You can already do this in PHP without writing an extension:

<?php
  print exec("ip addr show wlp3s0 | grep -Po 'link/ether \K.([^\s]+)'") . PHP_EOL;

A one liner… But where’s the fun in that? As well as a lack of fun, using exec() is costly – it creates a new instance of a shell (and all the overhead that entails).

Structure

The files we’ll be using in this tutorial are:

config.m4
php_quidco.h
quidco.cpp
QNet.h and QNet.cpp

The source code (and brief explanation) of the files above:

config.m4

This file is used to describe what external libraries your extension requires, what source files are required to build your extension, whether to enable support for the extension etc… It basically tells the build system how to generate the files required to configure and build your PHP extension (as we’ll see later).

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

if test $PHP_QUIDCO != "no"; then
    PHP_REQUIRE_CXX()
    PHP_SUBST(QUIDCO_SHARED_LIBADD)
    PHP_ADD_LIBRARY(stdc++, 1, QUIDCO_SHARED_LIBADD)
    PHP_NEW_EXTENSION(quidco, quidco.cpp QNet.cpp, $ext_shared)
fi

php_quidco.h

This is a header that is mostly boilerplate code – it includes php.h, defines a handful of simple pre-processor macros (to make things a little easier), and defines the name and version of your extension.

#ifndef PHP_QUIDCO_H
#define PHP_QUIDCO_H

#define PHP_QUIDCO_EXTNAME  "quidco"
#define PHP_QUIDCO_EXTVER   "0.1"

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

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

extern zend_module_entry quidco_module_entry;
#define quidco_module_ptr &quidco_module_entry
#define phpext_quidco_ptr quidco_module_ptr

#endif /* PHP_QUIDCO_H */

QNet.h and QNet.cpp

A simple C++ class containing a single method that returns a string representing the MAC address for the given interface.

QNet.h:

#ifndef QNET_H
#define QNET_H

#include <string>

//
// Get Mac Address
//
class QNet
{
  public:

    QNet();
    std::string getMacAddr(const std::string &iface);
};

#endif // QNET_H

 

QNet.cpp:

#include <iostream>
#include <sstream>
#include <iomanip>
#include <string.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <unistd.h>

#include "QNet.h"

QNet::QNet() {}

std::string QNet::getMacAddr(const std::string &iface)
{
  int fd;
  struct ifreq ifr;
  unsigned char *mac;
  int mac_len = sizeof((unsigned char*)ifr.ifr_hwaddr.sa_data) - 2;
  std::stringstream buffer;

  fd = socket(AF_INET, SOCK_DGRAM, 0);
  ifr.ifr_addr.sa_family = AF_INET;
  strncpy((char*)ifr.ifr_name, 
         (const char*)iface.c_str(), IFNAMSIZ - 1);
  ioctl(fd, SIOCGIFHWADDR, &ifr);
  close(fd);

  mac = (unsigned char*)ifr.ifr_hwaddr.sa_data;

  for (int i = 0; i < mac_len; i++)
    buffer << std::hex 
           << std::setfill('0') 
           << std::setw(2) << (int)mac[i] << ":";

  std::string retval = (std::string)buffer.str();
  retval.resize(retval.size() - 1);

  return retval;
}

quidco.cpp

This is the main source file for the PHP extension. It defines the namespace (in this case, "Quidco"), the class name (in this case, "QNet"), the methods that are available from within PHP (the constructor, and getMacAddr(string)), how the extension should be loaded, and how it should be destroyed.

#include "php_quidco.h"
#include "QNet.h"

zend_object_handlers qnet_object_handlers;

typedef struct _qnet_object {
    QNet *qnet;
    zend_object std;
} qnet_object;

static inline qnet_object *php_qnet_obj_from_obj(zend_object *obj) {
    return (qnet_object*)((char*)(obj) - XtOffsetOf(qnet_object, std));
}

#define Z_TSTOBJ_P(zv)  php_qnet_obj_from_obj(Z_OBJ_P((zv)))

zend_class_entry *qnet_ce;

// ------------------------------------------------------------------
// ------------------------------------------------------------------
PHP_METHOD(QNet, __construct)
{
    zval *id = getThis();
    qnet_object *intern;

    intern = Z_TSTOBJ_P(id);
    if(intern != NULL) {
        intern->qnet = new QNet();
    }
}

// ------------------------------------------------------------------ // ------------------------------------------------------------------
PHP_METHOD(QNet, getMacAddr)
{
    unsigned char *iface;
    zval *id = getThis();
    qnet_object *intern;

    if (zend_parse_parameters(
      ZEND_NUM_ARGS() TSRMLS_CC, "s", &iface) == FAILURE) {
          RETURN_NULL();
    }

    intern = Z_TSTOBJ_P(id);
    if(intern != NULL) {
        std::string s = intern->qnet->getMacAddr((char*)iface);
        RETURN_STRING(s.c_str());
    }
}

// ------------------------------------------------------------------ // ------------------------------------------------------------------
const zend_function_entry qnet_methods[] = {
    PHP_ME(QNet, __construct, NULL, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
    PHP_ME(QNet, getMacAddr,  NULL, ZEND_ACC_PUBLIC)
    PHP_FE_END
};

// ------------------------------------------------------------------ // ------------------------------------------------------------------
zend_object *qnet_object_new(zend_class_entry *ce TSRMLS_DC)
{
    qnet_object *intern = (qnet_object*)ecalloc(1,
            sizeof(qnet_object) +
            zend_object_properties_size(ce));

    zend_object_std_init(&intern->std, ce TSRMLS_CC);
    object_properties_init(&intern->std, ce);

    intern->std.handlers = &qnet_object_handlers;

    return &intern->std;
}

// ------------------------------------------------------------------ // ------------------------------------------------------------------
static void qnet_object_destroy(zend_object *object)
{
    qnet_object *my_obj;
    my_obj = (qnet_object*)((char *)
        object - XtOffsetOf(qnet_object, std));

    // Call __destruct() from user-land.
    zend_objects_destroy_object(object);
}

static void qnet_object_free(zend_object *object)
{
    qnet_object *my_obj;
    my_obj = (qnet_object *)((char *)
        object - XtOffsetOf(qnet_object, std));
    delete my_obj->qnet;
    // Free the object using Zend macro.
    zend_object_std_dtor(object); 
}

// ------------------------------------------------------------------ // ------------------------------------------------------------------
PHP_MINIT_FUNCTION(quidco)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Quidco\\QNet", qnet_methods);
    qnet_ce = zend_register_internal_class(&ce TSRMLS_CC);
    qnet_ce->create_object = qnet_object_new;

    memcpy(&qnet_object_handlers, 
        zend_get_std_object_handlers(), 
        sizeof(qnet_object_handlers));

    // Handler for free'ing the object.
    qnet_object_handlers.free_obj = qnet_object_free;

    // Handler for the destructor.
    qnet_object_handlers.dtor_obj = qnet_object_destroy; 

    // Offset into the engine.
    qnet_object_handlers.offset = XtOffsetOf(qnet_object, std); 

    return SUCCESS;
}

// ------------------------------------------------------------------ // ------------------------------------------------------------------
zend_module_entry quidco_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    PHP_QUIDCO_EXTNAME,
    NULL,                  /* Functions */
    PHP_MINIT(quidco),
    NULL,                  /* MSHUTDOWN */
    NULL,                  /* RINIT */
    NULL,                  /* RSHUTDOWN */
    NULL,                  /* MINFO */
#if ZEND_MODULE_API_NO >= 20010901
    PHP_QUIDCO_EXTVER,
#endif
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_QUIDCO
extern "C" {
  ZEND_GET_MODULE(quidco)
}
#endif

Building the PHP Extension

There are four steps required to build a PHP extension:

$ phpize
$ ./configure
$ make
$ sudo make install

In this example we've created a "shared extension" (as opposed to a "static extension"). While a static extension is included with the PHP module, a shared extension is simply a .so file (in this case, quidco.so). The last step above will, on Unixoid systems, copy the .so file to the appropriate directory (i.e. the directory where your PHP install will look for extensions).

Loading the PHP Extension

Copying the extension (quidco.so) to a directory is not enough - you still have to tell PHP to load the extension in an .ini file. On my system, I only installed the CLI version of PHP, so all the .ini files for extensions are located here:

/etc/php/7.2/cli/conf.d

Within this directory, I have created a file called 99-quidco.ini, the contents of which are:

; Configuration for Quidco PHP extension.
; priority=99
extension=quidco.so

NOTE: If you are using PHP with Nginx or Apache, you'll have to also copy your quidco.ini file to the web conf.d path and then restart the web server.

You can see if your extension is loaded using:

$ php -r "echo extension_loaded('quidco');"

Or:

$ php -m | grep quidco

See if it Worked

Run the "one liner" that uses exec() to get the MAC address for given network interface (don't forget to change "wlp3s0" to the name of a network interface that exists on your machine):

$ php -r "echo exec(\"ip addr show wlp3s0 | grep -Po 'link/ether \K.([^\s]+)'\") . PHP_EOL;"

Output:
94:55:9c:D1:28:E1

Now do the same using the extension you've just created:

$ php -r "print (new Quidco\QNet)->getMacAddr('wlp3s0') . PHP_EOL;"

Output:
94:55:9c:D1:28:E1

Testing Your Extension

Testing extensions is very important. Because of the complexity involved, it's very easy to make mistakes in regards to memory management. This can bring down your web server - sometimes immediately, sometimes after months of (seemingly) smooth operation. Valgrind is a very handy tool that can be used to test for memory and threading issues. One example of a quick test for our quidco.so extension:

$ valgrind --leak-check=full --tool=memcheck --num-callers=500 /usr/bin/php -r "print (new Quidco\QNet)->getMacAddr('wlp3s0') . PHP_EOL;"

==18062== Memcheck, a memory error detector
==18062== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==18062== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==18062== Command: /usr/bin/php -r print\ (new\ Quidco\\QNet)-\>getMacAddr('wlp3s0')\ .\ PHP_EOL;
==18062==
94:55:9c:D1:28:E1
==18062==
==18062== HEAP SUMMARY:
==18062==     in use at exit: 1,126 bytes in 21 blocks
==18062==   total heap usage: 19,790 allocs, 19,769 frees, 2,713,241 bytes allocated
==18062==
==18062== LEAK SUMMARY:
==18062==    definitely lost: 0 bytes in 0 blocks
==18062==    indirectly lost: 0 bytes in 0 blocks
==18062==      possibly lost: 0 bytes in 0 blocks
==18062==    still reachable: 1,126 bytes in 21 blocks
==18062==         suppressed: 0 bytes in 0 blocks
==18062== Reachable blocks (those to which a pointer was found) are not shown.
==18062== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==18062==
==18062== For counts of detected and suppressed errors, rerun with: -v
==18062== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Performance Comparison

While this is by no means an exhaustive or accurate test, we can get a "finger in the air" idea about the performance difference of using the above two methods to obtain the MAC address for a given network interface:

// -------------------------------------------------------------
// Rudimentary "clock-on-the-wall" performance test.
// -------------------------------------------------------------

// *** USING EXEC() ***
$start_time = microtime(true);

for ($i=0; $i<1000; $i++)
  exec("ip addr show wlp3s0 | grep -Po 'link/ether \K.([^\s]+)'");

$end_time = microtime(true);
$execution_time = ($end_time - $start_time);

echo "Execution time using exec(): " . number_format((float)$execution_time, 10) . "\n";

// *** USING A PHP EXTENSION ***
$start_time = microtime(true);

for ($i=0; $i<1000; $i++)
  (new Quidco\QNet)->getMacAddr('wlp3s0');

$end_time = microtime(true);
$execution_time = ($end_time - $start_time);

echo "Execution time using PHP extension: " . number_format((float)$execution_time, 10) . "\n";

Output:

Execution time using exec(): 3.4652059078
Execution time using PHP extension: 0.0077860355

Summary

PHP extensions are awesome, but careful consideration should be given before using them within a production environment:

  • Is it really necessary to develop a PHP extension for your requirements?
  • How many people do you have who are able to develop and maintain PHP extensions?
  • Is there already an extension out there that fulfils your requirements?