From 5949f14842f5fa6f7e04c7e0805c998c25324580 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:41:30 +0000 Subject: [PATCH 1/2] async: a vectored write the caller can await MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vectored write is fire-and-forget in both of its modes: the completion sends no notification and the release callback carries no status. A caller that needs the outcome therefore has to use the single-buffer write, which does not take the buffer over — so it must free it itself, and the only moment it can is when its own wait ends. Those are different moments once the waiting coroutine is cancelled: the wait is over and the write is not. ZEND_ASYNC_IO_WRITEV_AWAIT says the caller waits. Ownership stays the mode's; the completion keeps the request alive and notifies io->event with the request as the result and no exception, so a caller matching by request pointer wakes and no sibling awaiter on the same handle does. Header only: the flag word already existed, so no signature changes and the existing values keep their meaning. --- Zend/zend_async_API.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Zend/zend_async_API.h b/Zend/zend_async_API.h index 181dde1fd36a..f07761c6b78d 100644 --- a/Zend/zend_async_API.h +++ b/Zend/zend_async_API.h @@ -609,6 +609,15 @@ typedef zend_async_io_req_t *(*zend_async_io_write_t)(zend_async_io_t *io, const * entry per the mode's contract). */ #define ZEND_ASYNC_IO_WRITEV_ZSTR 0u #define ZEND_ASYNC_IO_WRITEV_IOV 1u +/* The mode is the low bit of the flag word; the bits above it carry behaviour. */ +#define ZEND_ASYNC_IO_WRITEV_MODE_MASK 1u +/* Await this write instead of forgetting it. Ownership of the buffers is the + * mode's, unchanged. What changes is the completion: the reactor keeps the + * request alive and notifies io->event with the request as the result and no + * exception, so a caller filtering by request pointer wakes and no sibling + * awaiter on the same handle does. The status is on req->transferred and + * req->exception, and the caller disposes the request exactly once. */ +#define ZEND_ASYNC_IO_WRITEV_AWAIT (1u << 1) typedef zend_async_io_req_t *(*zend_async_io_writev_t)(zend_async_io_t *io, const void *bufs, unsigned nbufs, uint32_t flags, zend_async_io_write_free_cb_t free_cb, void *user_data); @@ -2930,6 +2939,13 @@ END_EXTERN_C() #define ZEND_ASYNC_IO_WRITEV(io, bufs, nbufs) \ zend_async_io_writev_fn((io), (const void *)(bufs), (nbufs), \ ZEND_ASYNC_IO_WRITEV_ZSTR, NULL, NULL) +/* Vectored write the caller awaits. `bufs` is a zend_string ** and the reactor + * takes one reference per slot, so the buffers outlive a caller that is + * cancelled while parked — the reason this exists. Returns NULL when nothing + * was submitted, with the references already released unless nbufs was 0. */ +#define ZEND_ASYNC_IO_WRITEV_AWAITED(io, bufs, nbufs) \ + zend_async_io_writev_fn((io), (const void *)(bufs), (nbufs), \ + ZEND_ASYNC_IO_WRITEV_ZSTR | ZEND_ASYNC_IO_WRITEV_AWAIT, NULL, NULL) /* Fire-and-forget vectored write — plain-iovec mode. `iov` is an array * of (base, len) zend_async_buf_t pointing into caller memory; reactor * calls free_cb(user_data, io) once on completion or submit failure. From 1fddaf3cdec7a530654bb72feb9d4125a2726879 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:59:13 +0000 Subject: [PATCH 2/2] async: tighten the two comments --- Zend/zend_async_API.h | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Zend/zend_async_API.h b/Zend/zend_async_API.h index f07761c6b78d..ea843a675bdc 100644 --- a/Zend/zend_async_API.h +++ b/Zend/zend_async_API.h @@ -611,12 +611,10 @@ typedef zend_async_io_req_t *(*zend_async_io_write_t)(zend_async_io_t *io, const #define ZEND_ASYNC_IO_WRITEV_IOV 1u /* The mode is the low bit of the flag word; the bits above it carry behaviour. */ #define ZEND_ASYNC_IO_WRITEV_MODE_MASK 1u -/* Await this write instead of forgetting it. Ownership of the buffers is the - * mode's, unchanged. What changes is the completion: the reactor keeps the - * request alive and notifies io->event with the request as the result and no - * exception, so a caller filtering by request pointer wakes and no sibling - * awaiter on the same handle does. The status is on req->transferred and - * req->exception, and the caller disposes the request exactly once. */ +/* Await this write. Buffer ownership stays the mode's; only the completion + * differs — the request survives it and io->event is notified with the request + * as the result and no exception, so siblings on the handle stay asleep. Status + * is on req->transferred / req->exception; the caller disposes once. */ #define ZEND_ASYNC_IO_WRITEV_AWAIT (1u << 1) typedef zend_async_io_req_t *(*zend_async_io_writev_t)(zend_async_io_t *io, const void *bufs, unsigned nbufs, uint32_t flags, @@ -2939,10 +2937,10 @@ END_EXTERN_C() #define ZEND_ASYNC_IO_WRITEV(io, bufs, nbufs) \ zend_async_io_writev_fn((io), (const void *)(bufs), (nbufs), \ ZEND_ASYNC_IO_WRITEV_ZSTR, NULL, NULL) -/* Vectored write the caller awaits. `bufs` is a zend_string ** and the reactor - * takes one reference per slot, so the buffers outlive a caller that is - * cancelled while parked — the reason this exists. Returns NULL when nothing - * was submitted, with the references already released unless nbufs was 0. */ +/* Vectored write the caller awaits. `bufs` is a zend_string **; the reactor + * takes one reference per slot, so the buffers outlive a caller cancelled while + * parked. NULL means nothing was submitted — references already released, + * unless nbufs was 0. */ #define ZEND_ASYNC_IO_WRITEV_AWAITED(io, bufs, nbufs) \ zend_async_io_writev_fn((io), (const void *)(bufs), (nbufs), \ ZEND_ASYNC_IO_WRITEV_ZSTR | ZEND_ASYNC_IO_WRITEV_AWAIT, NULL, NULL)