What for?
I am writing this article so that the reader, who took a total of not less than a year to complete the journey, could walk a couple of hours. As my personal experience has shown, simply programming in C is somewhat easier than making a serious PHP extension work. Here I will tell you in as much detail as possible how to make an extension using the example of the
libtrie library, which implements a prefix tree, better known as trie. I will write and perform the described actions in parallel on a freshly installed Lubuntu 18.04 system.
Let's start.
Software installation
Php
- First we install the php7.2-dev package, in it the phpize script needed for building the extension. In addition, we need a working version of php, on which we will check our extension. Installing this package will pull up a certain number of dependent packages; we put everything that is offered.
sudo apt install php7.2-dev
- Go to the site php.net, go to the downloads section and pull out a link to the archive with the latest stable version of php, now it is version 7.2.11.
Downloading the php source archive:
')
cd /tmp && wget http://it2.php.net/get/php-7.2.11.tar.gz/from/this/mirror -O php7.tar.gz
- Now unpack the archive yourself:
sudo tar -xvf php7.tar.gz -C /usr/local/src
Code editors
I usually use 2 code editors. Simple and fast geany and pretty brake, but very advanced clion of the company JetBrains. Geany install from standard turnips Ubuntu.
sudo apt install geany
Clion download from the official website of JetBrains:
cd ~/Downloads && wget https://download.jetbrains.com/cpp/CLion-2018.2.5.tar.gz -O clion.tar.gz
sudo tar -xvf clion.tar.gz -C /usr/share
Make a link to make it convenient to run clion from the console.
sudo ln -s /usr/share/clion-2018.2.5/bin/clion.sh /usr/bin/clion
After the first launch, clion will create shortcuts for itself from the LXpanel shell menu, but the first time you need to launch it with your hands.
Creating an extension
Here we have at least 3 options:
- Take the raw standard disc from the php sources we downloaded.
- Saw a standard blank with a special ext_skel script
- Get a good minimalist blank from here .
The third option I like the most, but I will use the second, in case of failure to minimize the number of places where I could be mistaken. Although picking a pig of developers is still a pleasure :-)
- Go to the directory with standard php extensions.
cd /usr/local/src/php-7.2.11/ext
In addition to the name of the script, you can set some extension parameters through the proto file. All this can not be done. I'll do everything with my hands, but how proto works will show. We make a trie, so we will call our extension libtrie. To work in the / usr / local / src directory, you need administrator privileges, so as not to endlessly write sudo, I will enable bash with elevated privileges.
sudo bash
- Here I will set the parameters of only 1 function that the extension will implement. This is just a demonstration function to show how this is done.
We will make a complete analog of the standard function.
array array_fill ( int $start_index , int $num , mixed $value )
The syntax in the proto file is very simple, you just need to specify the name of the function. I will write a little more to look more informative.
echo my_array_fill \( int start_index , int num , mixed value \) >> libtrie.proto
- Now run the ext_skel script, giving it the extension name and the proto file we created.
./ext_skel --extname=libtrie --proto=./libtrie.proto
- We have created a directory with our extension. Let's go into it.
cd libtrie
File structure and build principle
File structure config.m4 - phpize ./configure, makefile. CREDITS - , , libtrie.c - php_libtrie.h - config.w32 - windows EXPERIMENTAL - . , . libtrie.php - php . tests -
To successfully build the extension, you need only 3 files. In the minimalist extension bar, which I mentioned above, there are only 3 files.
config.m4 php_libtrie.h libtrie.c
I do not like the standard naming convention in php; I like the header files and files with the body of the program to be called the same. Therefore rename
libtrie.c
at
php_libtrie.c
mv libtrie.c php_libtrie.c
Editing config.m4
The default config.m4 file is literally stuffed with content, the abundance of which is confusing and confusing. As I said, this file is needed to form the configure script. Read more about it
here .
geany config.m4 &
We leave only this:
PHP_ARG_ENABLE(libtrie, whether to enable libtrie support, [ --enable-libtrie Enable libtrie support]) if test "$PHP_LIBTRIE" != "no"; then

The first macro creates the ability to enable and disable our extension when running the configure script being created.
The second block is the most important, it determines which files will be compiled as part of our extension, whether the extension will be dynamically connected via the .so file, or the extension will be static and will be integrated when building php. Our will be dynamic.
Save the file.
Copy the file in the user directory, so as not to work in root mode.
We copy:
cp /usr/local/src/php-7.2.11/ext/libtrie ~/Documents/ -r
Demonstration function
Let me remind you that we will do the full analogue of array_fill (). I will work through clion, but this guide can be done in any editor. Clion is good in that it allows you to automatically do a basic syntax check, and also supports quick transition to files or functions via ctrl + click. In order for such transitions to work in our extension, you will have to configure the CMakeLists.txt file.
For clion to work properly, you will need to install the cmake build system, along with which a bunch of dependent packages will be installed. Install all this with the command:
sudo apt install cmake
Cmake setup
Open our catalog with the extension in clion. We create a CMakeLists.txt file with the following contents through the context menu by clicking on the name of the root directory at the top of the screen:
cmake_minimum_required(VERSION 3.12) project(php-ext-libtrie C) set(CMAKE_C_STANDARD 11)

