Crear un módulo de PHP con C++

PHP y C++ son dos lenguajes de programación.

PHP es normalmente usado como lenguaje del lado del servidor en el desarrollo web. Está interpretado lo que significa que el programa esta escrito en texto plano y un programa en tiempo de ejecución lo interpreta y ejecuta las instrucciones.

C++ tiene aplicación en muchos dominios. El programa es también escrito en texto plano pero un compilador lo traduce en instrucciones de máquina en un fichero ejecutable en tiempo de compilación y es el ejecutable lo que corre por si solo en tiempo de ejecución.

Los dos lenguajes soportan el paradigma de Programación Orientada a Objetos.

Una diferencia importante en los dos es que C++ puede ser muchas veces más rápido que PHP en el mismo dominio. Esto no viene gratis, porque es mucho mas rápido programar y tener algo que funcione en PHP.

PHP es mas fácil de programar y de depurar y no hay tiempo de compilación mientras que C++ es todo lo contrario.

Hay escenarios donde es mas conveniente invertir tiempo en desarrollo para aprovechar mas la máquina por varias razones como que tengamos grandes cargas de trabajo o porque nos cobran por poder de computación de modo que queremos reducir el sobre coste de interpretar el código.

Pero no tenemos que elegir entre uno y otro, podemos trabajar con los dos codo con codo.

Si en un desarrollo de PHP identificamos un cuello de botella podemos reemplazar piezas de código por C++.

La forma de hacer esto es creando un módulo de PHP con una interfaz que defina funciones y clases de PHP las cuales están realmente implementadas en C++.

Este código correría a toda velocidad en tu CPU ya que no necesita ser interpretado.

En este artículo voy a describir como preparar el entorno para hacer esto en Windows.
Voy a usar XAMP 8.1.12 el cual trae PHP 8.1.16 64bits Thread Safe, y Visual Studio 2022.

PHP 8.1.16 en Windows esta compilado con Visual C y el Platform Tool MSVC 14.26.
Este Platform tool MSVC 14.19 pertenece a Visual Studio 2015 pero es demasiado antiguo.
En vez de eso vamos a instalar MSVC 14.29 en nuestro Visual Studio 2022. Que por cierto, también puede funcionar en VS 2017 y VS 2019.

CONFIGURANDO EL ENTORNO:

Por lo tanto necesitamos el Visual Studio Instaler y hacer click en Modify.

En la siguiente ventana podemos seleccionar uno de esos MSVCs. Ya que estoy explicándolo en VS 2022, seleccionaré la versión mas moderna con la que pueda trabajar que es exactamente la versión con que se compiló PHP8, MSVC 14.29.

También necesitamos descargar los ficheros de cabecera de PHP.

En la dirección https://windows.php.net/download/ busca PHP 8.1 VS16 x64 Thread Safe cuyo enlace es https://windows.php.net/downloads/releases/php-devel-pack-8.1.16-Win32-vs16-x64.zip

El fichero zip contiene las cabeceras para muchas librerías de PHP y los ficheros .lib para enlazar con Visual Studio.
Por lo tanto descomprime lo en alguna parte.

Ahora creamos un proyecto en Visual Studio. Queremos una librería dinámica pero podemos empezar con un proyecto vacío.

En mis ejemplos he llamado al proyecto MyPHPModule.

Ahora necesitamos establecer las rutas a las librerías y el Platform Toolset.
Hacemos click con el botón derecho del ratón sobre el proyecto y vamos a propiedades.
Nos aseguramos de que Configuration está puesto a All Configurations y Platform a x64.

En la ventana de Propiedades vamos a Configuration PropertiesGeneral y ponemos:

Configuration Type as Dynamic Library (.dll)
Platform Toolset as Visual Studio 2019 (v142) 

Y por nuestra comodidad vamos a cambiar el Output Directory por la ruta a la carpeta de los módulos en nuestra distribución de PHP. En mi caso es C:\xampp\php\ext. De manera que la librería se construye justo donde esté disponible para el uso de PHP.

Ahora vamos a Configuration PropertiesVC++ Directories y en la propiedad Include Directories ponemos las rutas:

php-8.1.16-devel-vs16-x64\include
php-8.1.16-devel-vs16-x64\include\main
php-8.1.16-devel-vs16-x64\include\TSRM
php-8.1.16-devel-vs16-x64\include\win32
php-8.1.16-devel-vs16-x64\include\Zend

Donde php-8.1.16-devel-vs16-x64 es el directorio donde habíamos descomprimido las librerías. Obviamente tu tienes que poner ese directorio a esas 5 carpetas.

En la propiedad Library Directories ponemos la misma ruta para php-8.1.16-devel-vs16-x64\lib.

Ahora vamos a Configuration PropertiesC/C++Preprocessor y en la propiedad Preprocessor Definitions ponemos lo siguiente:

ZEND_DEBUG=0
ZTS=1
ZEND_WIN32
PHP_WIN32

Esto podría parecer confuso al principio. Estamos compilando para la plataforma x64 pero aún así tenemos que usar los flags ZEND_WIN32 y PHP_WIN32. Estos _WIN32 solo significan Windows.

