From 90764c477f24e4d67d3e0cae943c33fd2b8ae220 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 6 Aug 2026 02:40:00 +0900 Subject: feat(plugin): widen the RPC surface to LibraryInstaller-based plugins An installer that extends LibraryInstaller reaches for the Composer object graph in ways the proxy did not answer: the download manager and the config, the writable repository methods, a React promise as its own return value, and `new Package(...)` from its supports() path. * `Composer::getConfig`/`getDownloadManager` are dispatched, and both classes become generated proxy stubs. Their reads and mutators answer from the Rust entity; the surfaces needing stubs of their own (ConfigSourceInterface, DownloaderInterface) stay explicit errors. * The download manager's futures are driven to completion and handed back as already-settled React promises, since PHP declares a non-nullable PromiseInterface there. A promise a plugin returns is drained the same way: settled yields its value, rejected re-raises, pending is an explicit error. * Proxy stubs now carry the real class's constructor and ask the Rust side to allocate the entity; reviving a stub for an existing entity binds the handle without running it. Classes Rust cannot build name themselves in the error. * The package proxy covers `Package`'s own setters, `CompletePackage`'s metadata, `RootPackage`'s root-only state, and `BasePackage::$id`. Link values and release dates still have no wire image, so the methods carrying them remain explicit errors. --- crates/shirabe-php-rpc/php/worker.php | 55 ++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'crates/shirabe-php-rpc/php/worker.php') diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index 12297c83..9a36abdd 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -25,6 +25,9 @@ interface ShirabeRustStub * @return ?array{__rhandle: int, __class: string, __epoch: int} */ public function __shirabeRustHandleDescriptor(): ?array; + + /** Binds this stub to an existing Rust entity, in place of running its constructor. */ + public function __shirabeBind(int $rhandle, int $epoch): void; } /** Interns proxy stubs so the same Rust handle always yields the same stub instance. */ @@ -46,7 +49,10 @@ final class ShirabeRustObjectRegistry "no proxy stub class is available for {$class}" ); } - $stub = new $class($rhandle, $epoch); + // The stub's constructor belongs to plugin code building a *new* entity; an entity that + // already exists is bound directly, so proxying never runs it. + $stub = (new ReflectionClass($class))->newInstanceWithoutConstructor(); + $stub->__shirabeBind($rhandle, $epoch); self::$internTable[$rhandle] = WeakReference::create($stub); return $stub; } @@ -626,6 +632,53 @@ ShirabeRpcRuntime::$dispatch = [ } return true; }, + // An already-fulfilled promise for a Rust-side call whose PHP signature declares + // PromiseInterface. The Rust future ran to completion before this is called, so there is + // nothing left to defer; see .ken/plugin-arch/design.md ยง10.1.6. + '__shirabe_resolved_promise' => static function ($args) { + if (!function_exists('React\\Promise\\resolve')) { + throw new RuntimeException( + 'react/promise is not loaded in the plugin process, so a PromiseInterface cannot be built' + ); + } + return \React\Promise\resolve($args[0]); + }, + // Drains a promise a plugin returned to the Rust side. React settles promises + // synchronously, so an already-settled one runs these handlers during then(); one that is + // still pending is an explicit error rather than a silently dropped continuation. + '__shirabe_settle_promise' => static function ($args) { + $promise = $args[0]; + if (!$promise instanceof \React\Promise\PromiseInterface) { + throw new RuntimeException('__shirabe_settle_promise expects a promise handle'); + } + $settled = false; + $value = null; + $rejected = false; + $reason = null; + $promise->then( + static function ($result) use (&$settled, &$value) { + $settled = true; + $value = $result; + }, + static function ($error) use (&$settled, &$rejected, &$reason) { + $settled = true; + $rejected = true; + $reason = $error; + } + ); + if (!$settled) { + throw new RuntimeException( + 'the promise returned to Shirabe is still pending; deferred resolution across the' + . ' RPC boundary is not implemented yet' + ); + } + if ($rejected) { + throw $reason instanceof Throwable + ? $reason + : new RuntimeException('the promise returned to Shirabe was rejected with ' . gettype($reason)); + } + return $value; + }, // For testing only: reads a public property of a P-table entity (PHPUnit asserts like // `$plugins[0]->version` have no method to call). '__shirabe_get_property' => static function ($args) { -- cgit v1.3.1