aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-21 20:54:13 +0900
committernsfisis <nsfisis@gmail.com>2026-06-21 21:05:13 +0900
commit80c2870c5c38ba9b93d5f0ee4e594d72e5628723 (patch)
tree6e3f86dd9b95a8b66696cde80a2f198b632a1602 /crates/shirabe-php-shim
parent0ab867c1b880f6b59bba6899127a0b81be6c79ca (diff)
downloadphp-shirabe-80c2870c5c38ba9b93d5f0ee4e594d72e5628723.tar.gz
php-shirabe-80c2870c5c38ba9b93d5f0ee4e594d72e5628723.tar.zst
php-shirabe-80c2870c5c38ba9b93d5f0ee4e594d72e5628723.zip
feat(php-shim): implement version_compare/spl_object_hash/clone et al
Port php_version_compare (canonicalization + special-form ordering) and expose it via version_compare()/version_compare_2(). Implement clone() (via Clone), spl_object_hash()/spl_object_hash_process() (address-based), get_loaded_extensions() and the main-version case of phpversion(). Interpreter/reflection/autoload/eval/$GLOBALS/memory-accounting and Windows-only SAPI helpers remain TODO(phase-d): they have no equivalent in the compiled shim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim')
-rw-r--r--crates/shirabe-php-shim/src/runtime.rs200
1 files changed, 193 insertions, 7 deletions
diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs
index 2f98891..bf4d355 100644
--- a/crates/shirabe-php-shim/src/runtime.rs
+++ b/crates/shirabe-php-shim/src/runtime.rs
@@ -43,6 +43,8 @@ pub const PHP_OS: &str = match std::env::consts::OS.as_bytes() {
};
pub fn constant(_name: &str) -> PhpMixed {
+ // TODO(phase-d): resolving a constant by name needs a runtime constant registry, which the shim
+ // does not provide (constants are ported as Rust `const`s, not looked up by string).
todo!()
}
@@ -70,6 +72,7 @@ pub fn defined(name: &str) -> bool {
}
pub fn method_exists(_object: &PhpMixed, _method_name: &str) -> bool {
+ // TODO(phase-d): requires runtime class/method reflection, which PhpMixed::Object does not carry.
todo!()
}
@@ -168,23 +171,40 @@ pub fn ini_get(option: &str) -> Option<String> {
}
pub fn get_loaded_extensions() -> Vec<String> {
- todo!()
+ // Mirrors the set recognized by extension_loaded().
+ // TODO(phase-d): this models only the Composer-relevant subset, not PHP's full extension list
+ // (Core, standard, date, pcre, ...).
+ [
+ "Phar", "curl", "filter", "hash", "iconv", "intl", "mbstring", "openssl", "zip", "zlib",
+ ]
+ .iter()
+ .map(|s| s.to_string())
+ .collect()
}
pub fn phpversion(_extension: &str) -> Option<String> {
- todo!()
+ if _extension.is_empty() {
+ Some(PHP_VERSION.to_string())
+ } else {
+ // TODO(phase-d): per-extension version strings are not modeled; PHP returns the extension's
+ // own version, or false when the extension is not loaded.
+ todo!()
+ }
}
// TODO(php-runtime): the callback should be registered in PHP runtime.
pub fn set_error_handler(_callback: fn(i64, &str, &str, i64) -> bool) {}
pub fn debug_backtrace() -> Vec<IndexMap<String, PhpMixed>> {
+ // TODO(phase-d): capturing a PHP-style call stack requires runtime introspection of the
+ // interpreter frames, which has no equivalent in the compiled shim.
todo!()
}
/// Equivalent to PHP `include $file;`
pub fn include_file(file: &str) -> PhpMixed {
let _ = file;
+ // TODO(phase-d): `include` evaluates a PHP source file at runtime; there is no PHP interpreter.
todo!()
}
@@ -194,11 +214,14 @@ pub fn spl_autoload_register(
prepend: bool,
) -> bool {
let _ = (callback, throw, prepend);
+ // TODO(phase-d): class autoloading has no analogue in compiled Rust (classes are not loaded by
+ // name at runtime).
todo!()
}
pub fn spl_autoload_unregister(callback: Box<dyn Fn(&str) -> PhpMixed + Send + Sync>) -> bool {
let _ = callback;
+ // TODO(phase-d): see spl_autoload_register.
todo!()
}
@@ -214,15 +237,136 @@ pub fn error_reporting(level: Option<i64>) -> i64 {
}
pub fn spl_autoload_functions() -> Vec<PhpMixed> {
+ // TODO(phase-d): see spl_autoload_register; no autoload registry exists.
todo!()
}
pub fn version_compare(_v1: &str, _v2: &str, _op: &str) -> bool {
- todo!()
+ let c = php_version_compare(_v1, _v2);
+ match _op {
+ "<" | "lt" => c < 0,
+ "<=" | "le" => c <= 0,
+ ">" | "gt" => c > 0,
+ ">=" | "ge" => c >= 0,
+ "==" | "=" | "eq" => c == 0,
+ "!=" | "<>" | "ne" => c != 0,
+ // TODO(phase-d): PHP returns null for an unknown operator; this bool signature reports false.
+ _ => false,
+ }
}
pub fn version_compare_2(_v1: &str, _v2: &str) -> i64 {
- todo!()
+ php_version_compare(_v1, _v2) as i64
+}
+
+// Port of PHP's php_version_compare (ext/standard/versioning.c). Returns -1, 0 or 1.
+fn php_version_compare(v1: &str, v2: &str) -> i32 {
+ if v1.is_empty() || v2.is_empty() {
+ return match (v1.is_empty(), v2.is_empty()) {
+ (true, true) => 0,
+ (false, _) => 1,
+ (_, false) => -1,
+ };
+ }
+ let c1 = canonicalize_version(v1);
+ let c2 = canonicalize_version(v2);
+ let t1: Vec<&str> = c1.split('.').filter(|s| !s.is_empty()).collect();
+ let t2: Vec<&str> = c2.split('.').filter(|s| !s.is_empty()).collect();
+
+ let mut compare = 0;
+ let mut i = 0;
+ while i < t1.len() && i < t2.len() && compare == 0 {
+ compare = version_token_compare(t1[i], t2[i]);
+ i += 1;
+ }
+ if compare == 0 {
+ // A leftover numeric token wins; a leftover special form is compared against the implicit
+ // release baseline ("#", order 4).
+ if i < t1.len() {
+ let p = t1[i];
+ compare = if p.as_bytes()[0].is_ascii_digit() {
+ 1
+ } else {
+ special_form_order(p).cmp(&4) as i32
+ };
+ } else if i < t2.len() {
+ let p = t2[i];
+ compare = if p.as_bytes()[0].is_ascii_digit() {
+ -1
+ } else {
+ 4.cmp(&special_form_order(p)) as i32
+ };
+ }
+ }
+ compare
+}
+
+// PHP's php_canonicalize_version: separators (-, _, +, .) collapse to a single '.', and a '.' is
+// inserted at every digit <-> non-digit boundary.
+fn canonicalize_version(version: &str) -> String {
+ let bytes = version.as_bytes();
+ if bytes.is_empty() {
+ return String::new();
+ }
+ let mut q: Vec<u8> = Vec::with_capacity(bytes.len() * 2);
+ q.push(bytes[0]);
+ for &raw in &bytes[1..] {
+ let ch = if matches!(raw, b'-' | b'_' | b'+') {
+ b'.'
+ } else {
+ raw
+ };
+ let last = *q.last().unwrap();
+ if ch == b'.' {
+ if last != b'.' {
+ q.push(b'.');
+ }
+ } else if last.is_ascii_digit() != ch.is_ascii_digit() {
+ q.push(b'.');
+ q.push(ch);
+ } else {
+ q.push(ch);
+ }
+ }
+ String::from_utf8_lossy(&q).into_owned()
+}
+
+fn version_token_compare(t1: &str, t2: &str) -> i32 {
+ let d1 = t1.as_bytes()[0].is_ascii_digit();
+ let d2 = t2.as_bytes()[0].is_ascii_digit();
+ if d1 && d2 {
+ let l1 = t1.parse::<i64>().unwrap_or(0);
+ let l2 = t2.parse::<i64>().unwrap_or(0);
+ l1.cmp(&l2) as i32
+ } else if !d1 && !d2 {
+ special_form_order(t1).cmp(&special_form_order(t2)) as i32
+ } else if d1 {
+ // A numeric token is treated as the "#" form (order 4).
+ 4.cmp(&special_form_order(t2)) as i32
+ } else {
+ special_form_order(t1).cmp(&4) as i32
+ }
+}
+
+fn special_form_order(form: &str) -> i32 {
+ const FORMS: &[(&str, i32)] = &[
+ ("dev", 0),
+ ("alpha", 1),
+ ("a", 1),
+ ("beta", 2),
+ ("b", 2),
+ ("RC", 3),
+ ("rc", 3),
+ ("#", 4),
+ ("pl", 5),
+ ("p", 5),
+ ];
+ for (name, order) in FORMS {
+ if form.starts_with(name) {
+ return *order;
+ }
+ }
+ -1
}
// TODO(php-runtime): the previous handler should be restored in the PHP runtime.
@@ -230,11 +374,15 @@ pub fn version_compare_2(_v1: &str, _v2: &str) -> i64 {
pub fn restore_error_handler() {}
pub fn spl_object_hash<T: ?Sized>(_object: &T) -> String {
- todo!()
+ // PHP returns a unique 32-char hex id per object instance; the object's address serves as the
+ // identity here.
+ // TODO(phase-d): as in PHP, an address can be reused after an object is freed, so uniqueness is
+ // not guaranteed across an object's whole lifetime without an object store.
+ format!("{:032x}", _object as *const T as *const u8 as usize)
}
pub fn spl_object_hash_process<T>(_object: &T) -> String {
- todo!()
+ format!("{:032x}", _object as *const T as usize)
}
pub fn php_uname(mode: &str) -> String {
@@ -259,15 +407,21 @@ pub fn php_uname(mode: &str) -> String {
"r" => std::fs::read_to_string("/proc/sys/kernel/osrelease")
.map(|s| s.trim_end().to_string())
.unwrap_or_default(),
+ // TODO(phase-d): the remaining php_uname() modes ("n", "v", "m", "a") need uname(2) fields
+ // (nodename/version/machine) that are not reachable without a libc/syscall crate.
_ => todo!(),
}
}
pub fn trigger_error(_message: &str, _error_level: i64) {
+ // TODO(phase-d): emitting a PHP error obeys error_reporting and the installed error handler
+ // (both runtime state not modeled here); writing unconditionally to stderr would diverge.
todo!()
}
pub fn trigger_deprecation(_package: &str, _version: &str, _message: &str, _arg: &str) {
+ // TODO(phase-d): symfony/deprecation-contracts triggers an E_USER_DEPRECATED via the error
+ // subsystem, which is not modeled (see trigger_error).
todo!()
}
@@ -277,31 +431,40 @@ pub fn usleep(_microseconds: u64) {
/// Equivalent to PHP's __DIR__ magic constant
pub fn php_dir() -> String {
+ // TODO(phase-d): __DIR__ is the directory of the source file at compile time; it must be supplied
+ // per call site (e.g. via a macro), not from a runtime shim function.
todo!()
}
pub fn dir() -> String {
+ // TODO(phase-d): see php_dir; __DIR__ is a per-source-file compile-time value.
todo!()
}
/// Equivalent to PHP's `require <file>` returning the file's return value
pub fn require_php_file(_filename: &str) -> PhpMixed {
+ // TODO(phase-d): `require` evaluates a PHP source file at runtime; there is no PHP interpreter.
todo!()
}
pub fn php_require(_file: &str) -> PhpMixed {
+ // TODO(phase-d): see require_php_file.
todo!()
}
pub fn r#eval(_code: &str) -> PhpMixed {
+ // TODO(phase-d): `eval` runs arbitrary PHP source at runtime; there is no PHP interpreter.
todo!()
}
pub fn memory_get_usage() -> i64 {
+ // TODO(phase-d): PHP reports memory tracked by its own allocator (emalloc); Rust has no
+ // equivalent accounting, and process RSS would not match PHP's semantics.
todo!()
}
pub fn memory_get_peak_usage(_real_usage: bool) -> i64 {
+ // TODO(phase-d): see memory_get_usage; PHP's peak emalloc figure has no Rust equivalent.
todo!()
}
@@ -309,14 +472,18 @@ pub fn call_user_func<T>(_callback: &str, _args: &[PhpMixed]) -> T
where
T: From<PhpMixed>,
{
+ // TODO(phase-d): invoking a function by name needs a runtime function registry; the shim has no
+ // way to resolve a callable from a string.
todo!()
}
pub fn call_user_func_array(_callback: &str, _args: &PhpMixed) -> PhpMixed {
+ // TODO(phase-d): see call_user_func.
todo!()
}
pub fn call_php_callable(_callback: &PhpMixed, _args: &[PhpMixed]) -> PhpMixed {
+ // TODO(phase-d): PhpMixed carries no callable variant; a runtime callable cannot be invoked.
todo!()
}
@@ -326,18 +493,23 @@ pub fn error_get_last() -> Option<IndexMap<String, PhpMixed>> {
}
pub fn globals_get(_name: &str) -> PhpMixed {
+ // TODO(phase-d): the PHP $GLOBALS superglobal is not modeled in the shim.
todo!()
}
pub fn globals_set(_name: &str, _value: PhpMixed) {
+ // TODO(phase-d): the PHP $GLOBALS superglobal is not modeled in the shim.
todo!()
}
pub fn clone<T: Clone>(_value: T) -> T {
- todo!()
+ // PHP's `clone` makes a (shallow) copy of an object; Rust's Clone is the closest equivalent.
+ _value.clone()
}
pub fn ini_set(_varname: &str, _value: &str) -> Option<String> {
+ // TODO(phase-d): ini_set must return the previous value and have its override observed by a
+ // subsequent ini_get; ini_get is currently a static lookup, so overrides cannot be wired up yet.
todo!()
}
@@ -360,6 +532,8 @@ pub fn run_shutdown_functions() {
}
pub fn composer_dev_warning_time() -> i64 {
+ // TODO(phase-d): COMPOSER_DEV_WARNING_TIME is a build-time constant baked into Composer's release
+ // artifact; it has no fixed value in source and must be provided by the build process.
todo!()
}
@@ -377,18 +551,25 @@ pub fn gc_enable() {
}
pub fn react_promise_resolve(_value: PhpMixed) -> PhpMixed {
+ // TODO(phase-d): depends on the react/promise port (shirabe_external_packages), which is not yet
+ // available.
todo!()
}
pub fn ioncube_loader_iversion() -> i64 {
+ // TODO(phase-d): the ionCube loader is not present (extension_loaded reports it absent), so this
+ // function is never defined at runtime; left unimplemented.
todo!()
}
pub fn ioncube_loader_version() -> String {
+ // TODO(phase-d): see ioncube_loader_iversion.
todo!()
}
pub fn phpinfo(_what: i64) {
+ // TODO(phase-d): phpinfo() dumps the full PHP runtime configuration, which the shim does not
+ // model.
todo!()
}
@@ -399,17 +580,22 @@ pub fn exit(status: i64) -> ! {
}
pub fn sapi_windows_vt100_support(_resource: &crate::PhpResource) -> bool {
+ // TODO(phase-d): Windows-only SAPI function; not defined on the non-Windows target this build
+ // models (function_exists reports it absent).
todo!()
}
pub fn sapi_windows_cp_get(_kind: Option<&str>) -> i64 {
+ // TODO(phase-d): Windows-only SAPI function; see sapi_windows_vt100_support.
todo!()
}
pub fn sapi_windows_cp_set(_codepage: i64) -> bool {
+ // TODO(phase-d): Windows-only SAPI function; see sapi_windows_vt100_support.
todo!()
}
pub fn sapi_windows_cp_conv(_in_codepage: i64, _out_codepage: i64, _subject: &str) -> String {
+ // TODO(phase-d): Windows-only SAPI function; see sapi_windows_vt100_support.
todo!()
}