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
13 changes: 11 additions & 2 deletions doc/STYLE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ The Antora pipeline provides two mechanisms; use them instead of hand-authoring:
launder real code past B2. `doc-lint.mjs`'s SHAPE check runs a content heuristic over every
role=output/role=figure block and reports (non-gated, advisory) any whose content looks
like code, so a wrong tag is not silently permanent — see the check's header comment.
A `[mermaid]` block is diagram source for the Mermaid extension, so B2 and SHAPE skip it
too; like role=output/role=figure, it never clears a `[source,*]` block. Prefer one to a
hand-drawn `[role=figure]`, and include `partial$diagram-style.adoc` once on its page.
Note `role=` on a bare listing emits a real CSS class (`class="listingblock output"` /
`...figure`) with no stylesheet rule behind it today; a future UI bundle that styles
`.output`/`.figure` will change how every such block renders, project-wide, in one step.
Expand Down Expand Up @@ -219,6 +222,12 @@ it only in the sense the row names. **scheduler** is approved only in its P2300
- **D4. Document thread-safety *and* executor affinity** at the class level where relevant.
- **D5. No unexplained qualifiers.** Hedges like "even on X" or "where available" either get
explained or get cut.
- **D6. Examples follow the coroutine lifetime rules** of `4.guide/4t.lifetime.adoc`. A
coroutine an example starts with `run_async` takes by value whatever it reads after the
caller's scope may have ended, which for a lazy task includes reads before its first
`co_await`. It takes by reference only what outlives `run()`: the `io_context`, or an
object `main` owns for the whole program. A child coroutine awaited in the same expression
may take references.

## Part E — Presentation & Tooling

Expand Down Expand Up @@ -261,7 +270,7 @@ Not every rule is machine-checkable. Each rule sits in one of three tiers:
|---|---|
| **Gate** | A1, A6, A7, B2, B3, B6, C2, C4, C9, C10, D2 |
| **Warning** | A2, B1, C1, C3, C5, C6, D4, D5, E1 |
| **Review** | A3, A4, A5, B4, B5, C7, C8, C11, D1, D3, E2, E3, E4 |
| **Review** | A3, A4, A5, B4, B5, C7, C8, C11, D1, D3, D6, E2, E3, E4 |

The accuracy gates (B2, B3, D2 correctness) are enforced by the snippet-compile job, not by
Vale — that job is what makes examples unable to drift.
Expand Down Expand Up @@ -330,7 +339,7 @@ max: 25
compiled file (Part B2).
- **F3.** Doc PR checklist: mode declared (A1)? no hand-typed signatures (B1)? example
compiled (B2)? namespace alias, not a using-directive (B5)? terminology clean (`vale`)?
rationale present (D3)?
rationale present (D3)? examples follow the lifetime rules (D6)?

### F4 — A check is not adopted until a planted violation has failed it

