Goro

PHP engine implemented in pure Go. Feature-complete for PHP 8.5 language features.
Why?
PHP is a nice language but is having trouble keeping up with modern languages. This implementation makes a number of things possible:
- Usage of goroutines, go channels, etc from within PHP
- Better caching of compiled code by allowing sharing of compiled or live objects (classes, objects, etc) between running PHP scripts
- Use Go's memory management within PHP
- Ability to run functions or code sandboxed (including filesystem via
fs.FS) to limit security risks
- Easily call the PHP engine from Go to execute pieces of PHP code (user provided or legacy)
Install
go install github.com/KarpelesLab/goro/sapi/php-cli@latest
Status
Goro passes ~11,864 of 12,121 tests (~97.9%) from the PHP 8.5.5 test suite (~170 failures, 87 skipped in CI). PHP memory_limit enforcement (128MB default). Includes PCRE2 via gopcre2, IANA timezones via gotz, and 10 extensions (session, xml, curl, gd, sockets, zlib, mysqli, sqlite3, bz2).
Remaining test failures by area
| Area |
Failures |
Notes |
| ext/date |
51 |
DatePeriod serialization format, date_parse edge cases, DST fallback transitions |
| attributes |
20 |
Reflection __toString formatting, delayed target validation, AST printing |
| exceptions |
9 |
__toString error location, variance autoload, stream wrappers |
| closures |
9 |
Closure const expressions, binding edge cases |
| clone |
6 |
AST printing, clone-with edge cases |
| exit |
5 |
exit() in custom SAPIs, disabling exit |
| constexpr |
5 |
Constant expression edge cases (new in defaults, array unpack) |
| asymmetric_visibility |
5 |
Static props, nested variations, indirect modification |
| assert |
5 |
assert() callback exceptions, ??= in assert, AST pretty-printer |
| ext/mbstring |
4 |
Encoding conversion edge cases |
| constants |
4 |
Constant evaluation edge cases |
| ext/hash |
3 |
PHP serialization format edge cases |
| ext/gmp |
3 |
GMP unserialize with references |
| Other |
~75 |
Scattered across ~40 areas (≤3 failures each): reference tracking, object ID ordering, warning ordering, etc. |
SAPIs
| SAPI |
Status |
CLI (php-cli) |
Working |
CGI (php-cgi) |
Working |
FPM (php-fpm) |
Working |
HTTP handler (php-httpd) |
Working |
Test runner (php-test) |
Working |
Extensions
| Extension |
Functions |
Pass Rate |
Notes |
| standard |
527+ |
~70% |
Core functions, arrays, strings, files, math, output buffering, streams |
| ctype |
11 |
100% |
Complete |
| json |
5 |
98% |
json_encode, json_decode, json_validate, error handling |
| pcre |
11 |
67% |
preg_match, preg_replace, preg_split, preg_grep — PCRE2 via gopcre2 (backreferences, lookahead) |
| hash |
14 |
94% |
hash, hash_hmac, hash_file, hash_pbkdf2, hash_hkdf, incremental |
| gmp |
49 |
96% |
Arithmetic, division, modular, bitwise, primes, GCD/LCM, factorial, operator overloading, import/export |
| mbstring |
49 |
97% |
strlen, substr, strpos, strtolower/upper, convert_encoding, detect_encoding, check_encoding |
| date |
48 |
89% |
date, time, strtotime, mktime, DateTime, DateTimeImmutable, DateInterval, DatePeriod, DateTimeZone, sunrise/sunset |
| openssl |
16 |
— |
AES/DES/RSA/ECDSA encryption, signing, key generation via Go crypto |
| bz2 |
2 |
— |
Compress (gobzip2) and decompress (Go stdlib) |
| zlib |
22 |
— |
gzcompress/gzencode/gzdeflate, gzip file ops, stream filters, compress.zlib:// |
| session |
19 |
— |
session_start/id/destroy, file-based storage, $_SESSION superglobal |
| xml |
18 |
— |
SimpleXMLElement class, xml_parser_create/parse, simplexml_load_string/file |
| curl |
13 |
— |
CurlHandle class, curl_init/setopt/exec/getinfo via Go net/http |
| sockets |
25 |
— |
Socket class, socket_create/bind/listen/accept/connect, stream_socket_*, fsockopen |
| mysqli |
25 |
— |
mysqli/mysqli_result/mysqli_stmt classes, prepared statements, transactions via go-sql-driver/mysql |
| sqlite3 |
20+ |
— |
SQLite3/SQLite3Result/SQLite3Stmt classes, prepared statements via glebarez/go-sqlite (pure Go) |
| spl |
40+ |
82% |
ArrayObject, ArrayIterator, SplFileObject, SplFixedArray, SplHeap, SplObjectStorage, iterators |
| reflection |
8 classes |
75% |
ReflectionClass (with __toString), ReflectionMethod, ReflectionProperty, ReflectionFunction, ReflectionParameter, ReflectionAttribute |
| gd |
60+ |
— |
GdImage class, imagecreate/truecolor, drawing (lines, rectangles, ellipses, polygons, arcs, fill), text (TTF, built-in fonts), PNG/JPEG/GIF/BMP I/O, copy/resize/resample/rotate/crop/flip/scale, filters, convolution — pure Go via gogd |
| getimagesize |
— |
100% |
16 image formats (JPEG, PNG, GIF, BMP, WebP, AVIF, HEIF, TIFF, PSD, etc.) |
Not yet implemented
| Extension |
Notes |
| PDO |
Planned via database/sql (MySQL + SQLite drivers already available) |
| iconv |
Planned via golang.org/x/text/transform |
| intl |
Internationalization (ICU) |
| Phar |
PHP archive format |
Bytecode VM
Goro compiles every script body and user function to a stack-based
bytecode and runs it on its own VM. The AST tree-walker remains as
infrastructure for nodes the emitter doesn't lower piecewise
(match, switch, anonymous class, dynamic-name access, etc.), but is
no longer reachable as a standalone runtime.
The VM emitter handles natively:
- Scalar literals;
true/false/null (case-insensitive); user
constants (PHP_INT_MAX, MYAPP_FOO, …) via AST resolution.
- Variable read/write with a per-frame slot cache. Bodies with no
extract/compact/$$x/global/static/$GLOBALS/by-ref
also skip the FuncContext hashtable mirror on writes.
- Arithmetic / bitwise / shift / comparison / concat / unary
(
-, ~, !); plain and compound assignment (=, +=, …);
pre/post ++/-- (including on object props, array elements,
static props — those AST-delegate).
- Short-circuit
&& / ||; null-coalesce ?? (any LHS shape);
ternary ?: / short ternary.
if/elseif/else, while, do/while, for, foreach
(value, by-ref, and non-local-target forms), match, switch,
multi-level break N / continue N, return (incl. typed-return
coercion via AST delegation), throw, try/catch/finally.
- String interpolation, array literals (incl. spread
...$arr),
array reads and writes (incl. nested chains, append, compound).
- Object instantiation (incl. anonymous classes, dynamic class name,
named/spread args), property and method access (incl. nullsafe
?->, dynamic-name, named/spread args), class const / static prop /
Foo::class, clone, instanceof.
- Inline closures with use captures,
$this binding, arrow auto-
capture, indirect calls ($f(), [$obj, 'method']()).
isset/empty/unset, list($a,$b) = … / [$a,$b] = …
destructuring, variable variables ($$x, ${$expr}).
- Generator iteration:
function() { yield … } itself runs through
the AST (cooperative coroutine), but foreach over a generator
drives it through the VM's iterator dispatch.
Known limitations (about 20 phpt failures unique to the VM, all
reference-semantics or error-handler edge cases):
- ArrayAccess overloaded containers (
$obj[$k] where $obj
implements ArrayAccess) with by-ref propagation.
- Deep ref-cycle reshape patterns (
bug72543*).
- A few error-handler/backtrace tests that depend on specific
call-frame metadata that the VM doesn't replicate exactly.
- One isset string-offset edge case (
bug31098).
These need either a ref-aware call protocol (pass Writable args
through the VM stack) or a runtime fall-back that re-evaluates args
from AST nodes when the resolved callee declares by-ref. Both are
non-trivial architectural changes outstanding for a future pass.
Benchmark numbers (per iter, on the same machine before / after
flipping the VM gate on permanently):
| Benchmark |
AST baseline |
VM |
Δ |
| Arithmetic |
58 ms |
15 ms |
-74% |
| ArrayOps |
17 ms |
6 ms |
-65% |
| Fibonacci |
33 ms |
16 ms |
-52% |
| FunctionCalls |
22 ms |
12 ms |
-45% |
| StringConcat |
21 ms |
12 ms |
-43% |
Larger gains require either an unboxed value type or
register-based opcodes. The 64-bit instruction format already has
room for the latter.
Importing the VM
Any binary or test that uses core/compiler must blank-import
core/vm/vmcompiler so the package's init() installs the
TryBuildVMScript / TryBuildVMClosureBody hooks. compiler.Compile
panics with a clear message if those hooks are missing. All built-in
SAPIs and the in-tree test files already have the import.
Architecture
Process
A process object is typically created once per runtime environment. It caches compiled code and holds global stream wrapper resources, persistent connections, and runtime cache.
Global
When a request is received or script execution is requested, a new Global context is created. It contains runtime state: global variables, declared functions, classes, constants, output buffers, and memory limits.
Context
Context is a local scope (e.g., within a running function). Global has a root context, and each function call creates a new context to separate variable scope.
Contributing
See development.md for details on writing extensions.
Writing an extension: create a directory in ext/, write functions with magic comment prefixes, run make buildext to generate bindings, and add the extension import to each SAPI's main.go.