Wednesday, March 26, 2014

Getting Started with PHP Extension Development via PHP-CPP

In your dealings with PHP, you may come to consider writing a PHP extension yourself. There are several reasons I can think of that motivate me to do so:
  • to extend PHP functionality for some very particular usage (mathematics, statistics, geometric, etc).
  • to have a higher performance and efficiency compared to a pure PHP implementation
  • to leverage the swiftness obtained from programming in another previously grasped language (for me, C++).
When it comes to choosing a tool to build PHP extensions, we see two different approaches:
  • Use more pro-PHP semantics, like Zephir.
  • Use more pro-C/C++ semantics, like PHP-CPP, which will be addressed in this article.
For me, the main drive to select the second approach is simple: I started my programming hobby with C/C++, so I still feel more comfortable writing those lower level modules in C/C++. PHP-CPP’s official site gives a few other reasons to do so.

Installation and configuration

PHP-CPP is evolving rapidly. At the time of this article’s writing, it is in version 0.9.1 (with 0.9.0 released about 2 days before). According to its documentation, “this is a feature-freeze release that prepares for the upcoming v1.0 version”, so we are confident we’ll see its 1.0 major release very soon.

It is thus recommended, at least during this interim period, to use git to clone the repository and get the latest update later via git pull

NOTE: The PHP-CPP documentation on installation states that for the time being, it “only supports single-threaded PHP installations” because “internally the Zend engine uses a very odd system to ensure thread safety”. Future releases may support multi-threaded PHP installations but let’s just keep this in mind for now and stick to its current limitation. Luckily, “single-threaded PHP installations” should be the case for most of the PHP installations out there.

PHP-CPP is written in C++11. Thus, the older version of g++ installed in my Ubuntu 12.04 LTS does not support it. We need to upgrade our g++ compiler to version 4.8.x above. There is an article detailing the steps to do the upgrading. Please follow the instructions listed there. 

Also, PHP-CPP compilation will use the php.h header file. This file is normally missing in an Ubuntu box, unless php-dev was installed. We can install PHP5 related development files by issuing this command: 

sudo apt-get install php5-dev
 
After upgrading g++ and installing the necessary header files, we can issue the following command to compile and install the PHP-CPP library file (libphpcpp.so):

make && sudo make install
 
The compilation will be quite fast. After the installation, the libphpcpp.so file will be copied over to /usr/lib, and all PHP-CPP header files will be copied to /usr/include and /usr/include/phpcpp folders.

The installation of PHP-CPP lib is now complete. It is quite straightforward and we can now move on to the programming part. 

Before we do that, we’ll discuss a few important concepts and terminologies used in PHP-CPP. The full documentation can be found on its official site, and everyone is encouraged to read through it BEFORE doing any real programming.

Skeleton (empty) extension project files

PHP-CPP provides a skeleton extension project, containing the following 3 files:
  • main.cpp: the main cpp file containing a get_module function (will be discussed in more detail later)
  • Makefile: the sample MAKE file to compile the extension
  • yourextension.ini: contains just one line for extension loading

Makefile

If you are familiar with *nix development, you are familiar with this Makefile. Some slight changes shall be made to customize this file to fit our needs:
  • Change NAME = yourextension to a more meaningful one, like NAME = skeleton.
  • Change INI_DIR = /etc/php5/conf.d to match your system’s configuration. In my case, it is INI_DIR = /etc/php5/cli/conf.d. I modified the INI path to enable the extension for PHP’s cli environment first.
These are all the changes I have made. The rest of the Makefile can be kept as it is.

yourextension.ini

I renamed this file to skeleton.ini and changed the only line in that file like this:
extension=skeleton.so

main.cpp

In the empty project provided by PHP-CPP, this file contains only one function: get_module(), which is excerpted below:

#include <phpcpp.h>

/**
 *  tell the compiler that the get_module is a pure C function
 */
extern "C" {

    /**
     *  Function that is called by PHP right after the PHP process
     *  has started, and that returns an address of an internal PHP
     *  strucure with all the details and features of your extension
     *
     *  @return void*   a pointer to an address that is understood by PHP
     */
    PHPCPP_EXPORT void *get_module() 
    {
        // static(!) Php::Extension object that should stay in memory
        // for the entire duration of the process (that's why it's static)
        static Php::Extension extension("yourextension", "1.0");

        // @todo    add your own functions, classes, namespaces to the extension

        // return the extension
        return extension;
    }
}  
For now, let’s just change this line to match the extension name we intend to create:

static Php::Extension extension("skeleton", "1.0"); // To be humble, we can change the version number to 0.0.1
get_module() is called by PHP when the latter tries to load a required library. It is considered the entry point for a lib. It is declared using the extern "C" modifier to comply with PHP lib requirement for the get_module() function. It also uses a macro PHPCPP_EXPORT which makes sure that get_module() is publicly exported, and thus callable by PHP.

So far, we have made some changes to the empty project to suit our needs. We can now compile and install this project and install the extension: 

make && sudo make install
 
Next, we need to copy the required files into the appropriate folders:

cp -f skeleton.so /usr/lib/php5/20121212
 
cp -f skeleton.ini /etc/php5/cli/conf.d
 
We just need to make sure that the skeleton.so lib is copied to the right location of PHP extensions (in my Ubuntu setup, it should be /usr/lib/php5/20121212 as shown above). 

