Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/publish-crate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- 'windows-thread-ambient-sys-v*'
- 'windows-threadpool-sys-v*'
- 'windows-topology-sys-v*'
- 'windows-waitable-queues-v*'
- 'wtf-string-v*'
# Manual escape hatch for a tag whose commit cannot publish. `cargo publish
# --locked` refuses when Cargo.lock disagrees with the manifests, and the
Expand All @@ -38,6 +39,7 @@ on:
- windows-thread-ambient-sys
- windows-threadpool-sys
- windows-topology-sys
- windows-waitable-queues
- wtf-string

permissions:
Expand Down Expand Up @@ -107,7 +109,7 @@ jobs:
- name: Wait for workspace-sibling dependencies on crates.io
shell: bash
run: |
workspace_crates="windows-file-enumeration-sys windows-file-watcher windows-file-watcher-example-test-harness windows-impersonation-token-sys windows-ioring-sys windows-namespace-request-sys windows-overlapped-io-sys windows-thread-ambient-sys windows-threadpool-sys windows-topology-sys wtf-string"
workspace_crates="windows-file-enumeration-sys windows-file-watcher windows-file-watcher-example-test-harness windows-impersonation-token-sys windows-ioring-sys windows-namespace-request-sys windows-overlapped-io-sys windows-thread-ambient-sys windows-threadpool-sys windows-topology-sys windows-waitable-queues wtf-string"
metadata="$(cargo metadata --no-deps --format-version 1)"
# `tr -d '\r'` is load-bearing on the Windows runner: jq.exe writes
# CRLF, and `read` splits on LF alone, so without this the last field
Expand Down
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"crates/windows-thread-ambient-sys": "0.2.0",
"crates/windows-threadpool-sys": "0.1.3",
"crates/windows-topology-sys": "0.1.0",
"crates/windows-waitable-queues": "0.0.1",
"crates/wtf-string": "0.1.0"
}
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"crates/windows-thread-ambient-sys",
"crates/windows-threadpool-sys",
"crates/windows-topology-sys",
"crates/windows-waitable-queues",
"crates/wtf-string",
]
resolver = "2"
Expand Down
78 changes: 78 additions & 0 deletions crates/windows-waitable-queues/COMPLETED-CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Completed checklists: windows-waitable-queues

Append-only. Newest groups at the bottom.

## Moved 2026-09-05 -- public API surface, closed against eight published queue crates

# Checklist: public API surface

Closes the gaps found by comparing this crate's public surface against the eight
most-depended-on Rust queue crates: `crossbeam-channel`, `crossbeam-queue`,
`flume`, `thingbuf`, `ringbuf`, `rtrb`, `concurrent-queue`, and
`std::sync::mpsc`.

Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md).

## What the comparison established

Of seventeen capabilities present in three or more of those eight, this crate
already has nine, under the majority spelling in every case (`len`, `capacity`,
`is_empty`, `remaining`). The items below are the gaps that a reader coming from
any of those crates would notice. Deliberately **not** queued, because the
comparison showed them to be one- or two-crate features rather than
expectations: an explicit `close()` operation (1/8), `peek` (2/8, and no MPMC
crate offers it), bulk and slice transfers (2/8, both single-producer designs),
async (2/8), `force_push` (2/8), `sender_count` (1/8), and weak handles (1/8).

**These are breaking changes, and that is why they are queued now.** The crate is
unpublished at `0.0.1`, so the cost of making them is zero and it will not be
zero again.

## M1: the surface

- [x] **API-1** -- Make `pop` distinguish an empty queue from a departed
producer, by returning `Result<T, TryRecvError>` with `Empty` and
`Disconnected` variants rather than `Option<T>`.

**The strongest signal is internal, not comparative.** `push` already
distinguishes `PushError::Full` from `PushError::Disconnected`, and `recv`
already returns `RecvError::Disconnected`; only `pop` collapses the two. The
crate has the vocabulary and one method declines to use it.

Five of the eight crates distinguish these. The three that do not --
`crossbeam-queue`, `rtrb`, `ringbuf` -- have **no handles and no
disconnection concept at all**, so they have nothing to distinguish. This
crate is channel-shaped: handles, `Drop`-based disconnection, and
`is_disconnected` on both sides. The shape implies the expectation.

It also removes a protocol the caller is currently asked to remember. The
`Consumer` trait documents: *"Ask only after `pop` has returned `None`.
Draining to empty and then finding the producers gone is the only order that
cannot lose an item."* That is a `TryRecvError` written as prose, and prose
cannot be enforced.

- [x] **API-2** -- Put `is_full` on the `Bounded` trait, so it is reachable
generically and from a consumer.

Every concrete `Producer` answers `is_full` as an inherent method, but the
trait does not declare it, so generic code over `Bounded` cannot ask and no
`Consumer` offers it. The two surfaces disagree about which questions exist:
`remaining` is on the trait and therefore works on every consumer, yet is
spelled out inherently only on `reserving_mpsc`'s.

