diff options
Diffstat (limited to 'crates/shirabe-php-rpc/php')
| -rw-r--r-- | crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php | 98 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/php/worker.php | 22 |
2 files changed, 31 insertions, 89 deletions
diff --git a/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php index eee48266..c16b4e18 100644 --- a/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php +++ b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php @@ -3,10 +3,12 @@ // The PHP half of the materialized-value codec (crates/shirabe/src/plugin/php_plugin_value.rs). // An object whose entity lives on the Rust side crosses as a handle, but an immutable value // has no entity to point at: the child holds a genuine instance of the real class instead, and -// the wire carries the class name plus the constructor arguments needed to rebuild it. +// the wire carries the object record `serialize()` writes for it, which `unserialize()` revives +// without running a constructor. // -// Only the classes listed in CLASSES cross this way. An unknown class name is an explicit -// error rather than a `new $class`, so the descriptor can never name an arbitrary class. +// CLASSES is the whole vocabulary that may cross this way: it is both the set of classes handed +// to `serialize()` in place of a handle descriptor, and the `allowed_classes` list every frame +// payload is unserialized under, so a payload can never name another class. namespace Shirabe; @@ -18,7 +20,7 @@ use Composer\Semver\Constraint\MultiConstraint; final class MaterializedValue { - private const CLASSES = [ + public const CLASSES = [ Link::class, Constraint::class, MultiConstraint::class, @@ -29,86 +31,22 @@ final class MaterializedValue ]; /** - * @param array{__pnew: string, __args?: array, __calls?: array} $descriptor + * The object to serialize onto the wire in place of $value, or null when the Rust side has + * no value to rebuild it as (it then crosses as a P-table entity). */ - public static function build(array $descriptor): object + public static function forWire(object $value): ?object { - $class = $descriptor['__pnew']; - if (!\in_array($class, self::CLASSES, true)) { - throw new \RuntimeException( - "the class {$class} cannot be materialized in the plugin runtime" - ); - } - $value = new $class(...array_values($descriptor['__args'] ?? [])); - foreach ($descriptor['__calls'] ?? [] as [$method, $args]) { - $value->$method(...array_values($args)); - } - return $value; - } - - /** - * The descriptor for a value the Rust side rebuilds by value, or null when the object is - * not one of them (it then crosses as a P-table entity). - * - * @return ?array{__pnew: string, __args: array, __calls?: array} - */ - public static function describe(object $value): ?array - { - if ($value instanceof Link) { - return [ - '__pnew' => Link::class, - '__args' => [ - self::field($value, 'source'), - self::field($value, 'target'), - $value->getConstraint(), - self::field($value, 'description'), - // Not getPrettyConstraint(): that throws when the link was built without - // one, and an absent pretty constraint has to cross as absent. - self::field($value, 'prettyConstraint'), - ], - ]; - } - if ($value instanceof Constraint) { - return self::constraint($value, [$value->getOperator(), $value->getVersion()]); - } - if ($value instanceof MultiConstraint) { - return self::constraint($value, [$value->getConstraints(), $value->isConjunctive()]); - } - if ($value instanceof MatchAllConstraint || $value instanceof MatchNoneConstraint) { - return self::constraint($value, []); - } if ($value instanceof \DateTimeInterface) { - return [ - '__pnew' => $value instanceof \DateTime ? \DateTime::class : \DateTimeImmutable::class, - // DATE_ATOM widened by the microseconds a PHP date carries, so the instant - // crosses at the full precision this side can represent. - '__args' => [$value->format('Y-m-d\TH:i:s.uP')], - ]; + // The Rust side holds instants in UTC and carries no timezone database, so the date + // crosses rebased on UTC. Going through the offset rather than the zone keeps the + // instant exact across an ambiguous wall clock, and drops any subclass a plugin + // brought along. + return (new \DateTimeImmutable($value->format('Y-m-d H:i:s.uP'))) + ->setTimezone(new \DateTimeZone('UTC')); } - return null; - } - /** - * No constraint constructor takes the pretty string, and whether it was ever set is - * observable through getPrettyString(), so it travels as a post-construction call. - * - * @param list<mixed> $args - * @return array{__pnew: string, __args: list<mixed>, __calls: list<array{string, list<mixed>}>} - */ - private static function constraint(object $value, array $args): array - { - return [ - '__pnew' => \get_class($value), - '__args' => $args, - '__calls' => [['setPrettyString', [self::field($value, 'prettyString')]]], - ]; - } - - /** Reads a protected field these value classes expose no getter for. */ - private static function field(object $value, string $name) - { - $property = new \ReflectionProperty($value, $name); - (\PHP_VERSION_ID < 80100) and $property->setAccessible(true); - return $property->getValue($value); + // Not instanceof: a subclass has state and behaviour of its own that no Rust value + // carries, so it crosses as an entity instead. + return \in_array(\get_class($value), self::CLASSES, true) ? $value : null; } } diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index 092f535f..15b38770 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -227,9 +227,11 @@ final class ShirabeRpcRuntime // A natively-constructed dual-mode instance falls through to the P table below. } if (is_object($value)) { - $materialized = \Shirabe\MaterializedValue::describe($value); + // A materialized value needs no descriptor: it crosses as the object record + // serialize() writes for it, which the Rust side decodes into its own value. + $materialized = \Shirabe\MaterializedValue::forWire($value); if ($materialized !== null) { - return array_map([self::class, 'toWire'], $materialized); + return $materialized; } return ShirabePhpObjectRegistry::descriptor($value); } @@ -242,7 +244,10 @@ final class ShirabeRpcRuntime return $value; } - /** Converts a decoded wire value: handle descriptor arrays become live objects. */ + /** + * Converts a decoded wire value: handle descriptor arrays become live objects. A + * materialized value arrives as a real instance already, revived by unserialize(). + */ public static function fromWire($value) { if (!is_array($value)) { @@ -261,9 +266,6 @@ final class ShirabeRpcRuntime if (isset($value['__pclass']) && count($value) === 1) { return $value['__pclass']; } - if (isset($value['__pnew'])) { - return \Shirabe\MaterializedValue::build(array_map([self::class, 'fromWire'], $value)); - } return array_map([self::class, 'fromWire'], $value); } @@ -287,7 +289,7 @@ final class ShirabeRpcRuntime if ($inId !== $corrId) { self::fail("protocol violation: response for unexpected corr_id {$inId}"); } - $fields = unserialize($payload, ['allowed_classes' => false]); + $fields = unserialize($payload, ['allowed_classes' => \Shirabe\MaterializedValue::CLASSES]); if (!is_array($fields)) { self::fail('protocol violation: unparseable response payload'); } @@ -314,7 +316,7 @@ final class ShirabeRpcRuntime public static function dispatchRequest(int $tag, int $corrId, string $payload): void { - $fields = unserialize($payload, ['allowed_classes' => false]); + $fields = unserialize($payload, ['allowed_classes' => \Shirabe\MaterializedValue::CLASSES]); if (!is_array($fields)) { self::fail('protocol violation: unparseable frame payload'); } @@ -588,7 +590,9 @@ ShirabeRpcRuntime::$dispatch = [ // Shirabe-internal helpers, not PHP builtins: '__shirabe_eval' => static fn($args) => eval($args[0]), // Round-trips raw serialize() bytes through the PHP core codec, for the codec oracle tests. - '__shirabe_oracle_roundtrip' => static fn($args) => serialize(unserialize($args[0], ['allowed_classes' => false])), + '__shirabe_oracle_roundtrip' => static fn($args) => serialize( + unserialize($args[0], ['allowed_classes' => \Shirabe\MaterializedValue::CLASSES]) + ), '__shirabe_require' => static function ($args) { require_once $args[0]; // The required file may have registered further prepending autoloaders (a Composer |