Maybe someone knows how to make this file shorter so that clion starts indexing the project files. I did not find a shorter way. If someone knows, write in the comments.
Demo function code
Open our body file with our extension
php_libtrie.c
and
delete all comments so that they do not confuse us.


Clion checks whether all the functions and macros used in the code have been declared and throws an error if this is not the case. Obviously, PHP developers do not use clion, but they probably would have done something with it. To prevent these errors from appearing in our extension, we include the missing header files to us.
To streamline everything, I do this:
I include all include with headers from the
php_libtrie.c
file in
php_libtrie.h
, only 1 entry remains in the first file:
#include "php_libtrie.h"

In the file
php_libtrie.h
will be all the other necessary inclusions.

Content of my header file #ifndef PHP_LIBTRIE_H #define PHP_LIBTRIE_H #ifdef HAVE_CONFIG_H #include #endif #include <stdarg.h> // va_start() #include <inttypes.h> // // #if defined(__GNUC__) && __GNUC__ >= 4 # define ZEND_API __attribute__ ((visibility())) # define ZEND_DLEXPORT __attribute__ ((visibility())) #else # define ZEND_API # define ZEND_DLEXPORT #endif # define SIZEOF_SIZE_T 8 // ZVAL_COPY_VALUE() #ifndef ZEND_DEBUG #define ZEND_DEBUG 0 #endif // , #include #include #include #include //ZVAL_COPY_VALUE #include #include #include #include #include extern zend_module_entry libtrie_module_entry; ...
If everything is done correctly, the clion checker will show a yellow or green square in the upper right corner, which means that there are no critical errors.

A small theoretical retreat
For the normal operation of the expansion you need 2 things:
- It is necessary to initialize the special structure zend_module_entry, which contains the following:
zend_module_entry libtrie_module_entry = { STANDARD_MODULE_HEADER, // "libtrie", // libtrie_functions, // PHP_MINIT(libtrie), //, PHP_MSHUTDOWN(libtrie), // PHP_RINIT(libtrie), /* Replace with NULL if there's nothing to do at request start */ PHP_RSHUTDOWN(libtrie), /* Replace with NULL if there's nothing to do at request end */ PHP_MINFO(libtrie), // , php phpinfo() PHP_LIBTRIE_VERSION, // , STANDARD_MODULE_PROPERTIES // };
- Initialize the same array that contains all the functions of our extension.
Here, through a special macro wrapper PHP_FE (), the names of all functions in our extension are specified. In general, macros are very actively used in PHP, there are a lot of such macros that simply refer to other macros, and those in turn are further. It is necessary to get used to this. You can figure out if you go through the macros via ctrl + click. Here just clion is irreplaceable.
Remember the proto file? We set there 1 function my_array_fill (). So now we have 3 elements here:
const zend_function_entry libtrie_functions[] = { PHP_FE(confirm_libtrie_compiled, NULL) PHP_FE(my_array_fill, NULL) PHP_FE_END };
The first line is a test function that will indicate that the extension is working, the second is our function, the third is a special macro that should be the last in this array.
Find our function:
PHP_FUNCTION(my_array_fill)
As you can see, it is also initialized through a macro. The thing is that all php functions do not return anything (to be exact, they return void) inside C, and their arguments cannot be changed. Somewhere it is even convenient.
If you look at the file structure (part of the window on the left), all the functions of the file are listed here, but in the form in which they will be after precompiling macros. The screenshot shows that our function my_array_fill will actually be zif_my_array_fill.

Arguments from the depths of php to our C function we get a macro. More information about this macro can be found in the file:
/usr/local/src/php-7.2.11/README.PARAMETER_PARSING_API
Below is the code of our analog function with detailed explanations.
Code PHP_FUNCTION(my_array_fill) { // , // 2 : // zend_execute_data *execute_data, zval *return_value // , //zend_long int64_t x64 int32_t x86 // zend_long zend_long start_index; //1 , zend_long num; //2 zval *value; // mixed , zval, // , if (zend_parse_parameters(ZEND_NUM_ARGS(), "llz", &start_index, &num, &value) == FAILURE) { /* * RETURN_ * return_value */ RETURN_FALSE; } // , - if (num <= 0) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "argument 2 must be > 0"); // RETURN_FALSE; } //zval *return_value , // zval, , - // zend_long unsigned int32. // + -. .. 1, 3, 4 array_init_size(return_value, (uint32_t)(start_index + num)); // , , for(zend_long i = start_index, last = start_index + num; i < last; ++i) { // zval add_index_zval(return_value, i, value); } // , return_value return; }
Build and test extensions
First, run phpize, which will make us the configure file.
phpize
Now run ./configure, which will make the makefile.
./configure
Finally, run make, which will build us our extension.
make
Check out what we did.
Enter in php console:
print_r(my_array_fill(50, 2, "hello, baby!"));
Enjoying the result.

Someone will ask, but where is the trie? I will write about functions implementing trie in the second part.
Stay tuned.