From 2f28d8112970960dbb9b6b582a3c6cd259337d21 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 3 Aug 2026 01:05:57 +0900 Subject: feat(php-rpc): rework the RPC channel into the tagged plugin protocol Replace the name\0arg framing with the plugin wire protocol: tagged frames with corr_id multiplexing, a MAX_FRAME_LEN bound, a thread-ID based reentrant SessionLock, and the PluginValue codec (encoder plus the first recursive decoder, iterative with a 512-level depth cap). Float formatting is ported from php-src into shirabe-php-src so the encoder is byte-compatible with serialize() under serialize_precision=-1, which the spawned worker now pins. The worker gains a standing dispatch loop, CallRustMethod reentrancy, hand-written Event proxy stubs, and explicit-error answers for everything not implemented yet. The public query API (get_php_version and friends) is unchanged and now rides the new protocol; the codec is verified against real PHP by roundtrip oracle tests covering floats, non-UTF-8 bytes and deep nesting. Co-Authored-By: Claude Fable 5 --- crates/shirabe-php-src/src/lib.rs | 6 ++ crates/shirabe-php-src/src/main.rs | 1 + crates/shirabe-php-src/src/main/snprintf.rs | 83 +++++++++++++++++++++++ crates/shirabe-php-src/src/zend.rs | 1 + crates/shirabe-php-src/src/zend/zend_smart_str.rs | 73 ++++++++++++++++++++ 5 files changed, 164 insertions(+) create mode 100644 crates/shirabe-php-src/src/main.rs create mode 100644 crates/shirabe-php-src/src/main/snprintf.rs create mode 100644 crates/shirabe-php-src/src/zend.rs create mode 100644 crates/shirabe-php-src/src/zend/zend_smart_str.rs (limited to 'crates/shirabe-php-src/src') diff --git a/crates/shirabe-php-src/src/lib.rs b/crates/shirabe-php-src/src/lib.rs index 1e2c2a0f..a5682e24 100644 --- a/crates/shirabe-php-src/src/lib.rs +++ b/crates/shirabe-php-src/src/lib.rs @@ -1,5 +1,6 @@ //! Rust port from the original C implementation in php-src. //! See `LICENSE.md` at the repository root. +#![allow(special_module_name)] //! //! Rules for this crate: //! @@ -16,4 +17,9 @@ //! /// php-src: ext/standard/strnatcmp.c `strnatcmp_ex` (PHP 8.5.2) //! ``` +// The module mirrors php-src's `main/` directory; it is not a binary entry point (autobins is +// disabled in Cargo.toml). `special_module_name` is allowed crate-wide because the item-level +// attribute does not reach this early-pass lint. +pub mod main; pub mod standard; +pub mod zend; diff --git a/crates/shirabe-php-src/src/main.rs b/crates/shirabe-php-src/src/main.rs new file mode 100644 index 00000000..d2bbafb0 --- /dev/null +++ b/crates/shirabe-php-src/src/main.rs @@ -0,0 +1 @@ +pub mod snprintf; diff --git a/crates/shirabe-php-src/src/main/snprintf.rs b/crates/shirabe-php-src/src/main/snprintf.rs new file mode 100644 index 00000000..f1411d6e --- /dev/null +++ b/crates/shirabe-php-src/src/main/snprintf.rs @@ -0,0 +1,83 @@ +/// php-src: main/snprintf.c `php_gcvt` (PHP 8.5.8) +/// +/// Only the `ndigit < 0` path (dtoa mode 0, the shortest round-trip representation used when +/// `serialize_precision=-1`) is ported; the fixed-precision mode 2 path is not needed yet. +/// The digit extraction delegates to Rust's own shortest round-trip float formatting, which +/// produces the same digit string as `zend_dtoa` in mode 0 (both compute the unique shortest +/// decimal that round-trips), so only the digit placement logic is ported here. +pub fn php_gcvt(value: f64, ndigit: i32, dec_point: char, exponent: char) -> String { + let mode = if ndigit >= 0 { 2 } else { 0 }; + if mode != 0 { + todo!("php_gcvt is only ported for ndigit < 0 (serialize_precision=-1)"); + } + let ndigit = 17i32; + + let (sign, digits, decpt) = dtoa_shortest(value); + + let mut buf = String::new(); + if sign { + buf.push('-'); + } + + if if decpt < 0 { + decpt < -3 + } else { + decpt > ndigit + } { + // exponential format (e.g. 1.0E+17) + let exp = decpt - 1; + let mut chars = digits.chars(); + buf.push(chars.next().expect("dtoa always yields at least one digit")); + buf.push(dec_point); + let rest = chars.as_str(); + if rest.is_empty() { + buf.push('0'); + } else { + buf.push_str(rest); + } + buf.push(exponent); + if exp < 0 { + buf.push('-'); + } else { + buf.push('+'); + } + buf.push_str(&exp.abs().to_string()); + } else if decpt > 0 { + // standard format, integer part present + let decpt = decpt as usize; + if digits.len() <= decpt { + buf.push_str(&digits); + for _ in digits.len()..decpt { + buf.push('0'); + } + } else { + buf.push_str(&digits[..decpt]); + buf.push(dec_point); + buf.push_str(&digits[decpt..]); + } + } else { + // standard format, 0.000ddd + buf.push('0'); + buf.push(dec_point); + for _ in decpt..0 { + buf.push('0'); + } + buf.push_str(&digits); + } + + buf +} + +/// php-src: Zend/zend_strtod.c `zend_dtoa` mode 0 equivalent: the shortest round-trip digit +/// string of `|value|`, its sign, and the decimal point position (`value = 0.digits * 10^decpt`). +/// Implemented on top of Rust's `{:e}` formatting, which is also shortest-round-trip. +fn dtoa_shortest(value: f64) -> (bool, String, i32) { + let sign = value.is_sign_negative(); + let formatted = format!("{:e}", value.abs()); + let (mantissa, exp) = formatted + .split_once('e') + .expect("`{:e}` always contains an exponent"); + let digits: String = mantissa.chars().filter(|c| *c != '.').collect(); + let exp: i32 = exp.parse().expect("`{:e}` exponent is a decimal integer"); + (sign, digits, exp + 1) +} diff --git a/crates/shirabe-php-src/src/zend.rs b/crates/shirabe-php-src/src/zend.rs new file mode 100644 index 00000000..a11d8e55 --- /dev/null +++ b/crates/shirabe-php-src/src/zend.rs @@ -0,0 +1 @@ +pub mod zend_smart_str; diff --git a/crates/shirabe-php-src/src/zend/zend_smart_str.rs b/crates/shirabe-php-src/src/zend/zend_smart_str.rs new file mode 100644 index 00000000..b4a9f2ec --- /dev/null +++ b/crates/shirabe-php-src/src/zend/zend_smart_str.rs @@ -0,0 +1,73 @@ +use crate::main::snprintf::php_gcvt; + +/// php-src: Zend/zend_smart_str.c `smart_str_append_double` (PHP 8.5.8), folding in the `%H` +/// NAN/INF handling from main/snprintf.c `format_converter` that the original reaches through +/// `snprintf(buf, sizeof(buf), "%.*H", precision, num)`. +pub fn smart_str_append_double(dest: &mut String, num: f64, precision: i32, zero_fraction: bool) { + if num.is_nan() { + dest.push_str("NAN"); + return; + } + if num.is_infinite() { + dest.push_str(if num > 0.0 { "INF" } else { "-INF" }); + return; + } + let buf = php_gcvt(num, precision, '.', 'E'); + let had_period = buf.contains('.'); + dest.push_str(&buf); + if zero_fraction && !had_period { + dest.push_str(".0"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn serialize_repr(num: f64) -> String { + let mut out = String::new(); + smart_str_append_double(&mut out, num, -1, false); + out + } + + // Expected strings are the output of `serialize()` under PHP 8.5.8 with + // serialize_precision=-1 (without the `d:`/`;` wrapper). + #[test] + fn matches_php_serialize_output() { + assert_eq!(serialize_repr(0.0), "0"); + assert_eq!(serialize_repr(-0.0), "-0"); + assert_eq!(serialize_repr(1.5), "1.5"); + assert_eq!(serialize_repr(0.1), "0.1"); + assert_eq!(serialize_repr(2.0), "2"); + assert_eq!(serialize_repr(-2.0), "-2"); + assert_eq!(serialize_repr(100.0), "100"); + assert_eq!(serialize_repr(1e15), "1000000000000000"); + assert_eq!(serialize_repr(1e16), "10000000000000000"); + assert_eq!(serialize_repr(1e17), "1.0E+17"); + assert_eq!(serialize_repr(1e18), "1.0E+18"); + assert_eq!(serialize_repr(1e20), "1.0E+20"); + assert_eq!(serialize_repr(1.5e20), "1.5E+20"); + assert_eq!(serialize_repr(1e-4), "0.0001"); + assert_eq!(serialize_repr(1e-5), "1.0E-5"); + assert_eq!(serialize_repr(12345.6789e-9), "1.23456789E-5"); + assert_eq!(serialize_repr(1e-300), "1.0E-300"); + assert_eq!(serialize_repr(f64::MAX), "1.7976931348623157E+308"); + assert_eq!(serialize_repr(5e-324), "5.0E-324"); + assert_eq!(serialize_repr(1.0 / 3.0), "0.3333333333333333"); + assert_eq!(serialize_repr(0.30000000000000004), "0.30000000000000004"); + assert_eq!(serialize_repr(f64::NAN), "NAN"); + assert_eq!(serialize_repr(f64::INFINITY), "INF"); + assert_eq!(serialize_repr(f64::NEG_INFINITY), "-INF"); + } + + #[test] + fn zero_fraction_appends_dot_zero_to_integral_values() { + let mut out = String::new(); + smart_str_append_double(&mut out, 2.0, -1, true); + assert_eq!(out, "2.0"); + + let mut out = String::new(); + smart_str_append_double(&mut out, 1e17, -1, true); + assert_eq!(out, "1.0E+17"); + } +} -- cgit v1.3.1