Inspired by a chapter on compiling from source in How Linux Works, I decided to practice a bit by building a PHP extension. Compiling programs from source is a valuable skill, and what better way to practice than by extending everybody's favourite language, PHP? This post explores creating a simple PHP extension to add a function that wraps a string in a decorative border and experiment with creating a new class.
What are PHP extensions?
PHP extensions are compiled libraries that enhance PHP's core functionality. Written in C or C++, they integrate with the PHP engine to provide new features, boost performance, or connect to external systems.
Writing a PHP extension is useful when you need to:
- Add unique functionality not in PHP's standard library.
- Improve performance for compute-intensive tasks.
- Interface with custom or low-level libraries.
- Optimize specific application workflows.
Why not just create a standard PHP library, you ask? Well, creating an extension gives us access to pretty low level PHP plumbing, so when we want to squeeze every little bit of performance from the language that might be the best way. And didactically it's also a great way to learn how PHP works behind the scenes.
Project goal
The goal is to create a PHP extension with:
- A
printLuLu(string, length=10)
function that prints a string framed by alternating=
and-
lines. - A
PrintLu
class with two methods:printWrap(string, length=10)
: Prints a string with the same border.getLuLen(string)
: Returns the string's length.
Getting started
I followed tutorials from PHP Internals Book and Zend's guide. The former is detailed but slightly outdated, while the latter is more recent but less in-depth.
PHP vs. Zend Extension
PHP extensions add user-facing functionality, while Zend extensions modify the engine itself. Since I'm adding a simple function and class, a PHP extension suffices. Extensions can be compiled as shared libraries or statically linked into the PHP binary. I'm going with static linking for this one.
Setting up
First, I cloned a forked PHP source repository: php-src-fork and build PHP from it:
sudo apt install -y pkg-config build-essential autoconf bison re2c libxml2-dev libsqlite3-dev
./buildconf
./configure --enable-debug
make -j4
Generating the extension skeleton
PHP provides a script to create an extension template that generates boilerplate code and saves time on setup:
./sapi/cli/php ext/ext_skel.php --ext print_lu
Generated code can be cound in ext/print_lu.
Navigating the code
PHP extensions make heavy use of macros that simplify many tasks. However, these macros can make code hard to understand sometimes. To check out what gets replaced I can run the preprocessor that would inline the macros code:
gcc -E -P -Imain -I. -IZend -ITSRM ext/print_lu/print_lu.c > comp.c && clang-format -i comp.c
This creates a formatted comp.c
file with expanded macros.
Writing tests
PHP's testing framework is well-documented. The skeleton includes sample tests, so it's easy to figure out the structure. I'm writing 4 tests:
- Check if the
print_lu
extension is loaded. - Test
printLuLu
output. - Test
PrintLu::printWrap
. - Test
PrintLu::getLuLen
.
--TEST--
Check if print_lu is loaded
--EXTENSIONS--
print_lu
--FILE--
<?php
echo 'The extension "print_lu" is available';
?>
--EXPECT--
The extension "print_lu" is available
--TEST--
print_lulu Basic test
--EXTENSIONS--
print_lu
--FILE--
<?php
printLuLu("Hello, World!");
printLuLu("Hello, World!", 4);
?>
--EXPECT--
=-=-=-=-=-
Hello, World!
=-=-=-=-=-
=-=-
Hello, World!
=-=-
--TEST--
printWrap Basic test
--EXTENSIONS--
print_lu
--FILE--
<?php
$printLu = new PrintLu();
$printLu->printWrap("Hello, World!", 4);
?>
--EXPECT--
=-=-
Hello, World!
=-=-
--TEST--
getLuLen Basic test
--EXTENSIONS--
print_lu
--FILE--
<?php
$printLu = new PrintLu();
$len = $printLu->getLuLen("Hello World");
var_dump($len);
?>
--EXPECT--
int(11)
Run tests with:
make test TESTS="ext/print_lu"
Initially, tests were skipped because the extension wasn't loaded, so to enable it, I rebuilt PHP:
make clean
./buildconf
./configure --enable-print_lu --enable-debug
make -j4
This made the first test pass.
Implementing the function
Let's implement the first function. We need to add code to print_lu.c as well print_lu.stub.php, removing the old test functions and adding the new definitions. The convenient thing is that a lot of boilerplate code will be generated for us.
File print_lu.c defines the inner workings of the function using the PHP_FUNCTION macro as well as param parsing macros:
PHP_FUNCTION(printLuLu)
{
zend_long line_len = 10;
char *var;
size_t var_len;
size_t i;
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STRING(var, var_len)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(line_len)
ZEND_PARSE_PARAMETERS_END();
for (i = 0; i < line_len; i++) {
php_printf("%c", i % 2 == 0 ? '=' : '-');
}
php_printf("\n");
php_printf("%s\n", var);
for (i = 0; i < line_len; i++) {
php_printf("%c", i % 2 == 0 ? '=' : '-');
}
php_printf("\n");
RETURN_NULL();
}
File print_lu.stub.php defines the function interface:
function printLuLu(string $string, int $len = 10): void {}
This makes our second test pass
Implementing the class
I added the PrintLu
class to print_lu.stub.php
:
class PrintLu {
public function printWrap(string $string, int $len = 10): void {}
public function getLuLen(string $string): int {}
}
The class methods use the PHP_METHOD
macro:
PHP_METHOD(PrintLu, printWrap)
{
zend_long line_len = 10;
char *var;
size_t var_len, i;
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STRING(var, var_len)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(line_len)
ZEND_PARSE_PARAMETERS_END();
for (i = 0; i < line_len; i++) {
php_printf("%c", i % 2 == 0 ? '=' : '-');
}
php_printf("\n%s\n", var);
for (i = 0; i < line_len; i++) {
php_printf("%c", i % 2 == 0 ? '=' : '-');
}
php_printf("\n");
RETURN_NULL();
}
PHP_METHOD(PrintLu, getLuLen)
{
char *var;
size_t var_len;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(var, var_len)
ZEND_PARSE_PARAMETERS_END();
RETURN_LONG(var_len);
}
PHP_MINIT_FUNCTION(print_lu)
{
zend_class_entry *print_lu_ce = register_class_PrintLu();
return SUCCESS;
}
And now we have all the tests passing!
Next steps
This is a fairly simple example, but shows that starting writing a simple PHP extension is not overly complicated. Having some basic knowledge of this process allows us to inspect other extensions and understand their inner workings. There's much more that can be done with these extensions, but that's too much to cover in one post. I recommend reading the linked sources for more info and experimenting.