Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

PHP: stream stdout and stderr via StreamedPHPResponse #2266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
adamziel merged 5 commits into trunk from push-lzqtsuspwwoy
Jun 16, 2025

Conversation

@adamziel
Copy link
Collaborator

@adamziel adamziel commented Jun 12, 2025
edited
Loading

Implements a php.runStream() method that returns a StreamedPHPResponse instance:

export class StreamedPHPResponse {
	/**
	 * Resolves once HTTP status code is available.
	 */
	httpStatusCode: Promise<number>;
	/**
	 * Resolves once HTTP headers are available.
	 */
	headers: Promise<Record<string, string[]>>;
	/**
	 * Exposes the stdout bytes as they're produced by the PHP instance
	 */
	stdoutText: Promise<string>;
	/**
	 * Exposes the stderr bytes as they're produced by the PHP instance
	 */
	stderrText: Promise<string>;
	/**
	 * Resolves when the response has finished processing – either successfully or not.
	 */
	finished(): Promise<void>;
	/**
	 * True if the response is successful (HTTP status code 200-399),
	 * false otherwise.
	 */
	async ok(): Promise<boolean>;
}

It exposes stdout and stderr as ReadableStream-s, 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.

Usage example

const streamed = await php.runStream({
 code: `<?php 
 echo "first chunk";
 sleep(1);
 echo "second chunk";
`,
});
const reader = streamed.stdout.getReader();
const decoder = new TextDecoder();
// Read the first chunk
console.log( await reader.read() );
// "first chunk"
// Wait about a second until the second chunk is available
console.log( await reader.read() );
// "second chunk"

API changes

  • Adds a new method: php.runStream(request: PHPRequest): StreamedPHPResponse
  • Changes the return type of php.cli() from integer exit code to StreamedPHPResponse

Implementation

php.js registers 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).

Consumers 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.cli() now returns a StreamedPHPResponse instance 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 a PHPResponse object.

Follow-up work

  • Stream the response bytes in the web/service worker

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
@adamziel adamziel changed the title (削除) PHP: Stream stdout and stderr (削除ここまで) (追記) PHP: StreamedPHPResponse that streams stdout and stderr (追記ここまで) Jun 16, 2025
@adamziel adamziel changed the title (削除) PHP: StreamedPHPResponse that streams stdout and stderr (削除ここまで) (追記) PHP: stream stdout and stderr via StreamedPHPResponse (追記ここまで) Jun 16, 2025
@adamziel adamziel merged commit 04791f7 into trunk Jun 16, 2025
22 of 24 checks passed
@adamziel adamziel deleted the push-lzqtsuspwwoy branch June 16, 2025 09:12
Copy link
Member

Cool work, @adamziel!

adamziel reacted with hooray emoji

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 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Projects

No open projects
Status: Inbox

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /