Accรฉder au contenu principal

8min.

Writing the PHP Virtual Machine in Rust (with a lot of help from AI)

A few years ago, if someone had told me to rewrite the PHP engine in Rust, I would probably have laughed 😆. Not because it is technically impossible, but because such a project traditionally requires years of reading C code, reverse engineering decades of optimizations, and an intimidating amount of perseverance.

Today, Large Language Models have changed how we approach ambitious engineering projects. They do not magically produce production-ready software, but they dramatically reduce the cost of understanding unfamiliar codebases, exploring implementation strategies, and validating ideas.

Projects like Bun and the React Compiler show that revisiting mature ecosystems has become more realistic, not because AI replaces engineers, but because it multiplies their capacity.

This is the story of building a new PHP Virtual Machine in Rust, what worked, what failed, and why the goal was never to create yet another PHP interpreter.

Section intitulée the-first-prototype-surprisingly-easy-and-terribly-slowThe first prototype: surprisingly easy and terribly slow

Like many AI-assisted projects, the first version began with an intentionally naive approach. Without studying the PHP source code in depth, we asked the model to generate a VM from its own knowledge, then guided it with examples and corrections whenever something failed.

Before long, we had a functional prototype capable of executing a meaningful subset of PHP, but it was incredibly slow. That was hardly surprising: the generated implementation favored correctness over performance.

Furthermore it generated a stack vm where PHP is a registry vm

StackVMvsRegisterVm

I believe that this difference would have been very painful at some point. Those kinds of differences can make some behavior don’t work the same way, like destruction orders of values, or timing of errors emission. Given the current PHP ecosystem, I’m pretty sure that some existing libraries rely on those orders. Since the goal is to mimic a real migration of PHP, I believe that we should respect that.

The prototype (done in one day) only proved that the idea was viable, but it was clearly not the final architecture.

Section intitulée copying-php-exactly-was-not-the-goalCopying PHP exactly was not the goal

At that point, one option was to follow the path Bun took with its rewrite and reproduce the original implementation as closely as possible. This has obvious advantages: PHP’s engine contains more than twenty years of knowledge and optimization from hundreds of contributors, and reproducing its architecture often means reproducing its performance.

Such rewrite generally breaks the target language way of doing things, look at the Bun rewrite pull request. It is full of unsafe, which, in my opinion, completely defeats the purpose of using Rust and makes this PR a complete nonsense.

The purpose of the project was not simply to rewrite Zend Engine in another language, but to understand how PHP works and explore what its VM might look like if designed from the beginning around Rust’s philosophy.

That meant embracing ownership, avoiding global mutable state, minimizing unsafe code, and taking advantage of the language rather than constantly fighting it. Otherwise, there would be little point in choosing Rust.

Section intitulée learning-from-php-instead-of-copying-itLearning from PHP instead of copying it

Not copying PHP does not mean ignoring it. A significant part of the project involved reading Zend Engine code, asking AI why particular mechanisms existed, tracing historical decisions, and discussing alternative implementations.

I learned a lot on how PHP works, why it works that way, and how to implement similar behavior in Rust.

Those conversations often revealed that an optimization reflected older CPU behavior, that strange-looking code avoided an allocation on a critical path, or that Rust offered a more natural implementation. Just as often, they showed that PHP had already found the best solution years ago.

The objective gradually shifted from rewriting PHP to understanding why it became what it is today.

Section intitulée building-the-vm-i-always-wantedBuilding the VM I always wanted

Studying the engine also gave me an opportunity to experiment with ideas I had wanted to see in PHP for years.

Section intitulée respecting-ownership-and-avoiding-global-stateRespecting ownership and avoiding global state

One of the first constraints was to follow the Rust way of doing things: avoid global mutable state. Instead, the VM is designed to be a self-contained object that can be instantiated multiple times, each with its own state.

This makes the architecture easier to reason about while allowing multiple independent VMs to run within the same thread. It also simplifies testing and embedding, and fits much more naturally with Rust’s programming model. A global mutable state in Rust is problematic since it either forces a RefCell around it to make it safe (slower) or does unsafe code (a nogo).

Section intitulée a-fork-model-for-requestsA fork model for requests

Instead of rebuilding a fresh runtime for every request, the VM can be paused and cloned using copy-on-write semantics. The clone executes the request and is then discarded, while the untouched original is immediately ready to serve as the base for the next one.

Fork Mode

In the code it works like this:

<?php

require 'vendor/autoload.php';

// initialize a preloaded state
$kernel = new Symfony\Kernel('prod', false);
$kernel->boot();