Six of the eight crates offer `is_full`. Give it a default implementation in
terms of `remaining`, so no shape has to restate it, and add the inherent
method to each `Consumer` for the same reason the other accessors are
inherent: a caller should not need an import to ask.

- [x] **API-3** -- Add `try_iter` and `IntoIterator` on the consumers, keeping
`drain` as the name that describes the semantics.

`drain` is exactly `try_iter` -- take what is there, stop at the first empty
-- but `drain` is the one-crate spelling (flume) while `try_iter` is the
four-crate one, and `IntoIterator` on the receiving handle is four-crate as
well. Neither costs anything to add.

It is also trait-only today, so `rx.drain()` does not compile without
`use windows_waitable_queues::Consumer`. Make the iterator reachable from the
concrete types.
5 changes: 5 additions & 0 deletions crates/windows-waitable-queues/COMPLETED-PLANS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Completed plans: windows-waitable-queues

| Path to CHECKLIST.md | Completion Date | Brief description | Design Notes |
|---|---|---|---|
| [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md) | 2026-09-05 | Closed the public-surface gaps found by comparing this crate against the eight most-depended-on Rust queue crates: `pop` now distinguishes an empty queue from a departed producer, `is_full` is on the `Bounded` trait and both handles, and `try_iter`/`drain` are inherent. | [DESIGN-NOTES.md](DESIGN-NOTES.md) |
81 changes: 81 additions & 0 deletions crates/windows-waitable-queues/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright (c) 2026 Mike Grier

[package]
name = "windows-waitable-queues"
# `0.0.x` on purpose: this crate has never been published, so this is a starting
# point rather than a record of a release. With `bump-minor-pre-major`, the six
# breaking commits behind it make the first published version `0.1.0` -- which is
# what a first release should look like. Leaving it at `0.1.0` would have made
# that first release `0.2.0`, skipping `0.1.0` entirely. Not `0.0.0`, which
# release-please special-cases into a jump to `1.0.0`.
version = "0.0.1" # x-release-please-version
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation = "https://docs.rs/windows-waitable-queues"
description = "Bounded producer/consumer queues whose readiness is a waitable Windows HANDLE, so a consumer can park on a queue and a kernel object in the same wait."
readme = "README.md"
keywords = ["windows", "queue", "spsc", "mpsc", "concurrency"]
categories = ["concurrency", "os::windows-apis", "data-structures"]

# Deliberately publishable, unlike `windows-guard-alloc` next door: this is a
# general-purpose facility whose value is that it exists, and its first consumer
# (the I/O domain runtime) is not its only plausible one. Publishing is
# therefore an obligation accepted, not an oversight -- see DESIGN-NOTES.md D-8
# for what it commits us to.
publish = true

# The crate is Windows-only -- every public item is behind `cfg(windows)` and
# the implementation imports `std::os::windows::io` unconditionally -- so
# docs.rs must build it on a Windows target or the build fails outright.
[package.metadata.docs.rs]
default-target = "x86_64-pc-windows-msvc"
targets = ["x86_64-pc-windows-msvc"]

[features]
# An experimental claim protocol, measured against the shipping one by
# `probe-queue-contention` so that SH-14.3 can be decided on evidence. Not
# covered by this crate's semver promise; it will either be merged into
# `reserving_mpsc` or deleted (SH-15.6). Non-default so nothing depends on it
# by accident.
experimental-permit-claim = []

# A 128-bit claim word for `reserving_mpsc`, adding the `Wide` layout.
#
# **The only thing in this crate that costs a third-party dependency, which is
# why it is a feature rather than always present.** Rust's standard library has
# no 128-bit atomic -- `core::sync::atomic` stops at 64 bits -- so a
# double-width compare-and-swap needs `portable-atomic`. Without this feature
# the crate depends on `windows-sys` alone and every layout uses `AtomicU64`.
#
# `default-features = false` on the dependency is load-bearing: with its
# defaults, `portable-atomic` silently substitutes a global lock where the
# native instruction is unavailable, which would put a mutex in the claim path
# while still compiling. With them off, `AtomicU128` does not exist on such a
# target and the build fails naming it.
#
# Most callers should not need this. `Perpetual` reaches roughly twenty years
# before its claim position recurs, on a plain `AtomicU64` at no measured cost,
# whereas the 128-bit exchange measured 2-3x slower on the claim itself. See
# `ClaimLayout` for the comparison.
dwcas = ["dep:portable-atomic"]

[lib]
path = "src/lib.rs"

[dependencies.portable-atomic]
version = "1.15.0"
default-features = false
optional = true

[dependencies.windows-sys]
version = "0.61.2"
default-features = false
features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_Threading",
]
Loading