We can then verify the extension is loaded in CLI by php -i | grep skeleton, and the terminal shall display something like this:


(Recall that the skeleton.ini is the file we modified above, which contains the extension=skeleton.so line.)

We have so far compiled and installed our first PHP extension using PHP-CPP. Of course, this extension does nothing yet. We will now create our first few functions to further understand the process of building PHP extensions.

“Hello, Taylor” function

The first function we create will be a slightly modified version of “Hello, World”. Let’s see the full code of main.cpp first:

#include <phpcpp.h>
 
#include <iostream>

void helloWorld (Php::Parameters &params)
{
    std::string name=params[0];
    std::cout<<"Hello "<<name<<"!"<<std::endl;

}

extern "C" {

    PHPCPP_EXPORT void *get_module() 
    {
        static Php::Extension extension("skeleton", "1.0");
        extension.add("helloWorld", helloWorld);

        return extension;
    }
}
According to the PHP-CPP documentation on “Register native functions“, it supports four types of function signatures to be called from PHP:

void example1();
 
void example2(Php::Parameters &params);
 
Php::Value example3();
 
Php::Value example4(Php::Parameters &params);
 
In this case, I am using the second signature and the parameters are passed by value in an array form (PHP feature). 

However, in helloWorld, we have specifically used C++ type std::string to grab the first parameter. We have also used C++ std lib to output a welcoming message. 

In get_module() function, after declaring the extension variable, we add the function we would like to export (helloWorld()) and assign a name visible to the PHP script (helloWorld). 

Now let’s compile and install the extension. If everything goes smoothly, the new skeleton.so file will be copied to the extension directory. 

We can write a simple script to test the function just created:
<?php

echo "Testing helloWorld in skeleton.so\n";
 
echo helloWorld('Taylor'); 
 
 echo helloWorld(1234+5678);
 
echo helloWorld(['string', 123+456]);
 
Please take some time to look at the output:

We will come back to what we have observed here later.

Function parameters by reference

Next, we will see another function which passes parameters by reference, a swap() function. In this function, we will also try to specify the number of parameters and their type. 

In main.cpp, we add one more function swap():
void swap(Php::Parameters &params) {
    Php::Value temp = params[0];
    params[0] = params[1];
    params[1] = temp;
}
And also export the function by specifying the number of parameters and their type:
extension.add("swap", swap,{
            Php::ByRef("a", Php::Type::Numeric),
            Php::ByRef("b", Php::Type::Numeric)
        });
We explicitly say that:
  • There will be two parameters (a and b);
  • They should be passed by reference (instead of by value);
  • They should be of type Numeric.
Let’s compile and install the updated extension again and write some code snippets to see how this new functions works:
<?php

$a=10;
$b=20;

// swap($a); 
// Above will create a segment fault

swap($a, $b);
echo $a."|".$b."\n";

$c=10;
$d="string";
swap($c, $d);
echo $c."|".$d."\n";

$e=10;
$f=new \DateTime();
swap($e, $f);
var_dump($e);
var_dump($f);
 
swap($a) will fail. This is expected and unexpected. The expected part is that we need two parameters and only one is given. But, shouldn’t that error be captured by PHP when calling the function swap and prompting us something like Not enough parameters?

The first call (swap($a, $b)) shows the expected result: 20|10. The function swaps the two numbers passed in. 

The second call is somehow unexpected: we have told PHP that we are to swap two numbers! But it just ignores the fact that the 2nd parameter passed is a string and does the swapping anyway!

Well, in a way, it is still expected. PHP does not really distinguish a number type and a string type. This behavior complies to the PHP standard. Also due to this behavior, we didn’t and can’t use C++ internal types for the temporary variable used in the function (temp) but used Php::Value as the variable type. 

The third call will work. The first var_dump will show the DateTime object and the second will show the integer. This is somehow very much unexpected (at least to me). After all, an object is quite different from a number/string. But after considering that this “swap” behavior is also doable in PHP, it fits in with PHP’s oddities. 

So, does it mean the “type” specification won’t have any impact? Not really. To further elaborate this, we create a third function:

void swapObject(Php::Parameters &params)
{
    Php::Value temp = params[0];
    params[0] = params[1];
    params[1] = temp;
}
And we register this function like this:
extension.add("swapObject", swap,{
            Php::ByRef("a", "sampleClass"),
            Php::ByRef("b", "sampleClass")
});
The testing code will be like this:
class sampleClass {
    var $i;
    public function __construct($n) {
        $this->i = $n;
    }
}

$o1 = new sampleClass(10);
$o2 = new sampleClass(20);

swapObject($o1, $o2);
echo $o1->i . "|" . $o2->i . "\n";

class anotherClass {
}

$d1 = new anotherClass();
$d2 = new anotherClass();

swapObject($d1, $d2);
 
The first call to swapObject() will work as we passed in the correct class type (sampleClass). The second will fail, displaying “PHP Catchable fatal error: Argument 1 passed to swapObject() must be an instance of sampleClass, instance of anotherClass given...“. 

The above code segment illustrates one important aspect on type restriction: scalar types declaration is not really implemented. PHP and thus PHP-CPP only enforce object-type declaration. Also, the number of parameters is not really enforced on the PHP side.

Source: Site Point

No comments:

Post a Comment