// cache some data
$data = do_some_expensive_work();

// pause and wait for a request
rphp_request_pause();

// execution will start from here for every request, with the preloaded state and cached data available
$request = Request::createFromGlobals();
$kernel->handle($request);

$newData = something();

// newData / request / static cache created after the pause will be dropped at the end of the request, but $data will remain available for the next one

This mode sits between the classic PHP model, where each request starts from scratch, and a long-running process, like FrankenPHP worker mode, where state is preserved across requests. It allows the VM to avoid the repeated reconstruction of application state while still preserving the isolation between requests.

Section intitulée ai-as-a-discussion-partnerAI as a discussion partner

The most unexpected part of the project was not code generation, but the depth of the technical discussions. We could explore why PHP laid out an object in a particular way, whether an optimization existed because of CPU caches, or whether Rust’s type system could eliminate a runtime check.

I also learned a lot about how a compiler works, how to design a VM, and the inside of the Zend Engine.

The AI was rarely right on its first attempt, but it was almost always useful. It became less a code generator than a technical sparring partner, able to summarize thousands of lines of C, suggest alternative designs, and help turn questions into experiments.

But it still writes sloppy code, even after passing a lot of times trying to fix some structures, and telling the AI to follow specific rules. The current code is far from perfect.

In the end, the project was not written by AI; it was accelerated by it. That may be the most important change these tools bring to ambitious software projects.

For people that are afraid that AI will code everything for them, I can tell you that it is not the case. If you do not understand the code and the underlying concepts, you will fail miserably. AI is a tool, a partner, not a replacement for knowledge and experience. But it is also dangerous, the number of times I believe it has done something amazing, only to find out that it was wrong, is countless. It is a tool that can help you, but it can also mislead you. You need to be careful and always verify the results.

Section intitulée why-rust-why-not-insert-your-own-language-hereWhy Rust? Why not [insert your own language here]?

This experimentation could have been done in C (and just make a fork on PHP), or we could have used Zig, or even PHP.

There is mainly 2 reasons that I have chosen this :

  • I know the language well, meaning I don’t need to think about language when I read code, I only think about logic which saves a lot of time reviewing it.
  • I do think that Rust is actually the best language to work with AI, simply because it has so many constraints: which can be painful as a developer, but makes it safer when it’s AI writing it. AI is not perfect, and I don’t think it will ever be one day, simply because perfection is a moving state, and it is subjective. Also making errors is part of the process of learning. So in my point of view AI needs deterministic tools to be good, and the more deterministic constraints the more it’s powerful, since AI can have automatic feedback on those constraints and can adapt what it does. It also adds a lot of confidence on what is built for the end user.

Section intitulée where-the-project-stands-todayWhere the project stands today

You can see the project on GitHub at https://github.com/jolicode/rphp.

Although there is still a long way to go, roughly 80% of the basic compatibility tests now pass. Many features and edge cases remain, but the VM can already execute a significant amount of real-world PHP code.

For pure PHP code without external or native function calls, it is currently between 5 and 15 times slower than PHP. This is unsurprising given Zend Engine’s two decades of profiling and optimization, but raw interpreter speed is only part of the picture.

However, fork mode is around 30 times faster than the VM’s classic execution mode on the same application. This is the most encouraging result so far because it suggests that interpreter performance is not the only available lever. On the Symfony Demo application, avoiding the repeated reconstruction of application state is enough to make the VM faster than PHP / FrankendPHP in classic mode despite its slower interpreter.

If we can progressively close the gap in pure code execution while preserving the fork model’s architectural advantages, the result could be a genuinely fast engine for real-world applications.

It was written in about one month, with daily adjustment / reading / refocus on what was important. There was also a lot of autonomous work, where the AI was iterating over tests fixing, closing the gap between the current implementation and the expected / existing behavior. For this occasion I used a Max 20x Claude plan (about 200€, only take it for a month) and mostly used Opus 4.8 / Sonnet 4.6 and even Fable when it was available at some points. We could feel a difference between those 3 models, but I could feel, given how we constantly adjust it, that any of those 3 models would have rendered the same results.

Section intitulée what-s-nextWhat’s next?

I’m not sure what to do next. The project has already been a success in terms of learning and experimentation, but it is still far from being a production-ready PHP engine. I may continue to improve the VM, or I may start a new project with the knowledge gained from this one.

Some of those ideas may turn out to be mistakes, while others may prove surprisingly effective. Either way, the journey has already been worth it.

Commentaires et discussions

Ces clients ont profité de notre expertise