aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc/src/lib.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-07-02 05:12:39 +0900
committernsfisis <nsfisis@gmail.com>2026-07-02 05:12:39 +0900
commit7fd7df848cdbbd8792b0043799018d51408458fc (patch)
tree28aeeef87c3d86a662ed1328b53b1a2865df2c6d /crates/shirabe-php-rpc/src/lib.rs
parenteab3a31c5750013c53c0eb02adc976d6757dc9f7 (diff)
downloadphp-shirabe-7fd7df848cdbbd8792b0043799018d51408458fc.tar.gz
php-shirabe-7fd7df848cdbbd8792b0043799018d51408458fc.tar.zst
php-shirabe-7fd7df848cdbbd8792b0043799018d51408458fc.zip
feat(php-rpc): implement Runtime::has_constant/get_constant via php-rpc
Runtime::hasConstant/getConstant need a real PHP interpreter's defined()/ constant() to answer platform requirement checks (e.g. PHP_ZTS, PHP_INT_SIZE), which the shim can't provide since Rust constants aren't queryable by string. Extend shirabe-php-rpc's protocol to carry one string argument and return the full PHP scalar range, add defined/constant dispatch entries to the worker, and wire Runtime and get_php_version/get_php_binary onto them.
Diffstat (limited to 'crates/shirabe-php-rpc/src/lib.rs')
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs125
1 files changed, 117 insertions, 8 deletions
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:?}"),
+ }
+ }
}