aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--crates/shirabe-php-rpc/Cargo.toml1
-rw-r--r--crates/shirabe-php-rpc/php/worker.php12
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs125
-rw-r--r--crates/shirabe/src/platform/runtime.rs8
-rw-r--r--docs/dev/php-rpc.md33
6 files changed, 151 insertions, 29 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c5624c6..047ce92 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2055,6 +2055,7 @@ name = "shirabe-php-rpc"
version = "0.0.1"
dependencies = [
"shirabe-external-packages",
+ "shirabe-php-shim",
"tempfile",
]
diff --git a/crates/shirabe-php-rpc/Cargo.toml b/crates/shirabe-php-rpc/Cargo.toml
index 692dda9..115381c 100644
--- a/crates/shirabe-php-rpc/Cargo.toml
+++ b/crates/shirabe-php-rpc/Cargo.toml
@@ -5,6 +5,7 @@ edition.workspace = true
[dependencies]
shirabe-external-packages.workspace = true
+shirabe-php-shim.workspace = true
tempfile.workspace = true
[lints]
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php
index 214817a..58ba973 100644
--- a/crates/shirabe-php-rpc/php/worker.php
+++ b/crates/shirabe-php-rpc/php/worker.php
@@ -7,8 +7,8 @@ if ($client === false) {
exit(1);
}
$dispatch = [
- 'get_php_version' => static fn() => PHP_VERSION,
- 'get_php_binary' => static fn() => PHP_BINARY,
+ 'defined' => static fn($name) => defined($name),
+ 'constant' => static fn($name) => defined($name) ? constant($name) : null,
];
$read_exact = static function ($conn, int $len): ?string {
$buf = '';
@@ -31,7 +31,13 @@ while (true) {
if ($name === null) {
break;
}
- $result = isset($dispatch[$name]) ? ($dispatch[$name])() : null;
+ $sep = strpos($name, "\0");
+ $arg = null;
+ if ($sep !== false) {
+ $arg = substr($name, $sep + 1);
+ $name = substr($name, 0, $sep);
+ }
+ $result = isset($dispatch[$name]) ? ($dispatch[$name])($arg) : null;
$payload = serialize($result);
fwrite($client, pack('P', strlen($payload)) . $payload);
}
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index d55381b..04583fa 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -1,6 +1,7 @@
//! Rust-to-PHP RPC over a Unix domain socket. See `docs/dev/php-rpc.md`.
use shirabe_external_packages::symfony::process::PhpExecutableFinder;
+use shirabe_php_shim::PhpMixed;
use std::io::{Read as _, Write as _};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{LazyLock, Mutex};
@@ -8,12 +9,28 @@ use std::time::{Duration, Instant};
/// PHP `\PHP_VERSION`.
pub fn get_php_version() -> String {
- call("get_php_version").unwrap_or_default()
+ match get_constant("PHP_VERSION") {
+ PhpMixed::String(s) => s,
+ _ => String::new(),
+ }
}
/// PHP `\PHP_BINARY`.
pub fn get_php_binary() -> String {
- call("get_php_binary").unwrap_or_default()
+ match get_constant("PHP_BINARY") {
+ PhpMixed::String(s) => s,
+ _ => String::new(),
+ }
+}
+
+/// PHP `defined($name)`.
+pub fn has_constant(name: &str) -> bool {
+ matches!(call("defined", name), Some(PhpMixed::Bool(true)))
+}
+
+/// PHP `constant($name)`.
+pub fn get_constant(name: &str) -> PhpMixed {
+ call("constant", name).unwrap_or(PhpMixed::Null)
}
const GLUE_SCRIPT: &str = include_str!("../php/worker.php");
@@ -27,18 +44,21 @@ struct Worker {
}
impl Worker {
- fn request(&mut self, name: &str) -> Option<String> {
- write_frame(&mut self.stream, name.as_bytes()).ok()?;
- let payload = read_frame(&mut self.stream).ok()?;
- parse_serialized_string(&payload)
+ fn request(&mut self, name: &str, arg: &str) -> Option<Vec<u8>> {
+ let mut payload = name.as_bytes().to_vec();
+ payload.push(0);
+ payload.extend_from_slice(arg.as_bytes());
+ write_frame(&mut self.stream, &payload).ok()?;
+ read_frame(&mut self.stream).ok()
}
}
static WORKER: LazyLock<Mutex<Option<Worker>>> = LazyLock::new(|| Mutex::new(spawn_worker()));
-fn call(name: &str) -> Option<String> {
+fn call(name: &str, arg: &str) -> Option<PhpMixed> {
let mut guard = WORKER.lock().ok()?;
- guard.as_mut()?.request(name)
+ let payload = guard.as_mut()?.request(name, arg)?;
+ parse_serialized_scalar(&payload)
}
fn spawn_worker() -> Option<Worker> {
@@ -108,6 +128,33 @@ fn parse_serialized_string(payload: &[u8]) -> Option<String> {
Some(String::from_utf8_lossy(bytes).into_owned())
}
+/// Parse any of PHP's scalar/null `serialize()` forms: `N;`, `b:0/1;`, `i:<n>;`, `d:<f>;`,
+/// `s:<len>:"<bytes>";`.
+fn parse_serialized_scalar(payload: &[u8]) -> Option<PhpMixed> {
+ if payload == b"N;" {
+ return Some(PhpMixed::Null);
+ }
+ if let Some(rest) = payload.strip_prefix(b"b:") {
+ return match rest.strip_suffix(b";")? {
+ b"0" => Some(PhpMixed::Bool(false)),
+ b"1" => Some(PhpMixed::Bool(true)),
+ _ => None,
+ };
+ }
+ if let Some(rest) = payload.strip_prefix(b"i:") {
+ let s = std::str::from_utf8(rest.strip_suffix(b";")?).ok()?;
+ return s.parse().ok().map(PhpMixed::Int);
+ }
+ if let Some(rest) = payload.strip_prefix(b"d:") {
+ let s = std::str::from_utf8(rest.strip_suffix(b";")?).ok()?;
+ return s.parse().ok().map(PhpMixed::Float);
+ }
+ if payload.starts_with(b"s:") {
+ return parse_serialized_string(payload).map(PhpMixed::String);
+ }
+ None
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -151,6 +198,50 @@ mod tests {
}
#[test]
+ fn parses_scalar_null() {
+ assert_eq!(parse_serialized_scalar(b"N;"), Some(PhpMixed::Null));
+ }
+
+ #[test]
+ fn parses_scalar_bool() {
+ assert_eq!(
+ parse_serialized_scalar(b"b:0;"),
+ Some(PhpMixed::Bool(false))
+ );
+ assert_eq!(parse_serialized_scalar(b"b:1;"), Some(PhpMixed::Bool(true)));
+ }
+
+ #[test]
+ fn parses_scalar_int() {
+ assert_eq!(parse_serialized_scalar(b"i:8;"), Some(PhpMixed::Int(8)));
+ assert_eq!(parse_serialized_scalar(b"i:-1;"), Some(PhpMixed::Int(-1)));
+ }
+
+ #[test]
+ fn parses_scalar_float() {
+ assert_eq!(
+ parse_serialized_scalar(b"d:1.5;"),
+ Some(PhpMixed::Float(1.5))
+ );
+ }
+
+ #[test]
+ fn parses_scalar_string() {
+ assert_eq!(
+ parse_serialized_scalar(b"s:5:\"8.5.7\";"),
+ Some(PhpMixed::String("8.5.7".to_string())),
+ );
+ }
+
+ #[test]
+ fn rejects_malformed_scalar() {
+ assert_eq!(parse_serialized_scalar(b"b:2;"), None);
+ assert_eq!(parse_serialized_scalar(b"i:x;"), None);
+ assert_eq!(parse_serialized_scalar(b"d:x;"), None);
+ assert_eq!(parse_serialized_scalar(b"garbage"), None);
+ }
+
+ #[test]
fn frame_roundtrip() {
let (mut a, mut b) = UnixStream::pair().unwrap();
write_frame(&mut a, b"get_php_version").unwrap();
@@ -189,4 +280,22 @@ mod tests {
"binary should exist: {binary}",
);
}
+
+ #[test]
+ fn queries_constants_when_php_available() {
+ if PhpExecutableFinder::new().find(false).is_none() {
+ // No PHP in this environment; the worker cannot start.
+ return;
+ }
+
+ assert!(has_constant("PHP_VERSION"));
+ assert!(!has_constant("SHIRABE_DOES_NOT_EXIST_XYZ"));
+
+ assert_eq!(get_constant("PHP_INT_SIZE"), PhpMixed::Int(8));
+ assert_eq!(get_constant("SHIRABE_DOES_NOT_EXIST_XYZ"), PhpMixed::Null);
+ match get_constant("PHP_VERSION") {
+ PhpMixed::String(s) => assert!(!s.is_empty(), "expected a non-empty PHP_VERSION"),
+ other => panic!("expected a string, got {other:?}"),
+ }
+ }
}
diff --git a/crates/shirabe/src/platform/runtime.rs b/crates/shirabe/src/platform/runtime.rs
index 255aa9d..e4ff64a 100644
--- a/crates/shirabe/src/platform/runtime.rs
+++ b/crates/shirabe/src/platform/runtime.rs
@@ -3,8 +3,8 @@
use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::{CaptureKey, Preg};
use shirabe_php_shim::{
- PhpMixed, class_exists, constant, defined, function_exists, get_loaded_extensions,
- html_entity_decode, implode, instantiate_class, ltrim, phpversion, strip_tags, trim,
+ PhpMixed, class_exists, function_exists, get_loaded_extensions, html_entity_decode, implode,
+ instantiate_class, ltrim, phpversion, strip_tags, trim,
};
/// Seam over the PHP runtime so PlatformRepository can be tested against mocked
@@ -29,14 +29,14 @@ pub struct Runtime;
impl RuntimeInterface for Runtime {
fn has_constant(&self, constant_name: &str, class: Option<String>) -> bool {
- defined(&ltrim(
+ shirabe_php_rpc::has_constant(&ltrim(
&format!("{}::{}", class.as_deref().unwrap_or(""), constant_name),
Some(":"),
))
}
fn get_constant(&self, constant_name: &str, class: Option<String>) -> PhpMixed {
- constant(&ltrim(
+ shirabe_php_rpc::get_constant(&ltrim(
&format!("{}::{}", class.as_deref().unwrap_or(""), constant_name),
Some(":"),
))
diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md
index 9fc6ca3..ad5d3f1 100644
--- a/docs/dev/php-rpc.md
+++ b/docs/dev/php-rpc.md
@@ -10,10 +10,11 @@ system PHP as a child process and asks it for runtime information over a Unix do
The crate supports exactly one interaction pattern, and nothing else:
-> Rust calls a named, argument-less PHP function and receives a single fixed-type scalar back.
+> Rust calls a named PHP function, passing a single string argument, and receives a single scalar
+> back.
- Rust to PHP only. PHP never calls back into Rust.
-- No arguments.
+- Exactly one argument, and it must be a string.
- Scalar return values only (string / int / float / bool / null).
- Every failure is ignored: a PHP exception, serialization/deserialization
failure, a missing PHP function, a crashed child, etc. None are handled.
@@ -29,32 +30,36 @@ Reuse the existing `PhpExecutableFinder` class to resolve the PHP binary.
- 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 bare PHP function name as raw bytes.
- - Response payload: `serialize()` of the function's return value.
+ - 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 — any of `N;` (null), `b:0/1;`
+ (bool), `i:<n>;` (int), `d:<f>;` (float), or `s:<len>:"<bytes>";` (string).
-The PHP worker is a single read-eval-respond loop: read a framed function name,
-call the matching entry in a fixed dispatch table, send back `serialize($result)`.
+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)`.
## Global state and public API
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:
+The crate exposes plain free functions. For example:
-```rust
-shirabe_php_rpc::get_php_version() -> String
-```
+* get_php_version()
+* has_constant()
+* get_constant()
The connection is a process-global `static` (e.g. `OnceLock<Mutex<Worker>>`), lazily initialized on
-the first call: the first `get_php_version()` 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).
+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).
A future revision threads this runtime information through arguments or embeds it in structs; for now
callers just reach for the global getter.
## Out of scope
-Deferred things: arguments and non-scalar return values, PHP to Rust callbacks
+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.