La última configuración ahora es in Configuration PropertiesLinkerInput y en la propiedad Additional Dependencies el valor php8ts.lib.

Ya estamos listos para empezar.

PROGRAMAR NUESTRO PRIMER MÓDULO.

Ahora vamos a crear un fichero .cpp. En mi caso el fichero MyPHPModule.cpp.
Mi Módulo va a tener una función que imprime “Hello World!!”, una función que toma dos parámetros y devuelve su producto, una función que cuenta hasta un número dado, y una función que toma dos matrices y devuelve otra matriz con la suma de las dos elemento a elemento.

Primero incluimos la librería PHP:

#include <php.h> 

Y ahora definimos las cuatro funciones:

PHP_FUNCTION(hello_world) {
	
	ZEND_PARSE_PARAMETERS_NONE();

	php_printf("Hello World!\n");

	RETURN_NULL();
}

PHP_FUNCTION(counting)
{
	zend_long n;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_LONG(n)
		ZEND_PARSE_PARAMETERS_END();

	volatile zend_long count = 0;
	for (; count < n; count++);

	RETURN_LONG(count);
}

ZEND_FUNCTION(multiply)
{
	zend_long x, y;

	ZEND_PARSE_PARAMETERS_START(2, 2)
		Z_PARAM_LONG(x)
		Z_PARAM_LONG(y)
		ZEND_PARSE_PARAMETERS_END();

	RETURN_LONG(x * y);
}

PHP_FUNCTION(sum_arrays) {
	
	zval *arr1 = nullptr, *arr2 = nullptr;
	

	//we accept two params and the two are required
	ZEND_PARSE_PARAMETERS_START(2, 2)
		Z_PARAM_ARRAY(arr1) //the param 1 is an array
		Z_PARAM_ARRAY(arr2) //the param 2 is an array
	ZEND_PARSE_PARAMETERS_END();

	std::size_t arr1c = 0, arr2c = 0;
	arr1c = zend_array_count(Z_ARR_P(arr1));
	arr2c = zend_array_count(Z_ARR_P(arr2));

	zend_array *za1 = nullptr, *za2 = nullptr;
	std::size_t siz;

	//if the arra1 is longer than the arra2
	if (arr1c > arr2c) {
		za1 = Z_ARR(*arr1);
		za2 = Z_ARR(*arr2);
		siz = arr2c;
	}
	//if the arra2 is longer than the arra1
	else {
		za1 = Z_ARR(*arr2);
		za2 = Z_ARR(*arr1);
		siz = arr1c;
	}

	//we go through the array
	for (std::size_t index = 0; index < siz; index++) {
		//add the element of arr2 to arra1
		Z_LVAL_P(zend_hash_index_find(za1, index)) += Z_LVAL_P(zend_hash_index_find(za2, index));
	}

	//we return the arra1 with the values added.
	RETVAL_ARR(za1);
}

Como habrás notado usamos macros para declarar las funciones y es dentro de las funciones que extraemos los parámetros con las macros ZEND_PARSE_PARAMETERS_* y Z_PARAM_*.

ZEND_PARSE_PARAMETERS_START acepta dos parámetros, el primero es el mínimo número de parámetros que la función recibe, los que son obligatorios, y el segundo número es el número máximo de parámetros.

No es como los prototipos de funciones en C o C++. Esto es porque estas funciones son realmente puntos de entrada al módulo. Parámetros y valores de retorno entran y salen del módulo a través de estas funciones declaradas de esta manera. Podemos implementar la funcionalidad justo ahi o podemos llamar a otra función dentro de ellas.

El prototipo de estas funciones se expresa de esta forma:

//prototype of function hello_world; 
ZEND_BEGIN_ARG_INFO(arginfo_hello_world, 0) 
ZEND_END_ARG_INFO() 

//prototype of function counting; 
ZEND_BEGIN_ARG_INFO(arginfo_counting, 0) 
	ZEND_ARG_INFO(0, n) 
ZEND_END_ARG_INFO() 

//prototype of function multiply; 
ZEND_BEGIN_ARG_INFO(arginfo_multiply, 0) 
	ZEND_ARG_INFO(0, x) 
	ZEND_ARG_INFO(0, y) 
ZEND_END_ARG_INFO() 

//prototype of function sum_arrays; 
ZEND_BEGIN_ARG_INFO(arginfo_sum_arrays, 0) 
	ZEND_ARG_ARRAY_INFO(0, arr1, 0) 
	ZEND_ARG_ARRAY_INFO(0, arr2, 0) 
ZEND_END_ARG_INFO() 

La macro ZEND_BEGIN_ARG_INFO inicia variables llamadas arginfo_ y la macro ZEND_ARG_INFO declara cada parámetro, si por referencia o por valor, el nombre, y si acepta valores nulos.

En la siguiente pieza de código reunimos las funciones con sus definiciones:

static zend_function_entry hello_world_functions[] = {

	PHP_FE(hello_world, arginfo_hello_world)
	PHP_FE(counting, arginfo_counting)
	PHP_FE(multiply, arginfo_multiply)
	PHP_FE(sum_arrays, arginfo_sum_arrays)
	PHP_FE_END
};