Expand Down
11 changes: 11 additions & 0 deletions doc/lint/doc-lint.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
// `[quote,...]`) — a first cut that merged any `[...]`-shaped line
// made one of those, sitting directly above an exempt
// `[source,...,role=pseudocode]` block, defeat that exemption.
// A `[mermaid]` block (diagram source, not code) is exempt from B2
// and SHAPE both, unless it is also `[source,...]`.
// SHAPE — advisory only, NEVER gated (not in the summary the CI gate
// spec reads by rule prefix). A role=output/role=figure block is a
// permanent B2 exemption, so a block wrongly marked non-code would
Expand Down Expand Up @@ -207,6 +209,15 @@ function scanBlocks(lines, ch) {
// on a [source,*] block.
if (isSource && hasClearingRole) continue;

// A [mermaid] block is diagram source for the mermaid Antora extension:
// neither B2 nor SHAPE applies (sequence-diagram messages routinely say
// `co_await`, which SHAPE matches). Code pasted into one cannot hide, as
// mermaid fails to parse it and the page shows an error box. The style
// line sits directly above the walked [source,...]/[role=...] stack, and
// the exemption is isSource-gated for the same reason as role=output.
const isMermaid = a >= 0 && /^\[mermaid\b/i.test(lines[a].trim());
if (!isSource && isMermaid) continue;

if (!isSource && hasNonCodeRole) {
// Bare listing explicitly marked as program output / a figure: not a
// B2 candidate, but SHAPE still looks at its content (advisory).
Expand Down
37 changes: 37 additions & 0 deletions doc/lint/selftest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,31 @@ try {
'',
].join('\n'));

// A `[mermaid]` block is diagram source rendered by the mermaid Antora
// extension, not code: B2 and SHAPE must both leave it alone (a sequence
// diagram's messages routinely say `co_await`, which SHAPE matches).
// The exemption must not reach a block that is ALSO `[source,...]`, or
// a `[mermaid]` line stacked above a real C++ block would clear it.
// Line numbers are asserted below, so keep them stable.
write('mermaid.adoc', [
':page-mode: how-to', // 1
'', // 2
'= Mermaid', // 3
'', // 4
'[mermaid]', // 5
'----', // 6
'sequenceDiagram', // 7
' A->>B: co_await child()', // 8
'----', // 9
'', // 10
'[mermaid]', // 11
'[source,cpp]', // 12 <- must fire
'----', // 13
'int w = 4;', // 14
'----', // 15
'',
].join('\n'));

// ANCHOR fixture. Line numbers are asserted below, so keep them stable:
// 3 = the prose violation, 5-8 = the in-block negative, 10 = the
// passthrough negative.
Expand Down Expand Up @@ -512,6 +537,18 @@ try {
anchorHits.length === 0,
`anchor-above-pseudocode.adoc should have 0 B2 findings, got: ${JSON.stringify(anchorHits)}`);

const mermaidB2 = dOut.findings.B2.filter((f) => f.file === 'mermaid.adoc');
check('B2 leaves a [mermaid] diagram block alone',
!mermaidB2.some((f) => f.line === 5 || f.line === 6),
`mermaid.adoc:5 is a diagram, not code; got: ${JSON.stringify(mermaidB2)}`);
check('a [mermaid] line stacked above [source,cpp] does not clear the C++ block',
mermaidB2.length === 1 && mermaidB2[0].line === 12,
`expected exactly 1 mermaid.adoc B2 finding at line 12, got: ${JSON.stringify(mermaidB2)}`);
const mermaidShape = dOut.findings.SHAPE.filter((f) => f.file === 'mermaid.adoc');
check('SHAPE leaves a [mermaid] diagram block alone even when a message says co_await',
mermaidShape.length === 0,
`mermaid.adoc should have 0 SHAPE findings, got: ${JSON.stringify(mermaidShape)}`);

// ANCHOR: a C++ attribute in a prose code span. Asciidoctor's
// inline-anchor substitution runs inside a backtick span, so
// `` `[[nodiscard]]` `` renders as an EMPTY <code> -- the attribute
Expand Down
5 changes: 5 additions & 0 deletions doc/local-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ antora:
- require: '@antora/lunr-extension' # https://gitlab.com/antora/antora-lunr-extension
index_latest_only: true
- require: '@antora/collector-extension'
- require: '@sntke/antora-mermaid-extension'
mermaid_library_url: https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs
script_stem: header-scripts
mermaid_initialize_options:
start_on_load: true
- require: '@cppalliance/antora-cpp-tagfiles-extension'
cpp-tagfiles:
using-namespaces:
Expand Down
4 changes: 3 additions & 1 deletion doc/modules/ROOT/pages/2.networking-tutorial/2g.udp.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ A UDP datagram can theoretically be up to 65,535 bytes -- UDP's own 16-bit lengt

When a UDP datagram exceeds the path MTU, IP fragments it into smaller pieces. These fragments travel independently through the network. If every fragment arrives, the destination reassembles the original datagram and delivers it to your application. If *any* fragment is lost, the entire datagram is discarded. Your application receives nothing -- not even the fragments that did arrive.

Losing a fragment is not the only way to lose the datagram. A receiver is only required to accept a reassembled datagram of up to 576 bytes for IPv4 (RFC 791) or 1500 bytes for IPv6 (RFC 8200). Both sizes include the IP header, so they leave at most 548 bytes of UDP payload for IPv4 and 1452 for IPv6. A receiver may silently discard a larger fragmented datagram, and many firewalls drop fragments outright. A fragmented datagram may therefore never arrive, even on a network that loses nothing. Fragmentation is a correctness hazard, not just a performance cost.

On a typical internet path with a 1500-byte MTU, the practical limit for a UDP datagram you can send without risking fragmentation is about 1472 bytes (1500 minus the 20-byte IP header and 8-byte UDP header). Many applications choose an even smaller limit to account for paths with lower MTUs or encapsulation overhead from VPNs and tunnels.

The takeaway: keep UDP datagrams small. If your message does not fit in a single unfragmented packet, you either need to split it yourself or consider whether TCP is a better fit.
The takeaway: keep UDP datagrams small enough that they are never fragmented. If your message does not fit in a single unfragmented packet, you either need to split it yourself or consider whether TCP is a better fit.

== No Connection, No State

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
= Opening and Closing TCP Connections
:page-mode: explanation

include::partial$diagram-style.adoc[]

A TCP connection is not a physical wire. It is an agreement between two machines to track a shared conversation. That agreement begins with a handshake, ends with a teardown, and passes through a series of well-defined states in between. Knowing these states explains errors you encounter in every networked application you write.

== The Three-Way Handshake
Expand All @@ -20,17 +22,18 @@ Before any data flows, TCP establishes the connection with three segments:
. *SYN-ACK*: The server responds with both the SYN and ACK flags set. It acknowledges the client's ISN (by setting the acknowledgment number to the client's ISN + 1) and provides its own ISN. This says "I accept, I acknowledge your starting number, and my byte numbering starts here."
. *ACK*: The client sends a final acknowledgment of the server's ISN. At this point both sides have agreed on initial sequence numbers, and the connection is established.

[role=figure]
[mermaid]
----
Client Server
| |
|--- SYN (seq=100) ----------------->|
| |
|<-- SYN-ACK (seq=300, ack=101) -----|
| |
|--- ACK (ack=301) ----------------->|
| |
| Connection established |
sequenceDiagram
autonumber
accTitle: The three-way handshake
accDescr: The client sends a SYN with sequence number 100. The server answers with a SYN-ACK that has sequence number 300 and acknowledges 101. The client sends an ACK that acknowledges 301, and the connection is established.
participant C as Client
participant S as Server
C->>S: SYN (seq=100)
S->>C: SYN-ACK (seq=300, ack=101)
C->>S: ACK (ack=301)
Note over C,S: Connection established
----

Why three steps instead of two? Both sides need to agree on two things: the client's ISN and the server's ISN. A two-step handshake would let the server accept without the client confirming the server's ISN. The third step completes the exchange.
Expand All @@ -54,17 +57,19 @@ Closing a TCP connection takes four segments because each direction of the strea
. *FIN*: The other side sends its own FIN when it, too, has finished sending.
. *ACK*: The first side acknowledges the second FIN.

[role=figure]
[mermaid]
----
Client Server
| |
|--- FIN --------------------------->|
|<-- ACK ----------------------------|
| |
|<-- FIN ----------------------------|
|--- ACK --------------------------->|
| |
| Connection closed |
sequenceDiagram
autonumber
accTitle: Graceful teardown
accDescr: The client sends a FIN, and the server acknowledges it. Then the server sends its own FIN, and the client acknowledges it. The connection is closed.
participant C as Client
participant S as Server
C->>S: FIN
S->>C: ACK
S->>C: FIN
C->>S: ACK
Note over C,S: Connection closed
----

In practice, the server's ACK and FIN are often combined into a single segment, reducing the exchange to three segments. But logically, the four steps reflect two independent half-closes.
Expand Down Expand Up @@ -95,12 +100,33 @@ FIN_WAIT_1:: This side has sent a FIN and is waiting for an acknowledgment.

FIN_WAIT_2:: The other side acknowledged the FIN, but has not yet sent its own FIN. This side is waiting for the remote FIN.

CLOSING:: Both sides sent a FIN at about the same time. This side has received the remote FIN and is waiting for the acknowledgment of its own.

CLOSE_WAIT:: This side has received a FIN from the remote end but has not yet sent its own FIN. If your server has many connections in CLOSE_WAIT, it means the application is not closing sockets promptly after the remote end has disconnected.

LAST_ACK:: This side has sent its FIN (after receiving the remote FIN) and is waiting for the final ACK.

TIME_WAIT:: The connection is fully closed, but the socket lingers for a period (typically 60 seconds to 2 minutes) before returning to CLOSED. This is the state that causes the most confusion.

Closing takes one of two paths, depending on which side sends the first FIN. The side that closes first moves through FIN_WAIT_1 and FIN_WAIT_2 to TIME_WAIT. The other side moves through CLOSE_WAIT and LAST_ACK straight to CLOSED. If both sides close at the same time, each goes from FIN_WAIT_1 through CLOSING to TIME_WAIT. When the peer's ACK and FIN arrive in one segment, the side that closes first skips FIN_WAIT_2 and goes straight from FIN_WAIT_1 to TIME_WAIT. Each arrow names what moves the connection on: the application calling `close()`, or the segment that arrives.

[mermaid]
----
stateDiagram-v2
accTitle: The two ways a TCP connection closes
accDescr: From ESTABLISHED, the side that calls close() goes to FIN_WAIT_1, and the side that receives a FIN goes to CLOSE_WAIT. From FIN_WAIT_1, an ACK leads to FIN_WAIT_2, a FIN leads to CLOSING, and an ACK and FIN in one segment lead to TIME_WAIT. FIN_WAIT_2 goes to TIME_WAIT on a FIN, and CLOSING goes to TIME_WAIT on an ACK. TIME_WAIT goes to CLOSED after the 2MSL timeout. CLOSE_WAIT goes to LAST_ACK when the application calls close(), and LAST_ACK goes to CLOSED on an ACK.
ESTABLISHED --> FIN_WAIT_1: close()
ESTABLISHED --> CLOSE_WAIT: FIN
FIN_WAIT_1 --> FIN_WAIT_2: ACK
FIN_WAIT_1 --> CLOSING: FIN
FIN_WAIT_1 --> TIME_WAIT: ACK and FIN
FIN_WAIT_2 --> TIME_WAIT: FIN
CLOSING --> TIME_WAIT: ACK
TIME_WAIT --> CLOSED: 2MSL timeout
CLOSE_WAIT --> LAST_ACK: close()
LAST_ACK --> CLOSED: ACK
----

== TIME_WAIT

After both sides have exchanged FINs and ACKs, you might expect the connection to disappear immediately. Instead, the side that initiated the close enters TIME_WAIT and holds the socket open for a period called 2MSL (twice the Maximum Segment Lifetime, typically 60 seconds).
Expand Down
23 changes: 15 additions & 8 deletions doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ inline blocks the event loop and starves every other connection.

The solution is to keep I/O on the cpp:io_context[] and offload heavy computation
to a `thread_pool`. Capy's `run()` function makes this seamless. A single
`co_await` switches the coroutine to the pool, runs the work, and resumes
back on the original executor when it finishes.
`co_await` runs the work on the pool and resumes the coroutine on the original
executor when the work finishes.

This tutorial demonstrates:

Expand All @@ -55,9 +55,8 @@ work justifies leaving the event loop.
include::example$hash-server/hash_server.cpp[tag=hash_function]
----

This is a `capy::task` — a lazy coroutine that doesn't start until someone
awaits it. That matters because `run()` needs to control which executor the
task runs on.
This is a plain function. It knows nothing about coroutines or executors, so
any code can reuse it. The session decides where it runs.

== Session Coroutine

Expand All @@ -72,9 +71,9 @@ Three things happen in sequence, but on two different executors:

1. **Read** — runs on the cpp:io_context[] thread. The socket awaitable suspends
the coroutine until data arrives from the kernel.
2. **Hash** — `capy::run( pool.get_executor() )` posts `compute_fnv1a` to the
thread pool. The coroutine suspends on the cpp:io_context[] and resumes on a
pool thread. When the task completes, a trampoline posts the
2. **Hash** — `capy::run( pool.get_executor() )` posts a task that calls
`compute_fnv1a` to the thread pool. The coroutine stays suspended while a
pool thread runs the task. When the task completes, a trampoline posts the
coroutine back to the cpp:io_context[].
3. **Write** — back on the cpp:io_context[] thread, the hex result is sent to the
client.
Expand All @@ -91,6 +90,14 @@ When you write:
include::example$hash-server/hash_server.cpp[tag=run_switch,indent=0]
----

`run()` takes a task, so a coroutine lambda wraps the plain function. The
lambda gets `buf` and `n` as parameters, not captures. The lambda object lives
until the end of the full expression, which includes the `co_await`, so
captures would also be safe here. Parameters are still the habit the
xref:../4.guide/4t.lifetime.adoc[lifetime rules] teach, because they live in
the coroutine frame rather than in the lambda object. `buf` stays valid
because `do_session` stays suspended until the hash completes.

Behind the scenes:

1. `run()` creates an awaitable that stores the pool executor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,9 @@ include::example$snippets/4b_concurrent_programming.cpp[tag=dangling_reference,i
----

A coroutine may outlive the scope that started it. Ensure captured data lives
long enough.
long enough. Take by value whatever a started coroutine reads after it may be
stored; xref:4.guide/4t.lifetime.adoc[Lifetime and Teardown] gives the full
rules.

=== Cross-Executor Access

Expand Down
12 changes: 12 additions & 0 deletions doc/modules/ROOT/pages/4.guide/4c.io-context.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ The cpp:io_context[]:
* Provides an executor for coroutine dispatch
* Tracks outstanding work to know when to stop

== Why the Context Also Runs Coroutines

The cpp:io_context[] both waits for I/O and runs the coroutines that wait on
it. Coroutines could instead run elsewhere, such as on a thread pool, and call
into the context only to read or write. Every completion would then cost a
hand-off: queue the coroutine for another thread and wake that thread.

Keeping both in one place removes that hand-off. An operation often completes
on a thread that runs the context its awaiting coroutine belongs to. The
executor's `dispatch` then resumes the coroutine inline on that thread, with no
queue or wakeup.

== Construction

=== Default Construction
Expand Down
1 change: 1 addition & 0 deletions doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ include::example$snippets/4e_tcp_acceptor.cpp[tag=accept_loop]

Key points:

* The loop owns the acceptor, so the acceptor lives as long as the loop
* Create a fresh socket for each accept
* Move the socket into the handler coroutine
* Continue accepting after non-fatal errors
Expand Down
Loading
Loading