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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.