Como puedes ver, es una matriz y con las macros PHP_FE unimos la función y su arginfo_<function>
Podemos también declarar callbacks como PHP_MINFO_FUNCTION:

PHP_MINFO_FUNCTION(hello_world) 
{ 
	php_info_print_table_start(); 
		php_info_print_table_header(2, "Hello World Module", "enabled"); 				
        php_info_print_table_row(2, "Some parameter", "Some value");
	php_info_print_table_end();
} 

el cual es para imprimir detalles acerca del modulo cuando corremos la función phpinfo().

Finalmente podemos poner todo junto con esta otra estructura:

/* Define the module entry */ 
extern zend_module_entry hello_world_module_entry = { 
	STANDARD_MODULE_HEADER, 
	"hello_world", 			/* Extension name */ 
	hello_world_functions,	/* zend_function_entry */ 
	NULL, 					/* PHP_MINIT - Module initialization */ 
	NULL, 					/* PHP_MSHUTDOWN - Module shutdown */ 
	NULL, 					/* PHP_RINIT - Request initialization */ 
	NULL, 					/* PHP_RSHUTDOWN - Request shutdown */ 
	PHP_MINFO(hello_world), /* PHP_MINFO - Module info (PHP Info) */ 
	"1.0", 					/* Version */ 
	STANDARD_MODULE_PROPERTIES 
}; 

Y finalmente registramos el módulo:

ZEND_GET_MODULE(hello_world) 

CONFIGURANTO PHP:

Asumiendo que el modulo se compila en la carpeta de los módulos, en mi caso C:\xampp\php\ext, por tanto ya está ahí, tan solo necesitamos abrir el fichero php.ini, en mi caso en la ruta C:\xampp\php y añadir la línea:

extension=MyPHPModule.dll

Ahora podemos probar si el módulo esta ahí con el siguiente comando en la línea de comandos:

php -m | findstr hello_world 

Si está presente obtendremos hello_world.

Otra manera de probarlo es que hagamos un fichero .php con una llamada a phpinfo():

<?php
phpinfo();

y con el navegador exploramos ese fichero. En mi servidor local esta en la url:

http://localhost/MyPHPModule/

PHP mostrará los detalles que pusimos en la función callback PHP_MINFO_FUNCTION

Si vemos cualquiera de ellos, el modulo esta correctamente instalado y ya estamos listos para usarlo en PHP.

PRUEBAS EN PHP:

Podemos crear un fichero .php y llamar las cuatro funciones que implementamos en C++.

Podemos empezar con este pequeño script:

<?php 
echo '<pre>'; 
hello_world();

Si lo visitamos con el navegador obtendremos Hello World!.

Probemos el siguiente test:

$times = 100000;

$milliseconds = floor(microtime(true) * 1000);

//loop that only counts
for($n = 0; $n < $times ; $n++){
	;
}

$milliseconds = floor(microtime(true) * 1000) - $milliseconds;

echo "PHP loop took milliseconds: $milliseconds\n";

Vamos a comparar el rendimiento de tan solo contar.
Vamos a establecer un numero de veces que queremos contar en la variable $times.

Después capturamos el timestamp y justo despues tenemos un bucle for.

Después del bucle for obtenemos el tiempo otra vez y le restamos el tiempo que cogimos previamente de modo que obtenemos el tiempo que le tomo ejecutar el bucle.

Ahora hacemos lo mismo con nuestra función counting().

$milliseconds = floor(microtime(true) * 1000);

//calling our counting function
$count = counting($times);

$milliseconds = floor(microtime(true) * 1000) - $milliseconds;

echo "Our module's loop took milliseconds: $milliseconds\n";

Cuando comparamos uno con el otro obtenemos tal resultado como este:

PHP loop took milliseconds: 7 
Our module's loop took milliseconds: 0 

Puedes probar con un número mas grande.

La siguiente pieza es para probar la función multiply():

//testing our multiply function.
echo '3 * 3 = ' . multiply(3,3) . PHP_EOL;

Esta función no presenta ninguna mejora de rendimiento realmente. Es solo una demostración de una función que acepta parámetros y devuelve un resultado.

La última prueba es nuestra función sum_arrays():

//the first array contains numbers from 0 through 10
$arr = range(0, 10);

//the second array only then 1s
$arr2 = array_fill(0, 10, 1);

//we sum the values of the first and second arrays into a third array;
$arr3 = sum_arrays($arr, $arr2);

print_r($arr3);

En esta prueba creamos dos matrices, una tiene números de 0 a 10 y la otra solo unos.
La función suma una matriz a la otra elemento por elemento.

La función es cerca de 20 veces mas rápida que su versión en PHP.

Podríamos hacerla incluso mas rápida si nuestro módulo fuese capaz de enviar esas matrices a la GPU y paralelar el cálculo.

Tu puedes ver el potencial de mover partes de PHP a C++.

Espero que disfrutes de este tutorial.

Comentarios

Entradas populares