diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-30 23:01:25 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-30 23:09:56 +0900 |
| commit | d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49 (patch) | |
| tree | a745ecc3403104d5a34f29964362e239e8f5675b /crates/shirabe-php-rpc | |
| parent | 057f3b8de26293319e265c1d86d9a1153124f3c7 (diff) | |
| download | php-shirabe-d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49.tar.gz php-shirabe-d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49.tar.zst php-shirabe-d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49.zip | |
feat(plugin): serve ProcessExecutor as a proxy stub
Composer reaches this class two ways: the object graph hands one out through
Composer::getLoop()->getProcessExecutor(), and plugins write
`new ProcessExecutor($io)` freely. Both bind to a Rust-side entity, so the
timeout the run shares -- seeded from process-timeout and rewritten while the
run is in flight -- has one value instead of one per world, and the executor
can still be passed to the classes that take one (`new Filesystem($process)`).
Three things the stub generator was missing came with it:
- By-ref parameters. The call carries their positions and the answer carries
what each holds afterwards; a position the answer omits was never assigned
to, which is what PHP does with an untouched by-ref parameter.
ProcessExecutor::execute is the only one on a proxied class.
- Argument arity, reproduced where the real body reads func_num_args().
execute($cmd) forwards the child's output and execute($cmd, $out) captures
it, and nothing but the argument count separates the two.
- Static methods that cannot run in the worker. One that reads a static
property the Rust side owns, or that reaches a guarded class, forwards
through __shirabeCallStatic instead of being materialized. That also fixes
Filesystem::isLocalPath and getPlatformPath, whose materialized bodies
called the guarded Composer\Util\Platform.
The async surface stays an explicit error. executeAsync resolves its promise
with a Symfony Process, whose proc_open() resource and pipes belong to
whichever process called start(), so a Rust-side spawn has none to hand back;
running the real start() in the worker needs a promise representation that
crosses the boundary unresolved.
The fixture project drives the whole synchronous surface from plugin code and
compares the trace against upstream Composer byte for byte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-rpc')
| -rw-r--r-- | crates/shirabe-php-rpc/php/guards/Composer/Util/ProcessExecutor.php | 114 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php | 32 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php | 176 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/php/worker.php | 23 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/lib.rs | 24 |
5 files changed, 227 insertions, 142 deletions
diff --git a/crates/shirabe-php-rpc/php/guards/Composer/Util/ProcessExecutor.php b/crates/shirabe-php-rpc/php/guards/Composer/Util/ProcessExecutor.php deleted file mode 100644 index 405905e8..00000000 --- a/crates/shirabe-php-rpc/php/guards/Composer/Util/ProcessExecutor.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php - -// Generated by scripts/plugin-stub-generator; do not edit by hand. -// Guard for Composer\Util\ProcessExecutor. -// The Rust side owns this class and the worker has no proxy for it, so this -// declaration shadows the real one: the constants and the hierarchy stay, while -// constructing it or calling anything on it raises an explicit error. - -namespace Composer\Util; - -use Composer\IO\IOInterface; -use React\Promise\PromiseInterface; - -class ProcessExecutor -{ - private const STATUS_QUEUED = 1; - private const STATUS_STARTED = 2; - private const STATUS_COMPLETED = 3; - private const STATUS_FAILED = 4; - private const STATUS_ABORTED = 5; - private const BUILTIN_CMD_COMMANDS = [ - 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', - 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', - 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', - 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', - 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', - ]; - private const GIT_CMDS_NEED_GIT_DIR = [ - ['show'], - ['log'], - ['branch'], - ['remote', 'set-url'], - ]; - - public function __construct(?IOInterface $io = null) - { - \ShirabeUnsupportedClass::fail(self::class, '__construct'); - } - - public function execute($command, &$output = null, ?string $cwd = null): int - { - \ShirabeUnsupportedClass::fail(self::class, 'execute'); - } - - public function executeTty($command, ?string $cwd = null): int - { - \ShirabeUnsupportedClass::fail(self::class, 'executeTty'); - } - - public function executeAsync($command, ?string $cwd = null): PromiseInterface - { - \ShirabeUnsupportedClass::fail(self::class, 'executeAsync'); - } - - protected function outputHandler(string $type, string $buffer): void - { - \ShirabeUnsupportedClass::fail(self::class, 'outputHandler'); - } - - public function setMaxJobs(int $maxJobs): void - { - \ShirabeUnsupportedClass::fail(self::class, 'setMaxJobs'); - } - - public function resetMaxJobs(): void - { - \ShirabeUnsupportedClass::fail(self::class, 'resetMaxJobs'); - } - - public function wait($index = null): void - { - \ShirabeUnsupportedClass::fail(self::class, 'wait'); - } - - public function enableAsync(): void - { - \ShirabeUnsupportedClass::fail(self::class, 'enableAsync'); - } - - public function countActiveJobs($index = null): int - { - \ShirabeUnsupportedClass::fail(self::class, 'countActiveJobs'); - } - - public function splitLines(?string $output): array - { - \ShirabeUnsupportedClass::fail(self::class, 'splitLines'); - } - - public function getErrorOutput(): string - { - \ShirabeUnsupportedClass::fail(self::class, 'getErrorOutput'); - } - - public static function getTimeout(): int - { - \ShirabeUnsupportedClass::fail(self::class, 'getTimeout'); - } - - public static function setTimeout(int $timeout): void - { - \ShirabeUnsupportedClass::fail(self::class, 'setTimeout'); - } - - public static function escape($argument): string - { - \ShirabeUnsupportedClass::fail(self::class, 'escape'); - } - - public function requiresGitDirEnv($command): bool - { - \ShirabeUnsupportedClass::fail(self::class, 'requiresGitDirEnv'); - } -} diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php b/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php index e1532889..c66bcf56 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php @@ -83,26 +83,6 @@ class Filesystem implements \ShirabeRustStub return $path; } - public static function isLocalPath(string $path) - { - // on windows, \\foo indicates network paths so we exclude those from local paths, however it is unsafe - // on linux as file:////foo (which would be a network path \\foo on windows) will resolve to /foo which could be a local path - if (Platform::isWindows()) { - return Preg::isMatch('{^(file://(?!//)|/(?!/)|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); - } - - return Preg::isMatch('{^(file://|/|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); - } - - public static function getPlatformPath(string $path) - { - if (Platform::isWindows()) { - $path = Preg::replace('{^(?:file:///([a-z]):?/)}i', 'file://$1:/', $path); - } - - return Preg::replace('{^file://}i', '', $path); - } - public static function isReadable(string $path) { if (is_readable($path)) { @@ -121,6 +101,18 @@ class Filesystem implements \ShirabeRustStub return false; } + // Forwarded rather than materialized: it references Composer\Util\Platform, which a guard shadows in the worker. + public static function isLocalPath(string $path) + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'isLocalPath', [$path]]); + } + + // Forwarded rather than materialized: it references Composer\Util\Platform, which a guard shadows in the worker. + public static function getPlatformPath(string $path) + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'getPlatformPath', [$path]]); + } + public function remove(string $file) { return \ShirabeRpcRuntime::callRust($this->__rhandle, 'remove', [$file]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php b/crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php new file mode 100644 index 00000000..dfa1f0d2 --- /dev/null +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Util/ProcessExecutor.php @@ -0,0 +1,176 @@ +<?php + +// Generated by scripts/plugin-stub-generator; do not edit by hand. +// Proxy stub for Composer\Util\ProcessExecutor: the public surface forwards to the Rust-side entity over RPC. + +namespace Composer\Util; + +use Composer\IO\IOInterface; +use React\Promise\PromiseInterface; + +class ProcessExecutor implements \ShirabeRustStub +{ + /** @var int */ + protected $__rhandle; + /** @var int */ + protected $__epoch; + + /** + * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses + * the constructor, which belongs to plugin code building a new entity instead. + */ + public function __shirabeBind(int $rhandle, int $epoch): void + { + $this->__rhandle = $rhandle; + $this->__epoch = $epoch; + } + + public function __destruct() + { + \ShirabeRustObjectRegistry::release($this->__rhandle); + } + + public function __shirabeRustHandleDescriptor(): array + { + return [ + '__rhandle' => $this->__rhandle, + '__class' => static::class, + '__epoch' => $this->__epoch, + ]; + } + + public function __clone() + { + // PHP has already shallow-copied this stub, so both copies would point at one + // entity and release it twice. The Rust side clones the entity instead, applying + // whatever __clone semantics the real class defines, and this copy rebinds to the + // fresh handle. Entities without clone semantics answer with an explicit error. + [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust($this->__rhandle, '__shirabeClone', []); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + + public function __construct(?IOInterface $io = null) + { + [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$io]]); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + private const STATUS_QUEUED = 1; + private const STATUS_STARTED = 2; + private const STATUS_COMPLETED = 3; + private const STATUS_FAILED = 4; + private const STATUS_ABORTED = 5; + private const BUILTIN_CMD_COMMANDS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; + private const GIT_CMDS_NEED_GIT_DIR = [ + ['show'], + ['log'], + ['branch'], + ['remote', 'set-url'], + ]; + + // Forwarded rather than materialized: it reads the static property $timeout, whose value the Rust side owns. + public static function getTimeout(): int + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'getTimeout', []]); + } + + // Forwarded rather than materialized: it reads the static property $timeout, whose value the Rust side owns. + public static function setTimeout(int $timeout): void + { + \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'setTimeout', [$timeout]]); + } + + // Forwarded rather than materialized: it references Composer\Util\Platform, which a guard shadows in the worker. + public static function escape($argument): string + { + return \ShirabeRpcRuntime::callRust(0, '__shirabeCallStatic', [self::class, 'escape', [$argument]]); + } + + public function execute($command, &$output = null, ?string $cwd = null): int + { + $__args = [$command, $output, $cwd]; + array_splice($__args, func_num_args()); + $__out = []; + $__result = \ShirabeRpcRuntime::callRust($this->__rhandle, 'execute', $__args, [1], $__out); + if (array_key_exists(1, $__out)) { + $output = $__out[1]; + } + return $__result; + } + + public function executeTty($command, ?string $cwd = null): int + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'executeTty', [$command, $cwd]); + } + + public function executeAsync($command, ?string $cwd = null): PromiseInterface + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'executeAsync', [$command, $cwd]); + } + + public function setMaxJobs(int $maxJobs): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setMaxJobs', [$maxJobs]); + } + + public function resetMaxJobs(): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'resetMaxJobs', []); + } + + public function wait($index = null): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'wait', [$index]); + } + + public function enableAsync(): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'enableAsync', []); + } + + public function countActiveJobs($index = null): int + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'countActiveJobs', [$index]); + } + + public function splitLines(?string $output): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'splitLines', [$output]); + } + + public function getErrorOutput(): string + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getErrorOutput', []); + } + + public function requiresGitDirEnv($command): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'requiresGitDirEnv', [$command]); + } +} diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index 6ed5df40..bc10c2de 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -295,15 +295,29 @@ final class ShirabeRpcRuntime return array_map([self::class, 'fromWire'], $value); } - /** Sends a CallRustMethod request and drives the cooperative loop until its Return. */ - public static function callRust(int $rhandle, string $method, array $args) - { + /** + * Sends a CallRustMethod request and drives the cooperative loop until its Return. + * + * `$outParamPositions` names the by-ref parameters of the called method; the Rust side + * answers with the value each of them holds afterwards, and the caller (a generated stub) + * assigns those back to its own by-ref parameters. + * + * @param list<int> $outParamPositions + * @param array<int, mixed>|null $outParams + */ + public static function callRust( + int $rhandle, + string $method, + array $args, + array $outParamPositions = [], + ?array &$outParams = null + ) { $corrId = self::$nextCorrId; self::$nextCorrId += 2; self::writeFrame( SHIRABE_TAG_CALL_RUST_METHOD, $corrId, - serialize([$rhandle, $method, self::toWire($args), []]) + serialize([$rhandle, $method, self::toWire($args), $outParamPositions]) ); while (true) { $frame = self::readFrame(); @@ -320,6 +334,7 @@ final class ShirabeRpcRuntime self::fail('protocol violation: unparseable response payload'); } if ($tag === SHIRABE_TAG_RETURN) { + $outParams = self::fromWire($fields[1] ?? []); return self::fromWire($fields[0]); } [$class, $message, $code] = $fields; diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs index b2bc2c28..dfd8fa67 100644 --- a/crates/shirabe-php-rpc/src/lib.rs +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -643,6 +643,12 @@ impl std::error::Error for PhpThrow {} /// Handles `CallRustMethod` requests arriving while a Rust-initiated call is waiting for its /// `Return` (the cooperative reentrancy loop). A handler may itself issue nested RPC calls. +/// +/// `out_param_positions` names the by-ref parameters of the called method, as the calling stub +/// declared them. A handler that assigns to one writes the resulting value into `out_params` under +/// the same position, and the stub copies it back into the caller's variable; leaving a position +/// out means the method never assigned to it, which is what PHP does with an untouched by-ref +/// parameter. pub trait RustMethodDispatcher { fn dispatch( &mut self, @@ -650,6 +656,7 @@ pub trait RustMethodDispatcher { method_name: &str, args: Vec<PluginValue>, out_param_positions: &[u32], + out_params: &mut IndexMap<u32, PluginValue>, ) -> Result<PluginValue, PhpThrow>; } @@ -812,10 +819,15 @@ fn rpc_call( args, out_param_positions, } => { + let mut out_params = IndexMap::new(); let outcome = match dispatcher.as_deref_mut() { - Some(dispatcher) => { - dispatcher.dispatch(rhandle, &method_name, args, &out_param_positions) - } + Some(dispatcher) => dispatcher.dispatch( + rhandle, + &method_name, + args, + &out_param_positions, + &mut out_params, + ), // Never fall back to a silent null: an unroutable callback is reported as an // explicit error on the PHP side. None => Err(PhpThrow::runtime(format!( @@ -827,7 +839,7 @@ fn rpc_call( Ok(value) => Frame::Return { corr_id, value, - out_params: IndexMap::new(), + out_params, }, Err(throw) => Frame::Throw { corr_id, @@ -1011,6 +1023,10 @@ const STUB_FILES: &[(&str, &str)] = &[ "Composer/Util/Filesystem.php", include_str!("../php/stubs/Composer/Util/Filesystem.php"), ), + ( + "Composer/Util/ProcessExecutor.php", + include_str!("../php/stubs/Composer/Util/ProcessExecutor.php"), + ), ]; /// Hand-written worker-side classes (two-world implementations with behavior of their own, not |
