Backend Development 12 min read

phpy v2 – A High‑Performance PHP‑Python Bridge: Architecture, Usage, and Benchmarks

The article introduces the second version of phpy, a PHP extension that embeds a Python interpreter, explains its dual‑VM runtime architecture, shows how to use it under PHP‑FPM, presents detailed performance benchmarks, covers exception handling, IDE auto‑completion, compilation options, dynamic‑library troubleshooting, and provides numerous code examples for testing and scientific computing.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
phpy v2 – A High‑Performance PHP‑Python Bridge: Architecture, Usage, and Benchmarks

Last week we released the first version of the phpy project, which attracted many PHP developers who submitted issues, pull requests, and suggestions. After a week of optimization, the new version brings significant improvements, and this article answers common questions and outlines the changes in version 2.

Runtime Principle

Within a single process, both the ZendVM and the CPython VM are created, allowing direct C‑function calls between them. The only overhead is the conversion between zval and PyObject structures, resulting in very high performance.

phpy is built on the official PHP ZendAPI and the Python Py C API , without any external C library dependencies, making it cross‑platform for Linux, Windows, and macOS.

Using Under PHP‑FPM

The first version discouraged use under PHP‑FPM . Subsequent tests showed that importing a Python package the first time incurs a larger cost, but subsequent imports reuse the cached module in Python sys.modules , so phpy works fine in short‑lived environments like PHP‑FPM or Apache .

It is even possible to keep certain objects resident in memory across PHP‑FPM requests.

<code>$app = PyCore::import('app.user');
$storage = $app->storage;
if (!isset($storage['data'])) {
    $storage['data'] = uniqid();
    var_dump("no cache");
    $o = new stdClass();
    $o->hello = uniqid();
    $storage['obj'] = $o;
} else {
    var_dump("cached");
    var_dump(strval($storage['data']));
    var_dump($storage['obj']);
}</code>

The code above persists a Python dictionary in PHP‑FPM , enabling memory reuse.

phpy sets a memory safety boundary; even if a Python‑persisted PHP object survives a request, it is destroyed and set to NULL , preventing memory errors.

Performance Tests

A stress‑test script creates a PyDict and performs 10 million read/write operations from both PHP and Python code.

PHP version: PHP 8.2.3 (cli) (NTS)

Python version: Python 3.11.5

OS: Ubuntu 20.04

GCC version: 9.4.0

Note: the test requires a 2 GB hash table (≈10 million elements) and at least 2 GB of RAM.

Result Comparison

<code>(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$ php dict.php 
 dict set: 4.663758 seconds
 dict get: 3.980076 seconds
(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$ php array.php 
 array set: 1.578963 seconds
 array get: 0.831129 seconds
(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$ python dict.py 
 dict set: 5.321664 seconds
 dict get: 4.969081 seconds
(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$</code>

Using the Python benchmark as a baseline:

Script

Set

Get

dict.php

114%

125%

array.php

337%

599%

phpy writes to a PyDict about 14 % faster than native Python and reads about 25 % faster.

PHP writing to a PHP array is up to 237 % faster than Python writing to a dict, and reads are nearly 500 % faster.

Exception Handling

The latest version merges Python and PHP exceptions, allowing PHP code to catch exceptions raised during Python execution.

<code>try {
    PyCore::import('not_exists');
} catch (PyError $e) {
    PyCore::print($e->error);
    PyCore::print($e->type);
    PyCore::print($e->value);
    PyCore::print($e->traceback);
}</code>

The underlying system sets $e->value to the exception message; use $e->getMessage() to retrieve it.

PyError does not set $e->code ; avoid using it.

IDE Auto‑Completion

phpy provides a tool to generate IDE auto‑completion files.

<code>cd phpy/tools
php gen-lib.php [Python package name]</code>

Example for matplotlib.pyplot :

Import directly: PyCore::import('matplotlib.pyplot')

Generate the hint file: php tools/gen-lib.php matplotlib.pyplot

Installation

<code>composer require swoole/phpy</code>

Using IDE Hints

<code>require dirname(__DIR__, 2) . '/vendor/autoload.php';
$plt = python\matplotlib\pyplot::import();
$x = new PyList([1, 2, 3, 4]);
$y = new PyList([30, 20, 50, 60]);
$plt->plot($x, $y);
$plt->show();</code>

Compilation Options

The first version hard‑coded the Python source directory; the new version allows specifying the directory and version via compile‑time flags.

--with-python-dir

Set the Python installation path, e.g., /usr/bin/python becomes --with-python-dir=/usr . For Conda installations, use --with-python-dir=/opt/anaconda3 .

--with-python-version

Specify the Python version (e.g., 3.10, 3.11, 3.12). By default, the build script runs $python-dir/bin/python -V to detect the version.

Dynamic Library Issues

Linker errors may arise from an incorrect LD path. Set the environment variable to point to the Python C‑module libraries:

<code>export LD_LIBRARY_PATH=/opt/anaconda3/lib
php plot.php</code>

This change only affects the current Bash session and avoids modifying global linker configuration.

Full Python Built‑In Support

Version 2 implements PyCore::__callStatic() to forward static method calls to Python's builtins module, enabling all Python built‑in functions, including eval and exec , to be executed from PHP.

<code>$pycode = <<<PYCODE
square = {
    f'{prefix}{i}': i**2 for i in range(n)
}
PYCODE;

$globals = new PyDict([
    'n' => 10,
    'prefix' => 'square_'
]);

PyCore::exec($pycode, $globals);
$this->assertEquals(64, $globals['square']['square_8']);
$this->assertEquals(16, $globals['square']['square_4']);</code>

Iterator Support

Iterators can now traverse Python objects, fully supporting the yield generator syntax.

<code>$iter = PyCore::iter($uname);
$this->assertTrue($iter instanceof PyIter);

$list = [];
while ($next = PyCore::next($iter)) {
    $list[] = PyCore::scalar($next);
}</code>

PHP Unit Tests

phpy includes comprehensive unit tests. After installation, run composer test or phpunit to verify the build.

Python Unit Tests

The project also uses pytest to test Python calls to the PHP API. Execute pytest -v tests/ to run the suite.

More Examples

Numpy Scientific Computing

<code>$np = PyCore::import('numpy');
$rs = $np->floor($np->random->random([3, 4])->__mul__(10));
PyCore::print($rs);</code>

matplotlib.pyplot Plotting

<code>$plt = PyCore::import('matplotlib.pyplot');
$x = new PyList([1, 2, 3, 4]);
$y = new PyList([30, 20, 50, 60]);
$plt->plot($x, $y);
$plt->show();</code>

For further reading, see the recommended articles on Python data visualization, movie ticket systems, scheduled tasks, and search‑ranking tools.

backendperformanceintegrationPHPextensionphpy
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.