aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-src/src/main
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-03 01:05:57 +0900
committernsfisis <nsfisis@gmail.com>2026-08-03 01:05:57 +0900
commit2f28d8112970960dbb9b6b582a3c6cd259337d21 (patch)
treeaf8bc223b3af9098e6925cbf6c47e198459ac818 /crates/shirabe-php-src/src/main
parentbf2c6fa58ae51f44fa0ec65c615f8e62de87812c (diff)
downloadphp-shirabe-2f28d8112970960dbb9b6b582a3c6cd259337d21.tar.gz
php-shirabe-2f28d8112970960dbb9b6b582a3c6cd259337d21.tar.zst
php-shirabe-2f28d8112970960dbb9b6b582a3c6cd259337d21.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-src/src/main')
-rw-r--r--crates/shirabe-php-src/src/main/snprintf.rs83
1 files changed, 83 insertions, 0 deletions
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)
+}