aboutsummaryrefslogtreecommitdiffhomepage
path: root/docs/dev/php-rpc.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/dev/php-rpc.md')
-rw-r--r--docs/dev/php-rpc.md160
1 files changed, 113 insertions, 47 deletions
diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md
index 6e0bc40c..b24f72d9 100644
--- a/docs/dev/php-rpc.md
+++ b/docs/dev/php-rpc.md
@@ -1,66 +1,132 @@
# PHP RPC
-Composer can require a specific PHP version or loaded extensions, i.e., platform requirements.
-To mimic this behavior needs a real PHP runtime.
+Composer can require a specific PHP version or loaded extensions, i.e., platform requirements,
+and its plugin/script machinery executes real PHP code. To mimic this behavior needs a real PHP
+runtime.
-This document describes a first design of PHP runtime: a `shirabe-php-rpc` crate that spawns the
-system PHP as a child process and asks it for runtime information over a Unix domain socket.
+The `shirabe-php-rpc` crate spawns the system PHP as a child process and talks to it over a Unix
+domain socket. There is exactly one child process per Shirabe process, shared by every caller;
+it hosts both the simple runtime queries (`get_php_version`, `has_constant`, ...) and the plugin
+protocol.
-## Scope
+## Locating PHP
-The crate supports exactly one interaction pattern, and nothing else:
+The existing `PhpExecutableFinder` class resolves the PHP binary. The child is started with
+`-d serialize_precision=-1` so the wire codec's float formatting is pinned to the default PHP
+behavior.
-> Rust calls a named PHP function, passing a single string argument, and receives a single value
-> back.
+## Transport
-- Rust to PHP only. PHP never calls back into Rust.
-- Exactly one argument, and it must be a string.
-- Return values are scalars (string / int / float / bool / null) or arrays of them.
-- Every failure `panic!`s: a PHP exception (not currently possible — the glue script never lets
- one escape), a serialization/deserialization failure, a missing PHP function, a crashed child,
- etc. None are handled as recoverable errors. This is not the final design — a future revision
- will replace these panics with proper `Result` propagation — but panicking beats silently
- returning a plausible-looking default.
-- Windows is unsupported and `panic!`s for now.
+- A Unix domain socket (no Windows support for now), bound in a `0700` temp dir with the socket
+ file itself chmodded to `0600`.
+- The PHP glue code (`php/worker.php`) and the proxy stub classes (`php/stubs/`) are embedded in
+ the Rust binary and written to the temp dir at spawn time, so both halves of the protocol are
+ always the same commit.
-## Locating PHP
+### Frame layout
-Reuse the existing `PhpExecutableFinder` class to resolve the PHP binary.
+```
+[u64 length LE] -- number of bytes that follow (1 + 8 + payload length)
+[u8 tag] -- message tag, see below
+[u64 corr_id LE] -- correlation id; 0 for one-way notifications
+[payload] -- the remaining fields as one PHP-serialize()d list
+```
-## Transport
+`length` is validated against `MAX_FRAME_LEN` (256 MiB) before any allocation; an oversized
+frame is a fatal channel error, not an allocation attempt.
+
+### Message tags
+
+| tag | name | direction | payload fields |
+|---|---|---|---|
+| `0x00` | `CallFunction` | Rust→PHP | `function_name`, `args`, `out_param_positions` |
+| `0x01` | `CallStaticMethod` | Rust→PHP | `pclass`, `method_name`, `args`, `out_param_positions` |
+| `0x02` | `NewObject` | Rust→PHP | `pclass`, `ctor_args` |
+| `0x03` | `CallPhpMethod` | Rust→PHP | `phandle`, `method_name`, `args`, `out_param_positions` |
+| `0x04` | `CallRustMethod` | PHP→Rust | `rhandle`, `method_name`, `args`, `out_param_positions` |
+| `0x05` | `Return` | both | `value`, `out_params` |
+| `0x06` | `Throw` | both | `exception_class`, `message`, `code` |
+| `0x07` | `ReleaseRustHandle` | PHP→Rust | `rhandle` |
+| `0x08` | `ReleasePhpHandle` | Rust→PHP | `phandle` |
+| `0x09` | `EpochBump` | Rust→PHP | `rhandle`, `epoch` |
+
+The Rust side allocates odd correlation ids, the PHP side even ones. `NewObject` and
+`CallPhpMethod` are protocol receptacles: the worker currently answers them with an explicit
+`Throw` (the P table is not implemented yet).
+
+### Values: `PluginValue` and the codec
+
+Payloads are encoded with a Rust reimplementation of the PHP `serialize()` grammar
+(`src/value.rs`), byte-compatible with the PHP core implementation under
+`serialize_precision=-1` (the float formatting itself is ported in `shirabe-php-src`). The value
+model is `PluginValue`: PHP scalars, byte strings (`Vec<u8>` — non-UTF-8 round-trips
+losslessly), lists, ordered maps, and three handle descriptor kinds encoded as reserved arrays:
+
+- `{__rhandle, __class, __epoch[, __snapshot]}` — entity lives on the Rust side
+- `{__phandle, __class, __implements}` — entity lives in the PHP child
+- `{__pclass}` — a PHP class name
+
+`PluginValue::Object` is encode-only: the wire erases the array/object distinction and object
+revival is banned (`unserialize(..., ['allowed_classes' => false])` is enforced on the PHP
+side), so the decoder only produces `List` (contiguous 0-based int keys) or `Array`. The
+decoder is iterative (input nesting never becomes call-stack depth) and additionally rejects
+payloads nested deeper than 512 levels.
+
+The codec is verified against the real PHP `serialize()`/`unserialize()` by oracle tests
+(`tests/oracle.rs`), with floats, non-UTF-8 byte strings and deep nesting as focus areas.
+
+## Concurrency and reentrancy
+
+A logical call session is serialized by a thread-ID based reentrant session lock
+(`src/session.rs`): the owning thread may nest calls freely (a `CallRustMethod` handler can
+itself call back into PHP), while other OS threads block until the whole outer session
+completes. This keeps the single-child-process invariant under `cargo test`'s parallel harness.
+The worker mutex itself is only held per frame, not across a call.
+
+While a Rust-initiated call waits for its `Return`, incoming `CallRustMethod` frames are
+dispatched to the caller-supplied `RustMethodDispatcher` (the cooperative loop); with no
+dispatcher active they are answered with an explicit `Throw`, never a silent null. Rust handle
+0 is reserved for the runtime service endpoint (e.g. `__shirabe_find_file`, which the worker's
+script-class autoloader uses to ask the Rust-side `ClassLoader` where a class file lives).
+
+The PHP side mirrors this: its top level is a standing serve loop, and `callRust` drives the
+same dispatch while waiting for its own `Return`.
-- A Unix domain socket. (No Windows support for now)
-- The PHP glue code is a small script written to a temporary file.
-- Message frame: `[usize length (little-endian)][payload]`.
- - Request payload: the PHP function name as raw bytes, followed by a `\0` byte and the string
- argument (function names are static literals and never contain `\0`, so the first `\0`
- unambiguously separates name from argument).
- - Response payload: `serialize()` of the function's return value.
+## Failure model
-The PHP worker is a single read-eval-respond loop: read a framed function name and argument, call
-the matching entry in a fixed dispatch table (`defined`, `constant`), send back
-`serialize($result)`.
+- The outer `anyhow::Result` of `call_function`/`call_static_method` is the fatal lane: dead
+ worker (EOF, with the child's exit status attached as context), broken framing, oversized
+ frames.
+- The inner `Result<PluginValue, PhpThrow>` is the recoverable lane: a PHP exception crossing
+ the boundary as a `Throw` frame.
+- A frame that decodes to something protocol-invalid is a bug in Shirabe itself (both halves
+ ship in the same commit) and panics; the PHP side symmetrically dies so Rust observes EOF.
+- The legacy scalar query API (`get_php_version` etc.) keeps its historical contract: every
+ failure panics.
-## Global state and public API
+## Worker dispatch table
-PHP runtime information (e.g., process handle) is held as process-global state
-rather than threaded through call sites for now.
-The crate exposes plain free functions. For example:
+`CallFunction` first consults the worker's dispatch table (composite queries like `diagnose`,
+Shirabe-internal helpers prefixed `__shirabe_`), then falls back to calling the named PHP
+function; an unknown name is an explicit error. Notable internal helpers:
-* get_php_version()
-* has_constant()
-* get_constant()
+- `__shirabe_eval` — runs a Rust-generated PHP snippet and returns its `return` value (used by
+ the `scripts` Command-class execution path).
+- `__shirabe_require` — `require_once` a file (e.g. an autoloader) into the worker.
+- `__shirabe_enable_script_autoloader` — registers the autoloader that resolves classes through
+ the Rust-side `ClassLoader` via handle 0.
+- `__shirabe_oracle_roundtrip` — codec oracle support for tests.
-The connection is a process-global `static` (e.g. `OnceLock<Mutex<Worker>>`), lazily initialized on
-the first call: the first call spawns the child, performs the handshake, and caches the connection.
-Commands that never query PHP never start it. The child lives for the rest of the process and is
-left to be reaped at exit (no explicit shutdown message).
+## Proxy stubs
-A future revision threads this runtime information through arguments or embeds it in structs; for now
-callers just reach for the global getter.
+`php/stubs/` holds hand-written proxy stub classes (currently `Composer\EventDispatcher\Event`
+and `Composer\Script\Event`), written in the shape the future stub generator will output. They
+are autoloaded with highest priority so a proxied FQCN can never be shadowed by the real
+implementation. Stubs are interned per rhandle (`WeakReference`-based registry) so identity
+(`===`) holds, and their destructors send `ReleaseRustHandle`.
-## Out of scope
+## Out of scope (deferred)
-Deferred things: multiple/non-string arguments, non-scalar return values, PHP to Rust callbacks
-and re-entrancy, object handles / proxies / identity, stub generation, error
-propagation, GC / lifecycle, and Windows support.
+The P table (PHP-owned objects crossing to Rust), `NewObject`/`CallPhpMethod` execution,
+out-parameter write-back at the call sites, epoch-based cache invalidation on the PHP side,
+error-class reconstruction across the boundary, and Windows support.