How to make an extension for PHP7 harder than “hello, world”, and not become a red-eyed. Part 1

  • Tutorial

What for?


I am writing this article so that the reader, who has taken a total of not less than a year in total, the reader can walk in 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 that 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


  1. First we install the php7.2-dev package, in it the phpize script necessary 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

  2. 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
    

  3. 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 for the first time you need to launch it with your hands.

#запустим
clion

Creating an extension


Here we have at least 3 options:

  1. Take the raw standard disc from the php sources we downloaded.
  2. Saw a standard blank with a special ext_skel script
  3. 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 developer disc is still a pleasure :-)

  1. 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
    

  2. Here I will set the parameters of only 1 function that the created extension will implement. This is just a demonstration function to show how this is done.

    We will do 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
    

  3. Now run the ext_skel script, giving it the name of the extension and the proto file we created.

    
    ./ext_skel --extname=libtrie --proto=./libtrie.proto
    

  4. 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 I mentioned above, there are only 3 files.


config.m4 	
php_libtrie.h
libtrie.c

I don’t 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
to
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])
iftest"$PHP_LIBTRIE" != "no"; then# если понадобится включить какие-то дополнительные заголовочные файлы# PHP_ADD_INCLUDE()# ключевая строка
  PHP_NEW_EXTENSION(libtrie, php_libtrie.c, $ext_shared)
  # PHP_NEW_EXTENSION(libtrie, php_libtrie.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)fi



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.

#выход из рутового башаexit

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)
# задаем переменную phproot, чтобы удобнее прописывать пути к файлам phpset(phproot /usr/local/src/php-7.2.11/)
# тут указываются каталоги, которые нужно включить в проект# мы делаем это чтобы заставить clion понимать внутренние функции и макросы самого php
include_directories(${phproot})
include_directories(${phproot}TSRM/)
include_directories(${phproot}main/)
include_directories(${phproot}Zend/)
# без этой строки clion не сможет прочитать файл и не будет ничего индексировать
add_executable(php-ext-libtrie php_libtrie.c)



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


We open our file with the body of our extension php_libtrie.cand
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 arrange everything, I do this: I transfer
all include with headers from a php_libtrie.cfile in php_libtrie.h, in the first file only 1 record remains:

#include"php_libtrie.h"



The file php_libtrie.hwill contain all other necessary inclusions.



Contents of my header file
#ifndef PHP_LIBTRIE_H#define PHP_LIBTRIE_H#ifdef HAVE_CONFIG_H#include"config.h"#endif#include<stdarg.h> //тут для макроса va_start()#include<inttypes.h> //тут стандартные числовые типы//нужные константы#if defined(__GNUC__) && __GNUC__ >= 4# define ZEND_API __attribute__ ((visibility("default")))# define ZEND_DLEXPORT __attribute__ ((visibility("default")))#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"php.h"#include"php_ini.h"#include"zend.h"#include"zend_types.h"//ZVAL_COPY_VALUE#include"ext/standard/info.h"#include"zend_API.h"#include"zend_modules.h"#include"zend_string.h"#include"spprintf.h"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:

  1. 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 //не знаю что это
    };
    

  2. 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)		/* For testing, remove later. */
    	PHP_FE(my_array_fill,	NULL)
    	PHP_FE_END	/* Must be the last line in libtrie_functions[] */
    };
    

    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 in 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_valuereturn;
}




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.

# Это заставит наш php, подключить наше скомпилированное расширение из каталога# modules. Ключ -a заставит php работь в режиме командной строки
php -d extension=modules/libtrie.so -a

Enter in php console:

print_r(my_array_fill(50, 2, "hello, baby!"));

Enjoying the result.



Someone will ask, but where is the trie? About the functions that implement the work trie, I will write in the second part.

Stay tuned.

Also popular now: