This is a series of blog post where I document my transition from PHP, an interpreted language, to Rust, a system programming language, while exploring some paradigm differences between both as a long-standing PHP developer.
I decided to jump ship for two reasons: I needed to make something that PHP doesn’t support, and that was microservice-friendly. I don’t think that pulling out thousands of files from the Internet for each Kubernetes Pod can qualify as friendly.
In my case that translated into creating a WebSocket Server. I needed handling video calls between two peers using WebRTC, a protocol that connects both peers directly, while also transferring very important files to the other party through WebSockets to avoid reloading the browser and losing the connection.
PHP wasn’t up to the task. It doesn’t support WebSockets natively, and for WebRTC, you can hack your way in unreliable and resource hogging ways. There is also the problem of not having coroutines, meaning, everything blocks the request-response behaviour.
PHP is nice and all, but I had to dive in and create something from scratch.
That’s where Rust comes in.
Going deep. Going hard.
Rust is a system language. It’s very different to PHP, which is a interpreted language:
- A binary “reads” files with PHP code, transform the code in the file to «opcode» into memory, and executes it.
- Opcache and the next JIT engine in PHP 8 may speed up things.
- PHP is a web-bound language.
With Rust, on the other hand, you can also do this with some work, but also a graphic engine, a web server (like NGINX), or even a transcoder. There is no limit. And that’s one of the core principles with a system language like Rust: you are the pilot, you just have to know how to drive if you want to go somewhere, anywhere.
Rust for dummies
Most of your coding work that goes into Rust, like you would using C++ or any other similar language, it’s finally compiled into a single binary, which depending on what you do, may take some megabytes. That binary (or “executable”) is bound to the system architecture (x86–64, ARM, RISC-V, for example) and the operative system kernel (Windows, Linux, MacOS, Android, iOS).
You would think that it would require changing the whole code when changing “targets”, but instead Rust comes with a switch that allows you to point where you want to deploy your software, and it supports a whole lot. Unless you’re really doing something very low level, you won’t need to do anything but changing the destination.
The PHP stack on Rust
The “PHP” stack is mostly comprised of having a web-server, like NGINX or Apache, the PHP interpreter running behind of it with a network socket, and the PHP files to read.
Also, you need to get your permissions right, otherwise your application won’t work or your whole server will become vulnerable to attacks.
Everytime a request is handled by the web-server, it calls the interpreter that reads the PHP files and process the data, returning an output and closing the connection. Opcache and JIT can optimize this to not hit the filesystem everytime this process is done, and not to transform the «opcode» to bitcode every time, but is not something that a developer will have access in all hostings plans — you usually pay for the privilege.
Rust is totally feasible for a Web scenario, but you’re on your own.
While you “could” have NGINX or Apache on front of your application, there is no point to doing so if you consider you can create your own HTTP Server inside Rust with less options but more granular control over it, and pretty much close to an hypothetical Request Router.
Interpreting files is a moot point. Your code is already compiled and ready to execute whatever it comes to it, not as «opcode» but bitcode. In a nutshell, your app will be the only process running to handle whatever request comes to it, and you will have performance close to the metal.
A system language
Let’s start with the basic: PHP is an interpreted language. Everytime you write a PHP file, is expected to be picked up by the PHP Interpreter and transform it to machine code, at least once. These files are useless if the PHP interpreter can’t picked them up.
There is a lot of abstraction that is hidden behind PHP, like handling types of data in memory. In reality, the Zend Engine does a lot of heavy lifting so you can just focus on your single-threaded idea.
On a system language, you are in control on what goes where, meaning, dealing with memory management and some low-level logic. Since you are more “deep” into the metal, trivial things like reading a file content in PHP are not very straightforward because you have to account, for example, the memory buffer where the file contents will be pushed.
Also, system languages are usually do-or-die processes. If something goes wrong and is not handled correctly, like pointing to an invalid location in memory, or returning unexpected data, the whole program just freaking explodes. In PHP, only the single request will be cancelled while the rest keeps ongoing, and you will be able to log the problem.
This is what Rust wants to get rid of. By declaring everything in an (VERY) explicit manner, and simplifying the memory management, it offers all the power of close-to-metal performance without the un-safe-ness of other similar languages.
Hell, if you return a integer out of bounds it won’t even compile, saving you a headache on production.
If you compare that to an interpreted language, you have also some “helps” with the language itself. For example, in PHP you can use the
Reflection class to check the name of the function or method, the namespace of the class, or whatever. In system languages, this is not supported. The closest you will have to something like that will be meta-language, or “macros”, which are tidbits that take your code and “expand” on them adding confetti and what not for you.
And there is where I’m headed at.
Well, that’s very different. In the next article I will check how different is to work with Rust.