aboutsummaryrefslogtreecommitdiffhomepage
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/dev/php-rpc.md20
-rw-r--r--docs/dev/plugin-class-classification.md97
-rw-r--r--docs/dev/plugin-stub-generation.md27
3 files changed, 91 insertions, 53 deletions
diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md
index eb93db19..aebf0097 100644
--- a/docs/dev/php-rpc.md
+++ b/docs/dev/php-rpc.md
@@ -184,10 +184,24 @@ Notable internal helpers:
The runtime service endpoint (handle 0) answers `__shirabe_find_file` (autoload lookups),
`__shirabe_run_rust_command` — the reverse half of the two-world command split: a
`\Shirabe\RustCommandStub` forwards its stringified input here and the built-in command runs in
-the Rust process, against the Rust-side application state — and `__shirabeConstruct`, which
+the Rust process, against the Rust-side application state — `__shirabeConstruct`, which
allocates the Rust entity behind a `new SomeProxiedClass(...)` written by plugin code and
-answers with `[rhandle, epoch]`. Classes whose entity Rust cannot build are an explicit error
-naming the class.
+answers with `[rhandle, epoch]`, and `__shirabeCallStatic`, which takes `[class, method, args]`
+and runs a static method no handle identifies a receiver for. Classes and static methods Rust
+cannot answer are an explicit error naming them.
+
+### By-ref parameters
+
+`out_param_positions` names the by-ref parameters of the called method, as the caller declared
+them; the answering `Return` carries `out_params`, a map from the same positions to the value
+each parameter holds afterwards, and the caller assigns those back into its own variables. A
+position the answer leaves out was never assigned to, which is what PHP does with an untouched
+by-ref parameter — so an out-param is genuinely optional rather than defaulting to null.
+
+A generated stub fills both halves mechanically from the real signature
+(`docs/dev/plugin-stub-generation.md`). `Composer\Util\ProcessExecutor::execute` is the method
+this exists for: it is the only by-ref parameter on Composer's public surface that belongs to a
+proxied class.
## Proxy stubs, runtime classes and guards
diff --git a/docs/dev/plugin-class-classification.md b/docs/dev/plugin-class-classification.md
index 1c7aec09..516cf226 100644
--- a/docs/dev/plugin-class-classification.md
+++ b/docs/dev/plugin-class-classification.md
@@ -139,11 +139,15 @@ design-sensitive.
The class writes to `static` properties. Sub-classified by the disposition
list (see exception lists): `memo-cache` (pure memoization, each world may
compute its own: `Git::$version`, `Platform::$isDocker`, …), `seed-once`
-(copied from the Rust side once at child startup:
-`ProcessExecutor::$timeout`, which Composer seeds from config),
-`needs-sync` (genuinely shared process state: `Platform`'s env table), or
-`needs-review` (default for newly appearing ones — the run fails loudly
-until a human files it).
+(copied from the Rust side once at child startup; nothing is filed this way
+today), `needs-sync` (genuinely shared process state: `Platform`'s env table,
+`ProcessExecutor::$timeout`), or `needs-review` (default for newly appearing
+ones — the run fails loudly until a human files it).
+
+A `needs-sync` static is not actually synced by copying it: the child holds no
+copy at all, because the stub forwards the accessors that read and write it
+(`__shirabeCallStatic`, see `docs/dev/plugin-stub-generation.md`). The
+disposition records that the state is shared, not the mechanism.
#### throwable
@@ -369,23 +373,35 @@ Earlier design analysis assumed no public getter returns a
`Composer::getLoop()` → `Loop::getProcessExecutor(): ?ProcessExecutor` /
`Loop::getHttpDownloader(): HttpDownloader` make both reachable. The
graph-owned `ProcessExecutor` instance must therefore be proxied (its job
-queue is driven by the Rust loop), while the design intent — plugins using
-it as a stateless utility — survives only for plugin-`new`ed instances.
-This is exactly the dual-instantiation situation the
-`plugin-constructible` attribute exists to surface.
+queue is driven by the Rust loop), which is the dual-instantiation situation
+the `plugin-constructible` attribute exists to surface.
+
+`ProcessExecutor` is one class in one category for both routes: it is a
+`rust-proxy` stub, and a plugin-`new`ed instance allocates a Rust-side entity
+of its own rather than a second, unconnected executor. That is what keeps
+`$timeout` — process-wide state Composer seeds from `process-timeout` and
+`RunScriptCommand` rewrites mid-run — readable and writable from both worlds
+through one value, and it is what lets the executor stay a constructor
+argument of the classes that take one (`new Filesystem($process)` is what
+plugins actually write). Children are spawned in the Rust process either way,
+which is also correct for stdio: the worker inherits the Rust process's
+standard streams, so both worlds see the same terminal.
-Both routes raise an explicit error today, and which one gets a story first
-is still open. The graph-owned instances cannot be obtained at all: the
-`Composer` proxy answers `getLoop()` with an explicit error, and `Loop`,
-`ProcessExecutor` and `HttpDownloader` are guarded classes, so a
-plugin-`new`ed instance is an explicit error too rather than a second
-instance the Rust side never sees.
+What stays an explicit error is the async surface — `executeAsync()`,
+`wait()`, `enableAsync()`, `countActiveJobs()` — for two reasons that outlive
+the choice of category. `executeAsync()` resolves its promise with a
+`Symfony\Component\Process\Process`, whose state is the `proc_open()`
+resource and OS pipes of whichever process called `start()`, so a Rust-side
+spawn has no such object to hand back; and driving it needs a promise
+representation that crosses the boundary unresolved, which the execution model
+does not have. Neither is specific to how the executor is obtained: a plugin
+can reach the async path on an instance of its own, since `enableAsync()`,
+`wait()` and `countActiveJobs()` are all public (`@internal` is a docblock
+note).
-An earlier argument for leaving plugin-`new`ed instances alone does not
-hold: `executeAsync()` does throw a `LogicException` unless `enableAsync()`
-ran first, but `enableAsync()`, `wait()` and `countActiveJobs()` are all
-public — `@internal` is a docblock note — so nothing stops a plugin from
-driving its own instance through the async path.
+`HttpDownloader` and `Loop` remain guarded, so `Composer::getLoop()` is still
+an explicit error and the graph's own executor is not reachable yet. Serving
+it needs only a `Loop` stub, since the executor's surface is already served.
### Dual instantiation
@@ -406,39 +422,24 @@ stub constructor forwards to `__shirabeConstruct`, the Rust side allocates
the entity and answers with its handle, and the plugin-`new`ed object is
then the same entity the graph sees. It is filled in per class, driven by
the explicit errors real plugins hit — today `Package`, `CompletePackage`,
-the three alias packages, the five solver operations and
-`Composer\Util\Filesystem` can be built this way, the last one only without
-a `ProcessExecutor` argument (that class has no stub, so the argument could
-only be an instance the Rust side never sees). Every other proxied class
-answers with an explicit error naming it.
+the three alias packages, the five solver operations,
+`Composer\Util\Filesystem` (with or without its `ProcessExecutor` argument)
+and `Composer\Util\ProcessExecutor` can be built this way. Every other
+proxied class answers with an explicit error naming it.
-`JsonFile`, `ArrayLoader` and `ProcessExecutor` are still undecided, so the
-classes above stay `unsupported` — but a guard now shadows each of them in
-the child, so touching one is an explicit error instead of real code
-running against a second instance.
+`JsonFile` and `ArrayLoader` are still undecided, so the classes above stay
+`unsupported` — but a guard now shadows each of them in the child, so
+touching one is an explicit error instead of real code running against a
+second instance.
The demotion rule has not been taught about the construction stories: it
demotes for constructing any `rust-proxy` service, whether or not that
service can now be built. `Composer\Package\Archiver\ArchivableFilesFinder`
-is `unsupported` for `new Filesystem` alone, which no longer forks any
-state; promoting such a class means feeding the per-class construction
-stories back into the rule.
-
-### Process: dual instantiation split by caller
-
-`ProcessExecutor::executeAsync()` resolves its promise with a
-`Symfony\Component\Process\Process` instance, so it may cross the language
-boundary despite being a wholesale-`php-native` vendor class. A `Process`
-can't be reconstructed PHP-side from Rust-generated data because its state
-is stored in a `resource` created by `proc_open()`.
-
-Proposed resolution (not adopted): split `ProcessExecutor`'s Rust
-implementation by caller.
-Rust-ported Composer code (`VersionGuesser`, `Git`, …) calls `execute_async()`
-directly and spawns in Rust. A plugin holding a `ProcessExecutor` handle
-(`Loop::getProcessExecutor()`) instead hits the `rust-proxy` stub's RPC entry,
-which forwards the spawn to the PHP child so the real `Process::start()` runs
-there. The plugin gets the genuine object, never a fake one.
+is `unsupported` for `new Filesystem` alone, and the VCS/auth belt is
+`unsupported` for `new ProcessExecutor` alone; neither forks any state any
+more. Promoting them means feeding the per-class construction stories back
+into the rule — and, for the belt, deciding `HttpDownloader` too, since
+those classes take one and no route to it exists yet.
### Package and CompletePackage
diff --git a/docs/dev/plugin-stub-generation.md b/docs/dev/plugin-stub-generation.md
index e2b6d848..a14d7ce1 100644
--- a/docs/dev/plugin-stub-generation.md
+++ b/docs/dev/plugin-stub-generation.md
@@ -88,11 +88,32 @@ the generator's vendor directory or the classifier report is unavailable.
the class's own public methods already cover their surface.
* Methods returning `self`/`static` perform the RPC and then `return $this;`
to preserve identity instead of round-tripping the handle.
+* **By-ref parameters** are declared `&$name` on the stub as well. The call
+ carries their positions, and the answer carries the value each of them holds
+ afterwards, which the stub assigns back (see "By-ref parameters" in
+ `docs/dev/php-rpc.md`). A position the answer omits is left alone, so a
+ parameter the callee never assigned to keeps the value it had.
+* **Arity** is reproduced when — and only when — the real method body calls
+ `func_num_args()`, in which case the stub trims the argument list to what the
+ caller actually passed instead of sending the declared defaults. Sending them
+ regardless would make the Rust side answer a call the plugin never made:
+ `ProcessExecutor::execute($cmd)` forwards the child's output, while
+ `execute($cmd, $out)` captures it, and only the argument count separates them.
+ A body calling `func_get_args()` fails generation instead.
* **Class constants, static methods and public static properties** are
materialized verbatim from the real source (they read no instance state and
run locally in the worker), together with any non-public static helpers the
methods call. Constants keep their declared visibility, so a non-public one
stays unreadable from outside the stub as it is in the real class.
+* A static method is materialized only when it can actually run in the worker.
+ One that reads a **static property** — whose value the Rust side owns, as
+ `ProcessExecutor::$timeout` does — or that reaches a class a **guard**
+ shadows there — as `ProcessExecutor::escape()` reaches `Composer\Util\Platform`
+ — forwards instead, through `__shirabeCallStatic` on handle 0, carrying a
+ comment that names the reason. The test covers the transitive closure of the
+ non-public static helpers the method calls, since those are materialized with
+ it. A forwarded static may not take by-ref parameters; that combination fails
+ generation.
* **Instance properties** are not declared on the stub, whatever their
visibility: they are entity state. Every root stub instead carries
`__get`/`__set`/`__isset`/`__unset` forwarders, so each access reaches the
@@ -137,8 +158,10 @@ Generation fails — instead of emitting something quietly wrong — on:
* a target missing from the classifier report, classified other than
`rust-proxy`/`contract`, or a report carrying violations,
-* by-ref or variadic parameters (in constructors too), static interface
- methods, magic methods other than `__toString`/`__clone`,
+* variadic parameters (in constructors too), by-ref parameters in a
+ constructor or in a forwarded static method, `func_get_args()` in a forwarded
+ body, static interface methods, magic methods other than
+ `__toString`/`__clone`,
* an omitted override diverging from the inherited stub signature,
* a subclass target listed before its base class, or extending a class that is
neither a target nor provided by `php/runtime/`,