PHP: stream stdout and stderr via StreamedPHPResponse#2266
Merged
Conversation
Implements a php.runStream() method that returns a StreamedPHPResponse instance. It exposes stdout and stderr as ReadableStreams, allowing the caller to interact with partial output data. Before this PR, we only had php.run() that buffered stdout and stderr data and returned it all at once after the PHP code was fully executed. ## Implementation We register three FS devices at: * /internal/stdout * /internal/stderr * /internal/headers They are private to every PHP instance and are never shared with other runtimes. Then, in JavaScript, whenever a chunk of data is written to either of these devices, we propagate it to consumer via a callback, e.g. `PHPWASM.onStdout(chunk)`. Users of the `PHP` class never have to interact with these devices or callbacks directly. The PHP class creates the relevant ReadableStreams and pushes the data through them – see php.#executeWithErrorHandling() for details. #### Why not use Emscripten's stdout and stderr? Emscripten's native stdout and stderr devices stop processing data when they encounter the first null byte. However, null bytes are common when dealing with binary data. ### Backwards Compatibility php.run() continues to work. It creates a streamed response under the hood and buffers the streamed output before returning a buffered `PHPResponse` object. ## Remaining work * Add streaming-specific tests ## Follow-up work * Stream the response bytes in the web/service worker Remove old PHP CLI bindings
18983ae to
b547441
Compare
745ea86 to
085a855
Compare
085a855 to
8b10520
Compare
Member
|
Cool work, @adamziel! |
adamziel
added a commit
that referenced
this pull request
Aug 4, 2025
Guards against releasing the "request in progress" semaphore too early in the php.runStream() call. A single PHP runtime can only handle one request at a time. The PHP class calls a `wasm_sapi_handle_request` C function that initializes the PHP runtime and starts the request. That function is asynchronous and may yield back to the event loop before the request is fully handled, the exit code known, and the runtime is cleaned up and prepared for another request. The PHP class uses an async semaphore to protect against calling `wasm_sapi_handle_request` again while a previous call is still running. However, PR 2266 [1] introduced a regression where the semaphore was released too early. As a result, it opened the runtime to a race condition where a subsequent runStream() call tried to run PHP code on a runtime that was in a middle of handling a request. This test ensures that two runStream() calls can be made without crashing the runtime. [1] #2266
adamziel
added a commit
that referenced
this pull request
Aug 4, 2025
Guards against releasing the "request in progress" semaphore too early in the php.runStream() call. A single PHP runtime can only handle one request at a time. The PHP class calls a `wasm_sapi_handle_request` C function that initializes the PHP runtime and starts the request. That function is asynchronous and may yield back to the event loop before the request is fully handled, the exit code known, and the runtime is cleaned up and prepared for another request. The PHP class uses an async semaphore to protect against calling `wasm_sapi_handle_request` again while a previous call is still running. However, PR 2266 [1] introduced a regression where the semaphore was released too early. As a result, it opened the runtime to a race condition where a subsequent runStream() call tried to run PHP code on a runtime that was in a middle of handling a request. This test ensures that two runStream() calls can be made without crashing the runtime. [1] #2266
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements a
php.runStream()method that returns aStreamedPHPResponseinstance:It exposes
stdoutandstderrasReadableStream-s, allowing the caller to interactwith partial output data.
Before this PR, we only had
php.run()that bufferedstdoutandstderrdata and returned it all at once after the PHP code was fullyexecuted.
Usage example
API changes
php.runStream(request: PHPRequest): StreamedPHPResponsephp.cli()from integer exit code toStreamedPHPResponseImplementation
php.js registers three FS devices at:
They are private to every PHP instance and are never shared with other runtimes.
Then, in JavaScript, whenever a chunk of data is written to either of these devices, we propagate
it to consumer via a callback, e.g.
PHPWASM.onStdout(chunk).Consumers of the
PHPclass never have to interact with these devices or callbacks directly.The PHP class creates the relevant ReadableStreams and pushes the data through them
– see php.#executeWithErrorHandling() for details.
Why not use Emscripten's stdout and stderr?
Emscripten's native stdout and stderr devices stop processing data when they encounter
the first null byte. However, null bytes are common when dealing with binary data.
Backwards Compatibility
php.cli()now returns aStreamedPHPResponseinstance and not an integer exit code.php.run()continues to work as before. Internally, it now creates a streamed response and buffers the output before returning aPHPResponseobject.Follow-up work