From 372420a9d078e0c347cbbe6c530ae05c0c7bd981 Mon Sep 17 00:00:00 2001 From: Pedram Rezaei Date: Mon, 14 Sep 2026 15:23:34 -0700 Subject: [PATCH 1/4] Fingerprint website assets so deployments refresh cached styles and scripts --- src/website/README.md | 2 ++ src/website/build.py | 17 +++++++++++++++++ src/website/validate.py | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/src/website/README.md b/src/website/README.md index cbfa9c2..e54cfce 100644 --- a/src/website/README.md +++ b/src/website/README.md @@ -20,3 +20,5 @@ The header and footer use the official blue wordmarks from [cloudtoid/assets](ht Run `python3 src/website/validate.py` after building to check local links, anchors, metadata, and sitemap coverage. CI runs this check. All reference text and navigation work without JavaScript; `docs.js` enhances code blocks with highlighting and copy buttons. The homepage's language guide links point to these pages. The social preview uses `assets/social-card.png`; its editable SVG source is alongside it. SEO metadata uses `https://cloudtoid.com` as the canonical origin. Publishing makes the sitemap available at `/sitemap.xml`; search-engine indexing happens independently of deployment. + +CSS and JavaScript URLs include a content hash at build time, so browsers fetch changed assets after each deployment. Cloudflare build-watch includes must use `src/website/*` (a single wildcard also matches nested paths); GitHub Actions uses its own `src/website/**` glob. diff --git a/src/website/build.py b/src/website/build.py index 534afae..a1ddb57 100644 --- a/src/website/build.py +++ b/src/website/build.py @@ -1,5 +1,6 @@ """Build the static marketing site and developer reference, without dependencies.""" from html import escape +from hashlib import sha256 import json from pathlib import Path import re @@ -91,4 +92,20 @@ def page_path(page): + '\n' + '\n'.join(f'{url}' for url in urls) + '\n\n') (output / 'robots.txt').write_text('User-agent: *\nAllow: /\n\nSitemap: ' + base + '/sitemap.xml\n') +# Content-addressed assets prevent browsers reusing scripts/styles from an older deploy. +assets = {} +for page in output.rglob('*.html'): + def fingerprint(match): + url = match[2] + if "://" in url or url.startswith("//"): + return match[0] + source = output / url.lstrip('/') + if url not in assets: + digest = sha256(source.read_bytes()).hexdigest()[:12] + target = source.with_name(f'{source.stem}.{digest}{source.suffix}') + shutil.copyfile(source, target) + assets[url] = '/' + target.relative_to(output).as_posix() + return f'{match[1]}="{assets[url]}"' + page.write_text(re.sub(r'(src|href)="([^"?]+\.(?:css|js))"', fingerprint, page.read_text())) + print(f'Built {output}: homepage and {len(pages)} documentation pages') diff --git a/src/website/validate.py b/src/website/validate.py index 2fecdd8..5035aa1 100644 --- a/src/website/validate.py +++ b/src/website/validate.py @@ -1,5 +1,6 @@ """Check rendered page metadata, structured data, local links, and fragment targets.""" from html.parser import HTMLParser +from hashlib import sha256 import json from pathlib import Path from urllib.parse import urljoin, urlparse, unquote @@ -79,6 +80,9 @@ def handle_endtag(self, tag): if local.is_dir(): local /= 'index.html' assert local.is_file(), f'{path}: broken local link {link}' + if local.suffix in ('.css', '.js'): + digest = sha256(local.read_bytes()).hexdigest()[:12] + assert f'.{digest}{local.suffix}' in local.name, f'{path}: unversioned asset {link}' if target.fragment and local in pages: assert unquote(target.fragment) in pages[local].ids, f'{path}: missing anchor {link}' From 58e6a28fec2a668d48f86da7a30c6ed15e0bfe7c Mon Sep 17 00:00:00 2001 From: Pedram Rezaei Date: Mon, 14 Sep 2026 15:41:10 -0700 Subject: [PATCH 2/4] Link package READMEs to website docs and remove duplicated guides --- README.md | 224 ++----------------------------------------- docs/protocol.md | 2 + docs/releasing.md | 4 +- src/c/README.md | 24 ++--- src/dotnet/README.md | 16 +--- src/go/README.md | 32 ++----- src/node/README.md | 32 +------ src/python/README.md | 34 +------ src/rust/README.md | 40 +------- 9 files changed, 44 insertions(+), 364 deletions(-) diff --git a/README.md b/README.md index 3e052b0..727f8fd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Interprocess -[Website](https://cloudtoid.com) · [Languages and packages](#languages) · [Quick start](#quick-start) · [Performance](#performance) · [Protocol v3](docs/protocol.md) +[Website](https://cloudtoid.com) · [Documentation](https://cloudtoid.com/docs/) · [Languages and packages](#languages) · [Quick start](#quick-start) · [Performance](#performance) · [Protocol v3](docs/protocol.md) [![NuGet](https://img.shields.io/nuget/v/Cloudtoid.Interprocess?label=NuGet)](https://www.nuget.org/packages/Cloudtoid.Interprocess) [![Rust](https://img.shields.io/crates/v/cloudtoid-interprocess?label=Rust)](https://crates.io/crates/cloudtoid-interprocess) @@ -31,211 +31,20 @@ Interprocess is used internally by Microsoft. | Language | Package | Setup and API guide | | --- | --- | --- | -| Rust | [`cloudtoid-interprocess`](https://crates.io/crates/cloudtoid-interprocess) (Cargo) | [Rust core](src/rust/README.md) | -| C / C++ | [C SDK](https://github.com/cloudtoid/interprocess/releases/latest); [`cloudtoid-interprocess-ffi`](https://crates.io/crates/cloudtoid-interprocess-ffi) (Cargo) | [C ABI, headers, and shared library](src/c/README.md) | -| Python | Source build (PyPI pending); import `cloudtoid_interprocess` | [Python 3.9+](src/python/README.md) | -| Node.js | [`@cloudtoid/interprocess`](https://www.npmjs.com/package/@cloudtoid/interprocess) (npm) | [Node.js 18+, JavaScript and TypeScript](src/node/README.md) | -| Go | [`github.com/cloudtoid/interprocess/src/go/v3`](https://pkg.go.dev/github.com/cloudtoid/interprocess/src/go/v3) | [Go 1.24+, cgo, and the C SDK](src/go/README.md) | -| .NET | [`Cloudtoid.Interprocess`][NuGet] (NuGet) | [.NET 10+, C# and dependency injection](src/dotnet/README.md) | +| Rust | [`cloudtoid-interprocess`](https://crates.io/crates/cloudtoid-interprocess) (Cargo) | [Rust core](https://cloudtoid.com/docs/rust/) | +| C / C++ | [C SDK](https://github.com/cloudtoid/interprocess/releases/latest); [`cloudtoid-interprocess-ffi`](https://crates.io/crates/cloudtoid-interprocess-ffi) (Cargo) | [C ABI, headers, and shared library](https://cloudtoid.com/docs/c/) | +| Python | Source build (PyPI pending); import `cloudtoid_interprocess` | [Python 3.9+](https://cloudtoid.com/docs/python/) | +| Node.js | [`@cloudtoid/interprocess`](https://www.npmjs.com/package/@cloudtoid/interprocess) (npm) | [Node.js 18+, JavaScript and TypeScript](https://cloudtoid.com/docs/node/) | +| Go | [`github.com/cloudtoid/interprocess/src/go/v3`](https://pkg.go.dev/github.com/cloudtoid/interprocess/src/go/v3) | [Go 1.24+, cgo, and the C SDK](https://cloudtoid.com/docs/go/) | +| .NET | [`Cloudtoid.Interprocess`][NuGet] (NuGet) | [.NET 10+, C# and dependency injection](https://cloudtoid.com/docs/dotnet/) | Rust supplies the native engine; C, Python, Node.js, and Go use that engine. .NET has its own managed implementation of the same protocol. Node's platform binaries are companion `@cloudtoid/interprocess-*` packages; applications use the main package. ## Quick start -These examples send and receive in one process so both endpoints remain connected. In separate programs, use the same queue name, capacity, and shared directory, and keep at least one endpoint connected throughout the handoff. Each message goes to one subscriber. +Choose a language in the table above for installation commands, a working example, and its API reference. Start with [queue concepts](https://cloudtoid.com/docs/concepts/) when connecting separate processes or mixing languages. -
-Rust - -Run in your Cargo project. Requires Rust 1.87 or later. - -```sh -cargo add cloudtoid-interprocess -``` - -```rust -use cloudtoid_interprocess::{Options, Publisher, Subscriber}; - -let options = Options::new("example", 65536); -let subscriber = Subscriber::open(&options)?; -let publisher = Publisher::open(&options)?; -publisher.try_send(b"hello")?; -let message = subscriber.try_recv()?; -println!("{message:?}"); -``` - -Endpoints close when dropped. Use `recv()` to wait indefinitely or `recv_timeout(Duration)` for a bounded wait. - -
- -
-C / C++ - -macOS Apple Silicon example, using the GitHub CLI. For other platforms, choose darwin-x64, linux-arm64, linux-x64, or win32-x64 in both archive names. See the C guide for Windows setup. - -```sh -gh release download native-v3.0.1 --repo cloudtoid/interprocess --pattern "*-darwin-arm64.tar.gz" -mkdir -p cloudtoid-sdk -tar -xzf cloudtoid-interprocess-3.0.1-darwin-arm64.tar.gz -C cloudtoid-sdk --strip-components=1 -export PKG_CONFIG_PATH="$PWD/cloudtoid-sdk/lib/pkgconfig:$PKG_CONFIG_PATH" -``` - -[Complete C SDK setup](src/c/README.md). - -```c -#include - -int main(void) { - cip_subscriber *subscriber = NULL; - cip_publisher *publisher = NULL; - if (cip_subscriber_open("example", NULL, 65536, &subscriber) != 1) - return 1; - if (cip_publisher_open("example", NULL, 65536, &publisher) != 1) { - cip_subscriber_close(subscriber); - return 1; - } - if (cip_try_send(publisher, (const uint8_t *)"hello", 5) == 1) { - cip_buffer message; - if (cip_receive(subscriber, 1000, &message) == 1) - cip_buffer_free(message); - } - cip_publisher_close(publisher); - cip_subscriber_close(subscriber); - return 0; -} -``` - -Timeouts are milliseconds; free returned buffers with `cip_buffer_free`. - -
- -
-Python - -PyPI publishing is pending. Run in an activated Python 3.9+ virtual environment with Git, Rust, and a native linker installed. - -```sh -python -m pip install "git+https://github.com/cloudtoid/interprocess.git@native-v3.0.1#subdirectory=src/python" -``` - -```python -from cloudtoid_interprocess import Publisher, Subscriber - -with Subscriber("example", 65536) as subscriber, Publisher("example", 65536) as publisher: - if publisher.try_send(b"hello"): - print(subscriber.receive(timeout=1.0)) -``` - -Context managers close endpoints. Timeouts are seconds; `receive()` waits indefinitely and permits Python signal handling. - -
- -
-Node.js / TypeScript - -Run in your Node.js project. Requires Node.js 18 or later; platform binaries install automatically. - -```sh -npm install @cloudtoid/interprocess -``` - -```js -import { Publisher, Subscriber } from '@cloudtoid/interprocess'; - -const subscriber = new Subscriber('example', 65536); -const publisher = new Publisher('example', 65536); -try { - if (publisher.trySend(Buffer.from('hello'))) { - const message = await subscriber.receive({ - signal: AbortSignal.timeout(1000) - }); - console.log(message.toString()); - } -} finally { - publisher.close(); - subscriber.close(); -} -``` - -CommonJS `require` is also supported. Async receive accepts an optional `AbortSignal`; empty queues wait on a one-millisecond timer without occupying libuv workers. - -
- -
-Go - -Install the C SDK first ([C guide](src/c/README.md)), then run in your Go module. Requires Go 1.24+, cgo enabled, a C compiler, and pkg-config. - -```sh -go get github.com/cloudtoid/interprocess/src/go/v3@latest -``` - -```go -package main - -import ( - "context" - "fmt" - "github.com/cloudtoid/interprocess/src/go/v3" - "time" -) - -func main() { - options := interprocess.Options{Name: "example", Capacity: 65536} - subscriber, err := interprocess.OpenSubscriber(options) - if err != nil { - panic(err) - } - defer subscriber.Close() - publisher, err := interprocess.OpenPublisher(options) - if err != nil { - panic(err) - } - defer publisher.Close() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - if sent, err := publisher.TrySend([]byte("hello")); err != nil { - panic(err) - } else if sent { - message, err := subscriber.Receive(ctx) - if err != nil { - panic(err) - } - fmt.Println(string(message)) - } -} -``` - -Use `context.Context` for cancellation and deadlines. Install the C SDK before building the Go package. - -
- -
-.NET / C# - -```sh -dotnet add package Cloudtoid.Interprocess -``` - -```csharp -using Cloudtoid.Interprocess; - -var factory = new QueueFactory(); -var options = new QueueOptions("example", 65536); -using var subscriber = factory.CreateSubscriber(options); -using var publisher = factory.CreatePublisher(options); -using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(1)); - -if (publisher.TryEnqueue("hello"u8)) { - var message = subscriber.Dequeue(cancellation.Token); - Console.WriteLine(System.Text.Encoding.UTF8.GetString(message.Span)); -} -``` - -Blocking receives accept a `CancellationToken`. For dependency injection, register `services.AddInterprocessQueue()` and resolve `IQueueFactory`. See the [.NET package guide](src/dotnet/README.md) and [publisher/subscriber samples](src/dotnet/Sample/). - -
+Queues are transient: keep at least one publisher or subscriber connected throughout the handoff. Once all endpoints are gone, unread messages are lost and reopening the queue starts fresh. ## Faster with v3 @@ -251,15 +60,6 @@ Measured with .NET 10. [Original results and comparison harness](https://github. Upgrade existing v1/v2 applications together: drain the queue, stop all participants, and reopen a fresh queue using v3. All participants sharing a queue must use the same protocol. -## Queue behavior - -- Use the same name, capacity, and storage path in every participant. Queue names must be unique even across different paths; Windows uses the name and ignores the path. -- Each queue supports **2,048 connected publisher objects**. Slots are reused after disposal or confirmed process exit. The publisher table adds **256 KiB** plus 256 bytes of header/alignment storage; `Capacity` is the message-buffer size. -- On Windows, use the same user session for all participants. Cross-account connections are not supported. -- The queue is transient and remains available while any publisher or subscriber is connected. Once all are gone, unread messages are lost. Reopening the same name creates a fresh, empty queue. Keep a subscriber connected before a short-lived publisher exits. Dispose participants when finished. -- A paused live participant keeps ownership. Recovery can reclaim abandoned work after a process exits, but may discard queued messages, including completed messages behind an unfinished reservation. This is an IPC queue, not durable storage. -- Supply a destination buffer large enough for the message. A smaller buffer consumes the message and returns only the bytes that fit. - ## Performance Send means enqueue; receive means dequeue. A send + receive operation includes both operations; a send-only measurement excludes receiving. All benchmarks keep publishers and subscribers connected throughout measurement, with queue creation and cleanup outside the timed work. @@ -326,12 +126,6 @@ Same .NET workloads and allocation conventions as the Mac suite. Two launches an [Protocol v3](docs/protocol.md) documents the complete shared-memory format and synchronization rules. [Interoperability tests](tests/interop/README.md) exercise every publisher/subscriber language pair and mixed-language concurrent delivery across participant crashes. -## Implementation Notes - -Messages travel through a shared, circular memory-mapped buffer. Coalesced notifications reduce operating-system calls while keeping blocked subscribers responsive. Rust and .NET blocking readers also retry after five-millisecond waits when notifications are missed; bindings follow their documented waiting and cancellation behavior. Cross-process wakeups use named semaphores, with POSIX implementations on [Linux](src/dotnet/Interprocess/Semaphore/Linux/Interop.cs) and [macOS](src/dotnet/Interprocess/Semaphore/MacOS/Interop.cs). - -Positions advance monotonically while the physical buffer wraps. Before the queue reaches `INT64_MAX` bytes reserved or `INT32_MAX` participant registrations over its lifetime, drain it and move all participants to a fresh queue. - ## How to Contribute - Create a branch from `main`. diff --git a/docs/protocol.md b/docs/protocol.md index dd94cae..d0b287f 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -1,5 +1,7 @@ # Cloudtoid Interprocess protocol v3 +[Documentation website](https://cloudtoid.com/docs/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) + This document specifies the shared-memory protocol used by the .NET implementation and the Rust core behind the C, Python, Node.js, and Go packages. Package API versions and the protocol namespace are separate: an API release can remain compatible with v3. ## Scope and guarantees diff --git a/docs/releasing.md b/docs/releasing.md index 9eaff1f..04398fe 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,5 +1,7 @@ # Building and releasing the language packages +[Documentation website](https://cloudtoid.com/docs/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) + The .NET library stays independent under `src/dotnet`, with its own NuGet publishing job in the shared release workflow. Native packages share one Rust workspace version. Their public APIs wrap the same protocol v3 core; changing a package version does not automatically change the protocol. ## Build files @@ -41,4 +43,4 @@ Use the **Registry to publish** selector to retry just one registry after a work Binary targets: Linux x64 and ARM64 (glibc 2.34+), macOS ARM64 and x64, Windows x64. The x64 Mac release is cross-built; it does not add a permanent Intel Mac CI runner. Rust/C source builds support the core's platform/architecture restrictions. Python includes a source distribution. Go links the installed C SDK; its module does not silently download binaries. -The Website workflow validates the static build. Publish the public website at cloudtoid.com through its existing Sites hosting project. Advertise actual package availability and measured performance; do not label in-process microbenchmarks as application-to-application latency. +The Website workflow validates the static build. Cloudflare Pages automatically publishes website changes merged into main to cloudtoid.com. See [website build and deployment](../src/website/README.md). Advertise actual package availability and measured performance; do not label in-process microbenchmarks as application-to-application latency. diff --git a/src/c/README.md b/src/c/README.md index 9ed566b..57068d5 100644 --- a/src/c/README.md +++ b/src/c/README.md @@ -1,10 +1,12 @@ # Cloudtoid Interprocess C SDK +[API guide](https://cloudtoid.com/docs/c/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) · [Website](https://cloudtoid.com) + The C ABI exposes the shared Rust engine to C/C++ and other native callers. It interoperates with .NET protocol v3. ## Install the prebuilt SDK -macOS Apple Silicon example, using the GitHub CLI. For other platforms, choose darwin-x64, linux-arm64, linux-x64, or win32-x64 in both archive names. See the C guide for Windows setup. +macOS Apple Silicon example, using the GitHub CLI. For other platforms, choose darwin-x64, linux-arm64, linux-x64, or win32-x64 in both archive names. See the [C guide](https://cloudtoid.com/docs/c/) for Windows setup. ```sh gh release download native-v3.0.1 --repo cloudtoid/interprocess --pattern "*-darwin-arm64.tar.gz" @@ -27,6 +29,8 @@ cmake --install target/c-sdk --config Release Link with `pkg-config --cflags --libs cloudtoid-interprocess`. On Unix, these flags embed the installed library directory as a runtime search path. Windows callers use the DLL and import library; add the DLL directory to `PATH` or place the DLL beside the executable. The C header documents status codes, timeout units, ownership, buffer truncation, and concurrent-close requirements. +## Example + ```c #include cip_subscriber *subscriber = NULL; @@ -47,20 +51,6 @@ if (cip_publisher_open("example", NULL, 65536, &publisher) == CIP_OK) { cip_subscriber_close(subscriber); ``` -`cip_receive` returns owned bytes; free successful results with `cip_buffer_free`. Finish every call before closing its handle. See [protocol v3](https://github.com/cloudtoid/interprocess/blob/main/docs/protocol.md). - -## Errors - -Use `CIP_OK`, `CIP_UNAVAILABLE`, and `CIP_ERROR` to interpret status results. On error, `cip_last_error_kind()` gives a stable `cip_error_kind`; `cip_last_error()` provides diagnostic text on the same thread. The prebuilt SDK contains shared libraries. Static linking is source-build only (`cargo build --release -p cloudtoid-interprocess-ffi`); define `CIP_STATIC` when using the resulting static library on Windows. - -## Waiting and shutdown - -Handles must not be closed concurrently with an operation. Use bounded `cip_receive` timeouts when shutdown is needed. Open handles after `fork()`; do not use inherited handles in the child. Closing an inherited handle leaves the parent's registration intact. - -## Limits - -Queue names must be nonempty and contain no slash or NUL. Windows also rejects backslashes; Unix permits them for compatibility. The maximum is 24 UTF-8 bytes on macOS and 245 on Linux; use at most 24 bytes for portable names. - -## Queue lifetime +`cip_receive` returns owned bytes; free successful results with `cip_buffer_free`. Finish every call before closing its handle. -The queue is transient: it stays alive while at least one publisher or subscriber is connected. Once all endpoints are closed or their processes exit, unread messages are lost. Opening the same name again creates a fresh, empty queue. Keep a subscriber connected before a short-lived publisher exits; a surviving publisher also keeps the queue alive. +Queues are transient: once all publishers and subscribers are gone, unread messages are lost. Keep at least one endpoint connected throughout a handoff between processes. See the [API guide](https://cloudtoid.com/docs/c/) for waiting, errors, ownership, and limits. diff --git a/src/dotnet/README.md b/src/dotnet/README.md index f24b5d7..36293ba 100644 --- a/src/dotnet/README.md +++ b/src/dotnet/README.md @@ -1,5 +1,7 @@ # Cloudtoid.Interprocess +[API guide](https://cloudtoid.com/docs/dotnet/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) · [Website](https://cloudtoid.com) + Exchange byte messages between processes on the same machine using a shared-memory queue. Multiple publishers and subscribers can connect to the same queue on Windows, Linux, or macOS. ## Install @@ -39,16 +41,6 @@ if (publisher.TryEnqueue(payload) && For separate processes, create the publisher and subscriber in their respective programs using the same queue name, storage path, and capacity. The example uses the default temporary directory. Handle unsuccessful enqueue/dequeue attempts according to your application's retry and cancellation needs. -Each queue supports up to 2,048 connected publisher objects. Its shared publisher table uses 256 KiB in addition to the message capacity and header/alignment storage. Dispose participants when finished; the queue remains available while any participant is connected. - -For dependency injection, register the queue services with `services.AddInterprocessQueue()` and resolve `IQueueFactory`. - -## Limits - -Queue names must be nonempty and contain no slash or NUL. Windows also rejects backslashes; Unix permits them for compatibility. The maximum is 24 UTF-8 bytes on macOS and 245 on Linux; use at most 24 bytes for portable names. - -## Queue lifetime - -The queue is transient IPC storage. Once all publishers and subscribers are gone, unread messages are lost; reopening the same name creates a fresh, empty queue. Keep a subscriber connected before a short-lived publisher exits. Recovery after a process exits can discard queued messages, including completed messages behind an unfinished reservation. Paused live operations are not reclaimed merely because a timeout passes. Destination buffers must be large enough to avoid truncating a consumed message. +For dependency injection, register `services.AddInterprocessQueue()` and resolve `IQueueFactory`. -[Publisher and subscriber samples](https://github.com/cloudtoid/interprocess/tree/main/src/dotnet/Sample) · [Documentation](https://github.com/cloudtoid/interprocess) · [Report an issue](https://github.com/cloudtoid/interprocess/issues) · [MIT license](https://github.com/cloudtoid/interprocess/blob/main/LICENSE) \ No newline at end of file +Queues are transient: once all publishers and subscribers are gone, unread messages are lost. Keep at least one endpoint connected throughout a handoff between processes. See the [API guide](https://cloudtoid.com/docs/dotnet/) for waiting, errors, ownership, and limits. diff --git a/src/go/README.md b/src/go/README.md index db13599..a59d290 100644 --- a/src/go/README.md +++ b/src/go/README.md @@ -1,8 +1,10 @@ # Cloudtoid Interprocess for Go +[API guide](https://cloudtoid.com/docs/go/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) · [Website](https://cloudtoid.com) + Shared-memory byte queues backed by the same Rust core as the C, Python, and Node.js packages. Fully interoperable with .NET v3. -Install the [C SDK](../c/README.md), make its `pkgconfig` directory available through `PKG_CONFIG_PATH`, and, on Windows, add its DLL directory to `PATH`. On Unix, pkg-config embeds the installed library directory as a runtime search path. This package requires cgo, a C compiler, and `pkg-config`. +Install the [C SDK](https://cloudtoid.com/docs/c/), make its `pkgconfig` directory available through `PKG_CONFIG_PATH`, and, on Windows, add its DLL directory to `PATH`. On Unix, pkg-config embeds the installed library directory as a runtime search path. This package requires cgo, a C compiler, and `pkg-config`. ## Install @@ -28,31 +30,9 @@ if sent, err := publisher.TrySend([]byte("hello")); err != nil { panic(err) } else if sent { message, err := subscriber.TryReceive() - _ = message - _ = err + if err != nil { panic(err) } + println(string(message)) } ``` -`TrySend` reports full/recovery without waiting. `TryReceive` returns nil when empty; empty messages return non-nil empty slices. `TryReceiveInto` reuses caller storage and truncates/consumes messages that do not fit. - -## Errors - -Use `errors.Is` with `ErrCapacityMismatch`, `ErrPublisherLimit`, `ErrInvalidArgument`, `ErrExhausted`, `ErrCorrupt`, `ErrIO`, or `ErrClosed` to handle failures. - -## Waiting and cancellation - -`Receive(ctx)` accepts a `context.Context` and returns `ctx.Err()` on cancellation or deadline expiry. Use `context.Background()` to wait indefinitely, or `context.WithTimeout` for a deadline. Ready messages return immediately; otherwise Go timers back off through 1, 2, 4, 8, and 10 ms between nonblocking attempts, with context cancellation interrupting the timer. Each new receive checks immediately and resets the backoff. Idle-to-active delivery can incur that interval plus OS scheduling delay. - -Always close endpoints; do not copy them. Concurrent calls are supported. `Close` waits for the current native call, and outstanding receives return `ErrClosed`. - -Each pending `Receive` has its own timer and native checks while idle. Prefer one receive loop per subscriber and distribute work after receiving when practical. Backoff limits this idle CPU cost without holding OS threads in cgo. - -## Limits - -All participants must agree on name, capacity, and Unix path. This is volatile IPC with process crash recovery, not durable storage or broadcast. See [protocol v3](https://github.com/cloudtoid/interprocess/blob/main/docs/protocol.md). - -Queue names must be nonempty and contain no slash or NUL. Windows also rejects backslashes; Unix permits them for compatibility. The maximum is 24 UTF-8 bytes on macOS and 245 on Linux; use at most 24 bytes for portable names. - -## Queue lifetime - -The queue is transient: it stays alive while at least one publisher or subscriber is connected. Once all endpoints are closed or their processes exit, unread messages are lost. Opening the same name again creates a fresh, empty queue. Keep a subscriber connected before a short-lived publisher exits; a surviving publisher also keeps the queue alive. +Queues are transient: once all publishers and subscribers are gone, unread messages are lost. Keep at least one endpoint connected throughout a handoff between processes. See the [API guide](https://cloudtoid.com/docs/go/) for waiting, errors, ownership, and limits. diff --git a/src/node/README.md b/src/node/README.md index ecfd8cd..dc468d6 100644 --- a/src/node/README.md +++ b/src/node/README.md @@ -1,5 +1,7 @@ # Cloudtoid Interprocess for Node.js +[API guide](https://cloudtoid.com/docs/node/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) · [Website](https://cloudtoid.com) + Exchange bytes directly with Rust, C, Python, Go, and .NET processes through fast shared-memory queues. ## Install @@ -28,32 +30,8 @@ try { ES modules can use `import { Publisher, Subscriber } from '@cloudtoid/interprocess'`. CommonJS `require` and TypeScript declarations expose the same classes. -`trySend` returns false when full or recovering. `trySendBatch` returns the accepted prefix length. `tryReceive` returns a Buffer or null; an empty Buffer is a real message. - -Send methods accept `Uint8Array` (including `Buffer`). - -Build from this monorepo with `node build.js`. See the [protocol specification](https://github.com/cloudtoid/interprocess/blob/main/docs/protocol.md). - -## Errors - -Errors expose `code`, including `ERR_INVALID_ARGUMENT`, `ERR_CAPACITY_MISMATCH`, `ERR_PUBLISHER_LIMIT`, `ERR_EXHAUSTED`, `ERR_CORRUPT`, `ERR_IO`, and `ERR_CLOSED`. Endpoints support `Symbol.dispose` as an alias for `close`. - -## Waiting and cancellation - -`await subscriber.receive()` waits for a message. Pass `{ signal }` to cancel, or use `subscriber.receive({ signal: AbortSignal.timeout(1000) })` for a one-second deadline. Cancellation rejects with the signal's reason. `close()` releases the endpoint and makes pending receives reject. - -Ready messages use the nonblocking path immediately. Empty queues back off through 1, 2, 4, 8, and 10 ms waits without occupying libuv workers. Each new receive checks immediately and resets the backoff. Idle-to-active delivery can incur up to the 10 ms polling interval, and a busy event loop can delay timers and cancellation. Each pending receive has its own timer; prefer one receive loop per subscriber and use `AbortSignal` to stop unused receives. - -## Limits - -Every participant must agree on name, capacity, and Unix path (optional third constructor argument). Capacity is bytes, excludes metadata, exceeds 16, and is divisible by 8. Subscribers compete for messages. Queues are volatile, with recovery for crashed participants; they do not provide durable delivery. - -Queue names must be nonempty and contain no slash or NUL. Windows also rejects backslashes; Unix permits them for compatibility. The maximum is 24 UTF-8 bytes on macOS and 245 on Linux; use at most 24 bytes for portable names. - -Batch sends return the committed prefix length. A short count, including zero, can mean a full queue, recovery, or a mid-batch error. Retry the unsent suffix to observe a persistent error; errors before any commit are raised immediately. - -TypeScript declarations require TypeScript 5.2 or newer for `Symbol.dispose`; JavaScript users do not need TypeScript. +## Build from source -## Queue lifetime +From this directory, run `node build.js`. -The queue is transient: it stays alive while at least one publisher or subscriber is connected. Once all endpoints are closed or their processes exit, unread messages are lost. Opening the same name again creates a fresh, empty queue. Keep a subscriber connected before a short-lived publisher exits; a surviving publisher also keeps the queue alive. +Queues are transient: once all publishers and subscribers are gone, unread messages are lost. Keep at least one endpoint connected throughout a handoff between processes. See the [API guide](https://cloudtoid.com/docs/node/) for waiting, errors, ownership, and limits. diff --git a/src/python/README.md b/src/python/README.md index c2e3a5e..b5783d0 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -1,5 +1,7 @@ # Cloudtoid Interprocess for Python +[API guide](https://cloudtoid.com/docs/python/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) · [Website](https://cloudtoid.com) + Fast shared-memory byte queues. Exchange messages directly with Rust, C, Go, Node.js, and .NET processes using the same v3 queue. ## Install @@ -20,34 +22,8 @@ with Subscriber("example", 65536) as subscriber, Publisher("example", 65536) as print(subscriber.receive(timeout=1.0)) ``` -`try_send` returns false when full or recovering. `try_send_batch` returns the committed prefix length. `try_receive` returns bytes or `None`; empty bytes are a message. Use context managers or `close()` to release registrations promptly. - -`try_send` and `try_send_batch` accept buffer objects such as `bytes`, `bytearray`, and `memoryview`. `bytes` uses the direct path; other buffers are copied into a snapshot before sending. `path` also accepts `os.PathLike` objects. - -The prebuilt wheel supports standard CPython 3.9 and later. Free-threaded Python is not covered by the ABI3 wheel and requires a source build. Type hints are included. - -Build from this repository with `maturin develop --release` in this directory. See [protocol v3](https://github.com/cloudtoid/interprocess/blob/main/docs/protocol.md) for layout, memory ordering, resource lifetime, and crash recovery. - -## Errors - -Queue-specific exceptions inherit from `InterprocessError`, so callers can catch them together. Invalid arguments and closed endpoints raise `ValueError`; capacity mismatches raise `CapacityMismatchError`, publisher limits raise `PublisherLimitError`, corrupt records raise `CorruptQueueError`, and OS failures raise `OSError`. - -## Waiting and cancellation - -`receive(timeout=None)` waits indefinitely, releases the GIL while waiting, and checks Python signals periodically. Timeouts are seconds. - -Closing a publisher during a Python buffer-export callback is safe; the interrupted send raises `ValueError`. Closing a subscriber from another thread or a signal handler interrupts an idle receive within its 100 ms wait slice. For asyncio, use `await asyncio.to_thread(subscriber.receive, timeout=...)` with a finite timeout: cancelling an asyncio task does not stop its worker thread. Open endpoints after `fork()` and do not use inherited endpoints in the child; closing inherited endpoints does not release the parent's registrations. - -## Limits - -Every participant must agree on name, capacity, and Unix path. Pass `path="/shared/directory"` when different runtimes have different temp directories. Capacity is bytes, excludes metadata, exceeds 16, and is divisible by 8. - -Queue names must be nonempty and contain no slash or NUL. Windows also rejects backslashes; Unix permits them for compatibility. The maximum is 24 UTF-8 bytes on macOS and 245 on Linux; use at most 24 bytes for portable names. - -Batch sends return the committed prefix length. A short count, including zero, can mean a full queue, recovery, or a mid-batch error. Retry the unsent suffix to observe a persistent error; errors before any commit are raised immediately. - -## Queue lifetime +## Build from source -The queue is transient: it stays alive while at least one publisher or subscriber is connected. Once all endpoints are closed or their processes exit, unread messages are lost. Opening the same name again creates a fresh, empty queue. Keep a subscriber connected before a short-lived publisher exits; a surviving publisher also keeps the queue alive. +In an activated virtual environment, run `maturin develop --release` from this directory. -Subscribers compete for messages. Recovery after a process crash can discard queued messages; this is transient IPC, not broadcast or durable storage. +Queues are transient: once all publishers and subscribers are gone, unread messages are lost. Keep at least one endpoint connected throughout a handoff between processes. See the [API guide](https://cloudtoid.com/docs/python/) for waiting, errors, ownership, and limits. diff --git a/src/rust/README.md b/src/rust/README.md index e9c9bec..c192d92 100644 --- a/src/rust/README.md +++ b/src/rust/README.md @@ -1,5 +1,7 @@ # Cloudtoid Interprocess for Rust +[API guide](https://cloudtoid.com/docs/rust/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) · [Website](https://cloudtoid.com) + Fast shared-memory byte queues for processes on the same machine. Exchange messages with Rust, C, Python, Node.js, Go, and .NET using the open v3 protocol. ## Install @@ -24,40 +26,4 @@ fn main() -> Result<(), cloudtoid_interprocess::Error> { } ``` -Reuse receive storage with `try_recv_into`; it truncates and consumes oversized messages, matching .NET. `try_send_batch` amortizes admission across a prefix of messages. Use `recv_timeout(duration)` for a bounded wait (`None` means timeout), or `recv()` to wait indefinitely and return a message. Process crashes do not destroy queues with surviving participants. - -## Errors - -`try_send` returns `Error::Full` when space or recovery admission is unavailable; retry according to your application's deadline. Other error variants distinguish invalid options, capacity mismatch, publisher limits, exhausted counters, corrupt records, and I/O failures. A notification failure does not turn a committed send into an error. - -For an explicit retry loop, distinguish a full queue from other failures and apply your application's deadline: - -```rust -use cloudtoid_interprocess::{Publisher, Result}; - -fn send(publisher: &Publisher) -> Result<()> { - while let Err(error) = publisher.try_send(b"hello") { - if !error.is_full() { return Err(error); } - std::thread::yield_now(); - } - Ok(()) -} -``` - -## Waiting and cancellation - -`recv` and `recv_timeout` block the calling thread. In an async runtime, use a blocking worker (for example Tokio's `spawn_blocking`) with bounded waits so it can shut down. Open endpoints after `fork()`; inherited endpoints must not be used in the child. Dropping an inherited endpoint does not release the parent's registration. - -## Limits - -Supports little-endian 64-bit Linux, macOS, and Windows. Use an explicit shared path on Unix if runtime temp directories differ. Every participant must use the same name and capacity. Capacity is bytes, excludes metadata, exceeds 16, and is divisible by 8. - -Queue names must be nonempty and contain no slash or NUL. Windows also rejects backslashes; Unix permits them for compatibility. The maximum is 24 UTF-8 bytes on macOS and 245 on Linux; use at most 24 bytes for portable names. - -Batch sends return the committed prefix length. A short count, including zero, can mean a full queue, recovery, or a mid-batch error. Retry the unsent suffix to observe a persistent error; errors before any commit are raised immediately. - -## Queue lifetime - -The queue is transient: it stays alive while at least one publisher or subscriber is connected. Once all endpoints are closed or their processes exit, unread messages are lost. Opening the same name again creates a fresh, empty queue. Keep a subscriber connected before a short-lived publisher exits; a surviving publisher also keeps the queue alive. - -Publishers reserve with native 64-bit atomics. Readers serialize consumption. A paused live participant retains ownership; abandoned work is recovered only after checking process liveness. Queues are volatile and messages can be discarded during crash recovery. See the [v3 protocol specification](https://github.com/cloudtoid/interprocess/blob/main/docs/protocol.md) for the complete contract. +Queues are transient: once all publishers and subscribers are gone, unread messages are lost. Keep at least one endpoint connected throughout a handoff between processes. See the [API guide](https://cloudtoid.com/docs/rust/) for waiting, errors, ownership, and limits. From dbf9cc19f268a94ee1a96a0986cb047f1f587313 Mon Sep 17 00:00:00 2001 From: Pedram Rezaei Date: Mon, 14 Sep 2026 15:44:35 -0700 Subject: [PATCH 3/4] Move protocol specification to website with interactive queue illustration --- .github/workflows/website.yml | 1 + README.md | 4 +- docs/protocol.md | 165 ------------------- src/python/pyproject.toml | 2 +- src/website/README.md | 2 + src/website/build.py | 2 +- src/website/docs.css | 29 +++- src/website/docs/concepts.html | 2 +- src/website/docs/pages.json | 62 ++++++- src/website/docs/protocol.html | 291 +++++++++++++++++++++++++++++++++ src/website/docs/template.html | 2 +- src/website/index.html | 2 +- src/website/protocol.js | 85 ++++++++++ 13 files changed, 467 insertions(+), 182 deletions(-) delete mode 100644 docs/protocol.md create mode 100644 src/website/docs/protocol.html create mode 100644 src/website/protocol.js diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 72e2413..7ff189b 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -22,3 +22,4 @@ jobs: node --check src/website/site.js node --check src/website/theme.js node --check src/website/docs.js + node --check src/website/protocol.js diff --git a/README.md b/README.md index 727f8fd..fec5e40 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Interprocess -[Website](https://cloudtoid.com) · [Documentation](https://cloudtoid.com/docs/) · [Languages and packages](#languages) · [Quick start](#quick-start) · [Performance](#performance) · [Protocol v3](docs/protocol.md) +[Website](https://cloudtoid.com) · [Documentation](https://cloudtoid.com/docs/) · [Languages and packages](#languages) · [Quick start](#quick-start) · [Performance](#performance) · [Protocol v3](https://cloudtoid.com/docs/protocol/) [![NuGet](https://img.shields.io/nuget/v/Cloudtoid.Interprocess?label=NuGet)](https://www.nuget.org/packages/Cloudtoid.Interprocess) [![Rust](https://img.shields.io/crates/v/cloudtoid-interprocess?label=Rust)](https://crates.io/crates/cloudtoid-interprocess) @@ -124,7 +124,7 @@ Measured September 13, 2026, on an **Apple M5 Max**, Ubuntu 24.04 ARM64 VM (Lima Same .NET workloads and allocation conventions as the Mac suite. Two launches and eight measured iterations; single-thread runs used one pinned vCPU and 20 warmups (200 for send-only), while concurrent runs used all four vCPUs and three warmups. [.NET benchmark source](src/dotnet/Interprocess.Benchmark/) · [Original reports](https://github.com/cloudtoid/interprocess/tree/95c512672d580dd836ba2554cd1f78c0c3826f6c/docs/benchmarks/2026-09-13). -[Protocol v3](docs/protocol.md) documents the complete shared-memory format and synchronization rules. [Interoperability tests](tests/interop/README.md) exercise every publisher/subscriber language pair and mixed-language concurrent delivery across participant crashes. +[Protocol v3](https://cloudtoid.com/docs/protocol/) documents the complete shared-memory format and synchronization rules. [Interoperability tests](tests/interop/README.md) exercise every publisher/subscriber language pair and mixed-language concurrent delivery across participant crashes. ## How to Contribute diff --git a/docs/protocol.md b/docs/protocol.md deleted file mode 100644 index d0b287f..0000000 --- a/docs/protocol.md +++ /dev/null @@ -1,165 +0,0 @@ -# Cloudtoid Interprocess protocol v3 - -[Documentation website](https://cloudtoid.com/docs/) · [Queue concepts](https://cloudtoid.com/docs/concepts/) - -This document specifies the shared-memory protocol used by the .NET implementation and the Rust core behind the C, Python, Node.js, and Go packages. Package API versions and the protocol namespace are separate: an API release can remain compatible with v3. - -## Scope and guarantees - -A queue connects processes on one machine. Publishers reserve space concurrently. Subscribers compete for messages; one shared reader owner serializes consumption. Delivery follows reservation order, even if a later reservation becomes ready first. Concurrent calls from one publisher have no defined relative order. Successful sequential calls preserve order. An accepted batch is a prefix of the input, not a transaction; other publishers can interleave reservations. - -Ordinary operation delivers each message to one subscriber. This is volatile IPC, not durable storage, broadcast, or an acknowledged work queue. A reader can die after copying a message but before advancing the queue. Recovery can discard an abandoned reservation and completed messages behind it. Applications that need durable processing or exactly-once effects must implement those guarantees separately. - -The enqueue reservation path uses native atomics, without a per-message OS mutex. The entire queue is not formally lock-free: consumption has a reader ownership word, an indefinitely paused live reader can block readers, and recovery waits for admitted live writers. Resource creation and destruction use OS locks. A blocking receive also uses an OS semaphore. Language wrappers may synchronize their own handle lifetime. - -Supported participants must run on a little-endian, 64-bit architecture with naturally aligned, process-shared 32-bit and 64-bit atomics (x86-64 or ARM64). No 128-bit compare/exchange or `CMPXCHG16B` is required. Files containing this layout cannot be treated as durable queues across reboot, copied between machines as live queues, or shared with v1/v2 participants. - -## Identity and storage lifetime - -A queue is transient and exists only while at least one publisher or subscriber remains attached. After the last endpoint closes or its process exits, its messages cannot be resumed. Reopening the same name creates a fresh, empty queue. Keep participant lifetimes overlapping across process handoffs; either a publisher or a subscriber is sufficient to retain the queue. - -Participants must agree on the queue name, logical capacity, and, on Unix, the same backing directory. Use an explicit absolute directory for cross-language applications: runtime defaults for the temporary directory can differ. The name must fit platform object-name limits; short ASCII names without slashes or NUL work on all platforms. A distinct Unix path does **not** create a distinct semaphore name: use names unique across paths. - -Capacity `C` is the size of the circular message buffer only. It must exceed 16 and be divisible by 8. Total mapped bytes are `262400 + C`. All additions must be checked against the implementation's addressable range. - -### Linux and macOS - -For base directory `P` and name `N`: - -| Resource | Name | -|---|---| -| Backing file | `P/.cloudtoid/interprocess/v3/mmf/N.qu` | -| Coordination lock | `flock` on the `P/.cloudtoid/interprocess/v3/mmf` directory inode | -| Participant lease | `P/.cloudtoid/interprocess/v3/readers/N/ID` | -| Named semaphore | `/ct3ip.N` | - -Opening and closing coordinate under an exclusive directory `flock`. Every attached endpoint retains a shared `flock` on the backing file. A new opener tries an exclusive nonblocking backing-file lock. Success proves there are no attached participants: unlink any old named semaphore, remove stale lease files, truncate the backing file to zero, and resize it to the total mapped length. Otherwise the existing file length must match. Convert to a shared lock before releasing coordination. - -Close the semaphore and unmap before releasing the participant's backing-file lifetime lock. While holding coordination, try to convert that shared lock to an exclusive nonblocking lock. Only success permits unlinking the semaphore, lease directory, and backing file. Otherwise retain the queue for remaining participants. A process crash releases its OS locks. After the last participant crashes, stale names can remain until the next opener performs cleanup; they must never be interpreted as a durable saved queue. - -A participant lease is an open file with an exclusive `flock`, held throughout all uses of its registration. Liveness probes open that file and try an exclusive nonblocking lock: contention means alive; acquisition or a missing file means dead. Delete stale lease files after acquiring their lock. Permission and other inspection failures mean **unknown/alive**, never proof of death. Participant IDs are never reused in a live queue, so a stale probe cannot target a later owner of that ID. - -POSIX semaphores are created with initial count zero and requested mode `0777`, subject to the process umask. macOS requires the platform's real variadic `sem_open` ABI. The implementations use `sem_timedwait` on Linux and bounded `sem_trywait` retries on macOS. - -### Windows - -| Resource | Name | -|---|---| -| Page-file-backed mapping | `CT3_IP_N` | -| Initialization mutex | `CT3_INIT_N` | -| Participant lease mapping | `CT3_READER_N.ID` | -| Semaphore | `Global\CT3.IP.N` | - -Mapping and lease names are session-local; participants must share the Windows session and have compatible object permissions. The semaphore uses the existing global namespace. The path option has no effect on Windows. - -Acquire the initialization mutex before opening the mapping. An abandoned mutex still grants ownership. Create/open the total mapped length, then inspect the signed 64-bit logical capacity at offset 32 through a 40-byte view. Zero means this mapping needs initialization; write `C`. Any other value must equal `C`. Release the initialization mutex only after this is complete. No participant opens the mapping before owning that mutex, so a creator that crashes before initialization cannot leave an uninitialized mapping held by a waiting joiner. - -Windows retains mapping and semaphore objects while handles/views remain. Close all handles/views for an endpoint after its calls stop; other publishers and subscribers retain their own handles. Never destroy a queue because one reader crashed. - -A lease is a separate 16-byte mapping, created before its ID becomes visible as a queue owner: - -| Offset | Type | Meaning | -|---|---|---| -| 0 | signed 32-bit integer | Process ID | -| 4 | 4 bytes | Zero padding | -| 8 | signed 64-bit integer | Process creation time, UTC .NET ticks since 0001-01-01 | - -Read both fields to probe liveness. The PID must identify a running process whose creation time matches exactly, avoiding PID reuse. Convert Windows FILETIME to .NET ticks by adding `504911232000000000`; both count 100 ns intervals, with different epochs. Missing mappings, a nonexistent/exited process, or a creation-time mismatch mean dead. Inspection/access failures mean unknown/alive. Do not retain the probe mapping longer than the probe itself. - -## Shared memory layout - -All offsets are bytes from the mapping's first byte. All integers are little-endian. Initial memory is zero. Fields identified as atomic must always be accessed atomically at their natural alignment while shared. Padding remains reserved and zero; a port must not repurpose it without a protocol change. - -| Offset | Size | Field | -|---|---:|---| -| 0 | 8 | Atomic signed `ReadOffset` | -| 8 | 8 | Atomic signed `WriteOffset` | -| 16 | 8 | Atomic signed `ReadLockOwner`; 0 means unowned | -| 24 | 4 | Atomic signed `NotificationPending`; 0 or 1 | -| 28 | 4 | Atomic signed `LastParticipantId` | -| 32 | 8 | Windows logical capacity; initialization mutex protects it; unused on Unix | -| 40–127 | 88 | Reserved | -| 128 | 4 | Atomic signed recovery admission gate; 0 open, 1 closed | -| 132–255 | 124 | Reserved | -| 256 | 262144 | 2048 publisher slots, each 128 bytes | -| 262400 | `C` | Circular message buffer | - -Each publisher slot contains: - -| Relative offset | Size | Field | -|---|---:|---| -| 0 | 8 | Atomic signed participant ID; 0 means unused | -| 8 | 4 | Atomic signed number of admitted/in-flight enqueue calls | -| 12–127 | 116 | Reserved | - -Allocate participant IDs with a checked atomic increment of `LastParticipantId`, starting at 1. Publishers and subscribers share this allocator. Establish the liveness lease before installing the ID in a reader owner word or publisher slot. IDs never wrap: fail registration at `INT32_MAX` and use a fresh queue after its participants finish. - -A publisher claims an empty slot with compare/exchange, then resets its active count to zero before admitting calls. If all slots are occupied, it may reclaim a slot whose owner is proven dead, again with compare/exchange against the observed ID. Reject a 2049th live publisher. After all calls stop, release the slot only if it still contains the publisher's ID; release its lease afterward. - -## Circular records and position arithmetic - -`ReadOffset` and `WriteOffset` are monotonic signed 64-bit byte positions. Physical position is `262400 + (position % C)`. Logical positions never wrap, although the physical buffer wraps continually. Require checked arithmetic and reject reservation beyond `INT64_MAX`. Do not silently reset counters in a live queue. At 1 GiB/s of reserved bytes, exhaustion takes about 272 years; it is still an explicit error, not an assumption about runtime length. - -A record consists of: - -| Relative offset | Size | Meaning | -|---|---:|---| -| 0 | 4 | Atomic state: 0 unfinished/free, 1 reader claimed, 2 ready | -| 4 | 4 | Signed nonnegative payload byte length `L` | -| 8 | `L` | Opaque payload bytes | -| `8 + L` | 0–7 | Alignment padding | - -The reserved record length is `(L + 15) & ~7`, using checked/wide arithmetic. Empty payloads are valid and reserve 8 bytes. Every record starts at an 8-byte boundary; because capacity also divides by 8, its header never straddles the physical end. Payload and padding can straddle, requiring at most two copies. The complete cleared record, including padding, must be zero before its space becomes reusable. - -## Memory ordering and publication - -Acquire loads, release stores, and sequentially consistent read/modify/write operations describe the ordering used by the Rust implementation. .NET `Volatile` and `Interlocked` provide the corresponding ordering. A port must use real process-shared native atomics, not an implementation that substitutes process-private locks. - -For each enqueue (or admitted batch): - -1. Atomically increment the publisher slot's active count with a full-fence operation **before** observing the recovery gate. If the gate is closed, decrement and report no admission. -2. Load `ReadOffset` with acquire ordering, then `WriteOffset` with acquire ordering. Compute `used = write - read`. If the proposed padded length exceeds `C - used`, return full. A stale read position is conservative. Keep this order of loads. -3. Checked-add the record length to `write`; compare/exchange `WriteOffset` from that exact `write` to the new value. Retry the capacity calculation after a failed CAS. A successful CAS reserves one exclusive physical range. Never split the capacity check and reservation into unrelated atomic updates. -4. Copy the payload and store its length. Publish state 2 with release ordering **last**. Readers must acquire this state before accessing the length or payload. -5. Notify as described below. Decrement the active count in all exits, after writes and notification complete. - -Keeping one admission across a batch is allowed, provided the active count remains nonzero throughout, each message reserves independently, and recovery can observe the entire batch as active. No batch may continue writing after decrementing its active count. - -The active increment and gate closure/scan need the full-fence ordering: either a writer observes the closed gate and does not write, or the recovery scan observes its active count. Recovery never clears memory while a live admitted writer might still touch it. - -## Consumption and crash recovery - -A subscriber owns one unique participant ID and lease. Each receive attempt: - -1. Inspect `ReadLockOwner` before checking whether the queue is empty. If a different owner is present, periodically probe its lease. A live owner is never expired, however long it pauses. -2. For a proven-dead owner, CAS that exact ID to the repairing subscriber's own ID. Only the successful repairer can reopen a stranded recovery gate, then release ownership. Do not first clear ownership and later open the gate: a delayed repair could corrupt a newer recovery attempt. -3. If empty, return no message. Otherwise CAS owner 0 to the subscriber's own ID; fail without waiting if another reader owns it. Recheck emptiness under ownership. -4. CAS the head record state from ready (2) to claimed (1), with acquire/full-fence ordering. After success, validate/read its length, copy the payload, clear the entire padded record, then atomically advance `ReadOffset`. Releasing the read position only after clearing prevents a writer from reusing uncleared bytes. -5. Release ownership before returning, including unsuccessful attempts. Compare/exchange only the caller's own ID back to zero. - -For an unfinished or abandoned claimed head, retain a local observation containing the current read position, the then-current write tail, and a monotonic timestamp. A changed head starts a new observation. After at least ten seconds at the same blocked head: - -1. While holding reader ownership, atomically close the admission gate. -2. Scan every publisher slot. If any slot has a nonzero active count and an owner that is alive or cannot be proven dead, reopen admission and postpone recovery. Keep the originally captured tail. -3. Otherwise clear the range from the blocked head to the captured tail, then advance `ReadOffset` to that tail. Validate that this range is between zero and `C`. New reservations beyond the captured tail remain intact. -4. Reopen admission and release reader ownership. - -This waits out a slow live publisher and recovers from a killed publisher without erasing the whole queue. It may discard completed messages behind an abandoned head within the captured range. Dead-reader repair occurs even when `read == write`, because a reader can die after advancing the position but before reopening the gate. The ten-second observation interval is a recovery delay, not permission to expire a live process. - -## Notifications and blocking receives - -The semaphore is a wakeup hint, not a message count. On publication, atomically exchange `NotificationPending` to 1. Only a previous value of 0 posts a semaphore permit. A full semaphore is already signaled and does not turn a committed publication into an error. - -A blocking receiver attempts to read before sleeping. Only after actually consuming a semaphore permit may it clear `NotificationPending` to 0. If that receiver exits while data remains after consuming a permit, it relays notification using the same exchange/post sequence. An empty check must not clear another receiver's pending notification. - -Wait/retry in bounded intervals of at most 5 ms (subject to OS scheduling). This retains progress if a publisher crashes after setting the pending flag but before posting, or another receiver consumes a permit and then exits. Language-level timeout/cancellation affects the wait, not the shared protocol. Nonblocking receive does not require cancellation or a semaphore wait. - -## Interoperability and implementation obligations - -Use raw bytes across language boundaries. Managed objects, native pointers, language string layouts, and Arrow C Data pointers are not cross-process payload formats. Agree separately on encoding/schema when needed. Receive buffers belong to the caller; no wrapper should expose borrowed queue memory after advancing `ReadOffset`. - -Ports must test every publisher/subscriber direction, small capacities and repeated wraparound, empty payloads, full queues, concurrent participants, capacity mismatch, last-participant cleanup, process death, and paused live owners. `tests/interop/run.py` runs the six-language pair matrix. Rust tests pin the binary offsets and exercise the native implementation; .NET retains its own regression tests. - -Resource cleanup must happen only after all calls using that endpoint have stopped. Rust borrowing provides that lifetime rule. C callers must obey it explicitly. Other bindings enforce their documented close behavior. A queue protocol does not make use-after-close of a language handle valid. - -Queue names are limited to 24 UTF-8 bytes on macOS and 245 on Linux, without slashes or NUL. Backslashes are rejected on Windows and permitted on Unix for compatibility. Use at most 24 bytes when sharing configuration across platforms. A corrupt record length is rejected without advancing the reader or clearing storage; callers must stop the participants and start a fresh queue rather than retrying indefinitely. diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index 4c89391..c973340 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -16,7 +16,7 @@ classifiers = ["Programming Language :: Python :: 3", "Programming Language :: R [project.urls] Homepage = "https://cloudtoid.com" Repository = "https://github.com/cloudtoid/interprocess" -Documentation = "https://github.com/cloudtoid/interprocess/blob/main/docs/protocol.md" +Documentation = "https://cloudtoid.com/docs/protocol/" [tool.maturin] python-source = "python" diff --git a/src/website/README.md b/src/website/README.md index e54cfce..5a1d895 100644 --- a/src/website/README.md +++ b/src/website/README.md @@ -22,3 +22,5 @@ Run `python3 src/website/validate.py` after building to check local links, ancho The social preview uses `assets/social-card.png`; its editable SVG source is alongside it. SEO metadata uses `https://cloudtoid.com` as the canonical origin. Publishing makes the sitemap available at `/sitemap.xml`; search-engine indexing happens independently of deployment. CSS and JavaScript URLs include a content hash at build time, so browsers fetch changed assets after each deployment. Cloudflare build-watch includes must use `src/website/*` (a single wildcard also matches nested paths); GitHub Actions uses its own `src/website/**` glob. + +The complete protocol specification lives in `docs/protocol.html` at `/docs/protocol/`. Its interactive illustration uses `protocol.js` to show reservation order, unfinished publications, capacity, and physical wraparound; the specification below remains the authoritative contract. diff --git a/src/website/build.py b/src/website/build.py index a1ddb57..b87d33e 100644 --- a/src/website/build.py +++ b/src/website/build.py @@ -45,7 +45,7 @@ def metadata(title, description, path, kind='website'): } home = home.replace('', '\n') (output / 'index.html').write_text(home) -for name in ('style.css', 'site.js', 'theme.js', 'docs.css', 'docs.js'): +for name in ('style.css', 'site.js', 'theme.js', 'docs.css', 'docs.js', 'protocol.js'): shutil.copyfile(root / name, output / name) for name in ('assets', 'benchmarks', 'vendor'): shutil.copytree(root / name, output / name, dirs_exist_ok=True) diff --git a/src/website/docs.css b/src/website/docs.css index 39e2602..69a0988 100644 --- a/src/website/docs.css +++ b/src/website/docs.css @@ -4,7 +4,6 @@ .docs-sidebar nav, .docs-toc nav { display: flex; flex-direction: column; align-items: stretch; gap: 3px; } .docs-sidebar nav a { display: block; padding: 7px 10px; border-radius: 5px; color: var(--muted); font-size: 14px; } .docs-sidebar a[aria-current=page] { background: var(--hero-background); color: var(--accent); font-weight: 650; } -.docs-sidebar .protocol-link { display: block; margin-top: 25px; padding-top: 18px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; } .docs-toc a { font-size: 12px; color: var(--muted); padding: 4px 0; line-height: 1.5; } .doc-content { min-width: 0; } .doc-breadcrumb { display: flex; flex-wrap: wrap; gap: 9px; font-size: 12px; color: var(--muted); margin-bottom: 24px; } @@ -48,7 +47,6 @@ .docs-sidebar { position: static; max-height: none; padding-bottom: 20px; border-bottom: 1px solid var(--line); } .docs-sidebar nav { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 4px; } .docs-sidebar nav a { display: block; padding: 6px; font-size: 12px; } - .docs-sidebar .protocol-link { display: none; } .docs-sidebar .eyebrow { margin-bottom: 10px; } } @media (max-width:520px) { @@ -61,3 +59,30 @@ .doc-content .heading-link { color: inherit; text-decoration: none; } .doc-content .heading-link:hover { text-decoration: underline; } + +.queue-demo { background: var(--hero-background); border: 1px solid var(--line); border-radius: 10px; padding: 20px; margin: 24px 0; } +.queue-options, .queue-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 16px; } +.queue-options label { font-size: 13px; color: var(--ink); } +.queue-options select, .queue-actions button { font: inherit; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-radius: 5px; padding: 8px 10px; } +.queue-actions button { font-size: 12px; cursor: pointer; min-height: 40px; } +.queue-actions button:hover { border-color: var(--accent); } +.queue-actions button:disabled { opacity: .5; cursor: default; } +.queue-cells { display: grid; grid-template-columns: repeat(8,minmax(0,1fr)); gap: 4px; } +.queue-cell { min-width: 0; border: 1px solid var(--line); background: var(--surface); border-radius: 5px; padding: 8px 2px; text-align: center; color: var(--ink); } +.queue-cell span, .queue-cell strong, .queue-cell small { display: block; } +.queue-byte { font-size: 10px; } +.queue-cell strong { font: 600 12px/2 var(--mono); } +.queue-cell small { min-height: 16px; font: 11px/16px var(--mono); } +.queue-cell[data-publisher=A] { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); border-color: var(--accent); } +.queue-cell[data-publisher=B] { background: color-mix(in srgb, var(--ink) 12%, var(--surface)); border-color: var(--muted); } +.queue-cell[data-ready=false] { background-image: repeating-linear-gradient(135deg, transparent 0 5px, color-mix(in srgb, var(--ink) 12%, transparent) 5px 7px); border-style: dashed; } +.doc-content .queue-legend { font-size: 12px; margin: 12px 0; } +.queue-counters { display: flex; flex-wrap: wrap; gap: 12px 25px; margin: 20px 0; } +.queue-counters dt { font: 11px var(--mono); color: var(--muted); } +.doc-content .queue-counters dd { font: 18px/1.5 var(--mono); margin: 5px 0 0; color: var(--ink); } +#queue-status { margin: 0; min-height: 5.4em; } +@media (max-width:520px) { + .queue-demo { padding: 12px; } + .queue-cells { grid-template-columns: repeat(4,minmax(0,1fr)); } + .queue-actions button { min-height: 44px; } +} diff --git a/src/website/docs/concepts.html b/src/website/docs/concepts.html index 067e7b4..5198423 100644 --- a/src/website/docs/concepts.html +++ b/src/website/docs/concepts.html @@ -23,4 +23,4 @@

Crashes and recovery

Publisher reservations use native 64-bit atomics. The complete queue is not formally lock-free: readers serialize consumption, live paused owners can block progress, and creation and destruction use OS locks. Open native endpoints after fork(); do not use inherited endpoints in the child.

Upgrades and long-lived queues

v3 is incompatible with the v1/v2 shared-memory layout. Drain the old queue, stop all participants, and upgrade them together using a fresh queue. The physical buffer wraps; logical counters do not. Counter exhaustion is an explicit error and requires a fresh queue after participants finish.

-

For layout offsets, atomic ordering, and recovery details, read the full protocol v3 specification.

+

For layout offsets, atomic ordering, and recovery details, read the full protocol v3 specification.

diff --git a/src/website/docs/pages.json b/src/website/docs/pages.json index 558e90e..6b03ac3 100644 --- a/src/website/docs/pages.json +++ b/src/website/docs/pages.json @@ -1,10 +1,56 @@ [ - {"slug":"", "label":"Overview", "title":"Developer documentation", "description":"Build fast cross-process messaging with Cloudtoid Interprocess. Installation guides and API references for Rust, Node.js, Go, C, Python, and .NET."}, - {"slug":"concepts", "label":"Queue concepts", "title":"Queue lifetime, delivery, and interoperability", "description":"Understand transient queue lifetime, competing subscribers, capacity, message ordering, crash recovery, and cross-language compatibility in protocol v3."}, - {"slug":"rust", "label":"Rust", "title":"Rust API reference", "description":"Install cloudtoid-interprocess and use Options, Publisher, and Subscriber. Reference for batch sends, reusable receive buffers, blocking waits, and errors."}, - {"slug":"node", "label":"Node.js", "title":"Node.js & TypeScript API reference", "description":"Install @cloudtoid/interprocess. Send Uint8Array messages, receive Buffers, cancel with AbortSignal, and close endpoints using the Node.js API."}, - {"slug":"go", "label":"Go", "title":"Go API reference", "description":"Use Cloudtoid Interprocess from Go with cgo. Configure Options, send and receive byte slices, reuse buffers, and cancel receives with context.Context."}, - {"slug":"c", "label":"C", "title":"C API reference", "description":"Install the Cloudtoid Interprocess C SDK. Reference for handles, status codes, receive timeouts, owned buffers, and safe shutdown."}, - {"slug":"python", "label":"Python", "title":"Python API reference", "description":"Build Cloudtoid Interprocess for Python. Send bytes and buffer objects, receive with timeouts, use context managers, and handle queue exceptions."}, - {"slug":"dotnet", "label":".NET", "title":".NET API reference", "description":"Install Cloudtoid.Interprocess from NuGet. Use QueueFactory, QueueOptions, IPublisher, and ISubscriber with reusable buffers and CancellationToken."} + { + "slug": "", + "label": "Overview", + "title": "Developer documentation", + "description": "Build fast cross-process messaging with Cloudtoid Interprocess. Installation guides and API references for Rust, Node.js, Go, C, Python, and .NET." + }, + { + "slug": "concepts", + "label": "Queue concepts", + "title": "Queue lifetime, delivery, and interoperability", + "description": "Understand transient queue lifetime, competing subscribers, capacity, message ordering, crash recovery, and cross-language compatibility in protocol v3." + }, + { + "slug": "protocol", + "label": "Protocol v3", + "title": "Shared-memory protocol v3", + "description": "Explore an interactive circular queue, then read the complete Interprocess v3 specification: memory layout, atomic publication, ordering, lifetime, and crash recovery." + }, + { + "slug": "rust", + "label": "Rust", + "title": "Rust API reference", + "description": "Install cloudtoid-interprocess and use Options, Publisher, and Subscriber. Reference for batch sends, reusable receive buffers, blocking waits, and errors." + }, + { + "slug": "node", + "label": "Node.js", + "title": "Node.js & TypeScript API reference", + "description": "Install @cloudtoid/interprocess. Send Uint8Array messages, receive Buffers, cancel with AbortSignal, and close endpoints using the Node.js API." + }, + { + "slug": "go", + "label": "Go", + "title": "Go API reference", + "description": "Use Cloudtoid Interprocess from Go with cgo. Configure Options, send and receive byte slices, reuse buffers, and cancel receives with context.Context." + }, + { + "slug": "c", + "label": "C", + "title": "C API reference", + "description": "Install the Cloudtoid Interprocess C SDK. Reference for handles, status codes, receive timeouts, owned buffers, and safe shutdown." + }, + { + "slug": "python", + "label": "Python", + "title": "Python API reference", + "description": "Build Cloudtoid Interprocess for Python. Send bytes and buffer objects, receive with timeouts, use context managers, and handle queue exceptions." + }, + { + "slug": "dotnet", + "label": ".NET", + "title": ".NET API reference", + "description": "Install Cloudtoid.Interprocess from NuGet. Use QueueFactory, QueueOptions, IPublisher, and ISubscriber with reusable buffers and CancellationToken." + } ] diff --git a/src/website/docs/protocol.html b/src/website/docs/protocol.html new file mode 100644 index 0000000..2275d34 --- /dev/null +++ b/src/website/docs/protocol.html @@ -0,0 +1,291 @@ +

One circular buffer. Concurrent publishers reserve space; subscribers consume messages in reservation order.

+

Explore the queue

+

Publish a few messages, receive them, then publish again to see the buffer wrap. Pause a publication to see why a later ready message cannot overtake it.

+ + +

Illustration with a 64-byte message buffer and two connected publishers. Each action steps through a simplified operation; actual reader claims, atomic retries, notifications, and recovery are specified below. Read and write positions keep increasing while physical positions wrap modulo capacity.

+ + +

This document specifies the shared-memory protocol used by the .NET implementation and the Rust core behind the C, Python, Node.js, and Go packages. Package API versions and the protocol namespace are separate: an API release can remain compatible with v3.

+

Scope and guarantees

+

A queue connects processes on one machine. Publishers reserve space concurrently. Subscribers compete for messages; one shared reader owner serializes consumption. Delivery follows reservation order, even if a later reservation becomes ready first. Concurrent calls from one publisher have no defined relative order. Successful sequential calls preserve order. An accepted batch is a prefix of the input, not a transaction; other publishers can interleave reservations.

+

Ordinary operation delivers each message to one subscriber. This is volatile IPC, not durable storage, broadcast, or an acknowledged work queue. A reader can die after copying a message but before advancing the queue. Recovery can discard an abandoned reservation and completed messages behind it. Applications that need durable processing or exactly-once effects must implement those guarantees separately.

+

The enqueue reservation path uses native atomics, without a per-message OS mutex. The entire queue is not formally lock-free: consumption has a reader ownership word, an indefinitely paused live reader can block readers, and recovery waits for admitted live writers. Resource creation and destruction use OS locks. A blocking receive also uses an OS semaphore. Language wrappers may synchronize their own handle lifetime.

+

Supported participants must run on a little-endian, 64-bit architecture with naturally aligned, process-shared 32-bit and 64-bit atomics (x86-64 or ARM64). No 128-bit compare/exchange or CMPXCHG16B is required. Files containing this layout cannot be treated as durable queues across reboot, copied between machines as live queues, or shared with v1/v2 participants.

+

Identity and storage lifetime

+

A queue is transient and exists only while at least one publisher or subscriber remains attached. After the last endpoint closes or its process exits, its messages cannot be resumed. Reopening the same name creates a fresh, empty queue. Keep participant lifetimes overlapping across process handoffs; either a publisher or a subscriber is sufficient to retain the queue.

+

Participants must agree on the queue name, logical capacity, and, on Unix, the same backing directory. Use an explicit absolute directory for cross-language applications: runtime defaults for the temporary directory can differ. The name must fit platform object-name limits; short ASCII names without slashes or NUL work on all platforms. A distinct Unix path does not create a distinct semaphore name: use names unique across paths.

+

Capacity C is the size of the circular message buffer only. It must exceed 16 and be divisible by 8. Total mapped bytes are 262400 + C. All additions must be checked against the implementation's addressable range.

+

Linux and macOS

+

For base directory P and name N:

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ResourceName
Backing fileP/.cloudtoid/interprocess/v3/mmf/N.qu
Coordination lockflock on the P/.cloudtoid/interprocess/v3/mmf directory inode
Participant leaseP/.cloudtoid/interprocess/v3/readers/N/ID
Named semaphore/ct3ip.N
+

Opening and closing coordinate under an exclusive directory flock. Every attached endpoint retains a shared flock on the backing file. A new opener tries an exclusive nonblocking backing-file lock. Success proves there are no attached participants: unlink any old named semaphore, remove stale lease files, truncate the backing file to zero, and resize it to the total mapped length. Otherwise the existing file length must match. Convert to a shared lock before releasing coordination.

+

Close the semaphore and unmap before releasing the participant's backing-file lifetime lock. While holding coordination, try to convert that shared lock to an exclusive nonblocking lock. Only success permits unlinking the semaphore, lease directory, and backing file. Otherwise retain the queue for remaining participants. A process crash releases its OS locks. After the last participant crashes, stale names can remain until the next opener performs cleanup; they must never be interpreted as a durable saved queue.

+

A participant lease is an open file with an exclusive flock, held throughout all uses of its registration. Liveness probes open that file and try an exclusive nonblocking lock: contention means alive; acquisition or a missing file means dead. Delete stale lease files after acquiring their lock. Permission and other inspection failures mean unknown/alive, never proof of death. Participant IDs are never reused in a live queue, so a stale probe cannot target a later owner of that ID.

+

POSIX semaphores are created with initial count zero and requested mode 0777, subject to the process umask. macOS requires the platform's real variadic sem_open ABI. The implementations use sem_timedwait on Linux and bounded sem_trywait retries on macOS.

+

Windows

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ResourceName
Page-file-backed mappingCT3_IP_N
Initialization mutexCT3_INIT_N
Participant lease mappingCT3_READER_N.ID
SemaphoreGlobal\CT3.IP.N
+

Mapping and lease names are session-local; participants must share the Windows session and have compatible object permissions. The semaphore uses the existing global namespace. The path option has no effect on Windows.

+

Acquire the initialization mutex before opening the mapping. An abandoned mutex still grants ownership. Create/open the total mapped length, then inspect the signed 64-bit logical capacity at offset 32 through a 40-byte view. Zero means this mapping needs initialization; write C. Any other value must equal C. Release the initialization mutex only after this is complete. No participant opens the mapping before owning that mutex, so a creator that crashes before initialization cannot leave an uninitialized mapping held by a waiting joiner.

+

Windows retains mapping and semaphore objects while handles/views remain. Close all handles/views for an endpoint after its calls stop; other publishers and subscribers retain their own handles. Never destroy a queue because one reader crashed.

+

A lease is a separate 16-byte mapping, created before its ID becomes visible as a queue owner:

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
OffsetTypeMeaning
0signed 32-bit integerProcess ID
44 bytesZero padding
8signed 64-bit integerProcess creation time, UTC .NET ticks since 0001-01-01
+

Read both fields to probe liveness. The PID must identify a running process whose creation time matches exactly, avoiding PID reuse. Convert Windows FILETIME to .NET ticks by adding 504911232000000000; both count 100 ns intervals, with different epochs. Missing mappings, a nonexistent/exited process, or a creation-time mismatch mean dead. Inspection/access failures mean unknown/alive. Do not retain the probe mapping longer than the probe itself.

+

Shared memory layout

+

All offsets are bytes from the mapping's first byte. All integers are little-endian. Initial memory is zero. Fields identified as atomic must always be accessed atomically at their natural alignment while shared. Padding remains reserved and zero; a port must not repurpose it without a protocol change.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OffsetSizeField
08Atomic signed ReadOffset
88Atomic signed WriteOffset
168Atomic signed ReadLockOwner; 0 means unowned
244Atomic signed NotificationPending; 0 or 1
284Atomic signed LastParticipantId
328Windows logical capacity; initialization mutex protects it; unused on Unix
40–12788Reserved
1284Atomic signed recovery admission gate; 0 open, 1 closed
132–255124Reserved
2562621442048 publisher slots, each 128 bytes
262400CCircular message buffer
+

Each publisher slot contains:

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Relative offsetSizeField
08Atomic signed participant ID; 0 means unused
84Atomic signed number of admitted/in-flight enqueue calls
12–127116Reserved
+

Allocate participant IDs with a checked atomic increment of LastParticipantId, starting at 1. Publishers and subscribers share this allocator. Establish the liveness lease before installing the ID in a reader owner word or publisher slot. IDs never wrap: fail registration at INT32_MAX and use a fresh queue after its participants finish.

+

A publisher claims an empty slot with compare/exchange, then resets its active count to zero before admitting calls. If all slots are occupied, it may reclaim a slot whose owner is proven dead, again with compare/exchange against the observed ID. Reject a 2049th live publisher. After all calls stop, release the slot only if it still contains the publisher's ID; release its lease afterward.

+

Circular records and position arithmetic

+

ReadOffset and WriteOffset are monotonic signed 64-bit byte positions. Physical position is 262400 + (position % C). Logical positions never wrap, although the physical buffer wraps continually. Require checked arithmetic and reject reservation beyond INT64_MAX. Do not silently reset counters in a live queue. At 1 GiB/s of reserved bytes, exhaustion takes about 272 years; it is still an explicit error, not an assumption about runtime length.

+

A record consists of:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Relative offsetSizeMeaning
04Atomic state: 0 unfinished/free, 1 reader claimed, 2 ready
44Signed nonnegative payload byte length L
8LOpaque payload bytes
8 + L0–7Alignment padding
+

The reserved record length is (L + 15) & ~7, using checked/wide arithmetic. Empty payloads are valid and reserve 8 bytes. Every record starts at an 8-byte boundary; because capacity also divides by 8, its header never straddles the physical end. Payload and padding can straddle, requiring at most two copies. The complete cleared record, including padding, must be zero before its space becomes reusable.

+

Memory ordering and publication

+

Acquire loads, release stores, and sequentially consistent read/modify/write operations describe the ordering used by the Rust implementation. .NET Volatile and Interlocked provide the corresponding ordering. A port must use real process-shared native atomics, not an implementation that substitutes process-private locks.

+

For each enqueue (or admitted batch):

+
    +
  1. Atomically increment the publisher slot's active count with a full-fence operation before observing the recovery gate. If the gate is closed, decrement and report no admission.
  2. +
  3. Load ReadOffset with acquire ordering, then WriteOffset with acquire ordering. Compute used = write - read. If the proposed padded length exceeds C - used, return full. A stale read position is conservative. Keep this order of loads.
  4. +
  5. Checked-add the record length to write; compare/exchange WriteOffset from that exact write to the new value. Retry the capacity calculation after a failed CAS. A successful CAS reserves one exclusive physical range. Never split the capacity check and reservation into unrelated atomic updates.
  6. +
  7. Copy the payload and store its length. Publish state 2 with release ordering last. Readers must acquire this state before accessing the length or payload.
  8. +
  9. Notify as described below. Decrement the active count in all exits, after writes and notification complete.
  10. +
+

Keeping one admission across a batch is allowed, provided the active count remains nonzero throughout, each message reserves independently, and recovery can observe the entire batch as active. No batch may continue writing after decrementing its active count.

+

The active increment and gate closure/scan need the full-fence ordering: either a writer observes the closed gate and does not write, or the recovery scan observes its active count. Recovery never clears memory while a live admitted writer might still touch it.

+

Consumption and crash recovery

+

A subscriber owns one unique participant ID and lease. Each receive attempt:

+
    +
  1. Inspect ReadLockOwner before checking whether the queue is empty. If a different owner is present, periodically probe its lease. A live owner is never expired, however long it pauses.
  2. +
  3. For a proven-dead owner, CAS that exact ID to the repairing subscriber's own ID. Only the successful repairer can reopen a stranded recovery gate, then release ownership. Do not first clear ownership and later open the gate: a delayed repair could corrupt a newer recovery attempt.
  4. +
  5. If empty, return no message. Otherwise CAS owner 0 to the subscriber's own ID; fail without waiting if another reader owns it. Recheck emptiness under ownership.
  6. +
  7. CAS the head record state from ready (2) to claimed (1), with acquire/full-fence ordering. After success, validate/read its length, copy the payload, clear the entire padded record, then atomically advance ReadOffset. Releasing the read position only after clearing prevents a writer from reusing uncleared bytes.
  8. +
  9. Release ownership before returning, including unsuccessful attempts. Compare/exchange only the caller's own ID back to zero.
  10. +
+

For an unfinished or abandoned claimed head, retain a local observation containing the current read position, the then-current write tail, and a monotonic timestamp. A changed head starts a new observation. After at least ten seconds at the same blocked head:

+
    +
  1. While holding reader ownership, atomically close the admission gate.
  2. +
  3. Scan every publisher slot. If any slot has a nonzero active count and an owner that is alive or cannot be proven dead, reopen admission and postpone recovery. Keep the originally captured tail.
  4. +
  5. Otherwise clear the range from the blocked head to the captured tail, then advance ReadOffset to that tail. Validate that this range is between zero and C. New reservations beyond the captured tail remain intact.
  6. +
  7. Reopen admission and release reader ownership.
  8. +
+

This waits out a slow live publisher and recovers from a killed publisher without erasing the whole queue. It may discard completed messages behind an abandoned head within the captured range. Dead-reader repair occurs even when read == write, because a reader can die after advancing the position but before reopening the gate. The ten-second observation interval is a recovery delay, not permission to expire a live process.

+

Notifications and blocking receives

+

The semaphore is a wakeup hint, not a message count. On publication, atomically exchange NotificationPending to 1. Only a previous value of 0 posts a semaphore permit. A full semaphore is already signaled and does not turn a committed publication into an error.

+

A blocking receiver attempts to read before sleeping. Only after actually consuming a semaphore permit may it clear NotificationPending to 0. If that receiver exits while data remains after consuming a permit, it relays notification using the same exchange/post sequence. An empty check must not clear another receiver's pending notification.

+

Wait/retry in bounded intervals of at most 5 ms (subject to OS scheduling). This retains progress if a publisher crashes after setting the pending flag but before posting, or another receiver consumes a permit and then exits. Language-level timeout/cancellation affects the wait, not the shared protocol. Nonblocking receive does not require cancellation or a semaphore wait.

+

Interoperability and implementation obligations

+

Use raw bytes across language boundaries. Managed objects, native pointers, language string layouts, and Arrow C Data pointers are not cross-process payload formats. Agree separately on encoding/schema when needed. Receive buffers belong to the caller; no wrapper should expose borrowed queue memory after advancing ReadOffset.

+

Ports must test every publisher/subscriber direction, small capacities and repeated wraparound, empty payloads, full queues, concurrent participants, capacity mismatch, last-participant cleanup, process death, and paused live owners. tests/interop/run.py runs the six-language pair matrix. Rust tests pin the binary offsets and exercise the native implementation; .NET retains its own regression tests.

+

Resource cleanup must happen only after all calls using that endpoint have stopped. Rust borrowing provides that lifetime rule. C callers must obey it explicitly. Other bindings enforce their documented close behavior. A queue protocol does not make use-after-close of a language handle valid.

+

Queue names are limited to 24 UTF-8 bytes on macOS and 245 on Linux, without slashes or NUL. Backslashes are rejected on Windows and permitted on Unix for compatibility. Use at most 24 bytes when sharing configuration across platforms. A corrupt record length is rejected without advancing the reader or clearing storage; callers must stop the participants and start a fresh queue rather than retrying indefinitely.

diff --git a/src/website/docs/template.html b/src/website/docs/template.html index 1e851ed..9554dc6 100644 --- a/src/website/docs/template.html +++ b/src/website/docs/template.html @@ -15,7 +15,7 @@
CloudtoidCloudtoid / interprocess
- +
Interprocess/Docs$breadcrumb

$title

$content
diff --git a/src/website/index.html b/src/website/index.html index 86595ab..f003925 100644 --- a/src/website/index.html +++ b/src/website/index.html @@ -78,7 +78,7 @@

Send + receive

LANGUAGE INTERFACES

One queue.
Six language interfaces.

Rust, C, Python, Node.js, Go, and .NET can exchange messages through the same open v3 protocol. Pick the right language for each process.

Read the Rust guide →
Install

-

PROTOCOL V3

Open source.
Documented in full.

Inspect the memory layout, atomic ordering, resource lifetime, and crash recovery. Build on an MIT-licensed implementation with tests across every publisher/subscriber language pair.

Built for volatile queues on one machine. For durable jobs, add application-level acknowledgements and persistence.

Read the protocol
+

PROTOCOL V3

Open source.
Documented in full.

Inspect the memory layout, atomic ordering, resource lifetime, and crash recovery. Build on an MIT-licensed implementation with tests across every publisher/subscriber language pair.

Built for volatile queues on one machine. For durable jobs, add application-level acknowledgements and persistence.

Read the protocol
diff --git a/src/website/protocol.js b/src/website/protocol.js new file mode 100644 index 0000000..f3ebddf --- /dev/null +++ b/src/website/protocol.js @@ -0,0 +1,85 @@ +(() => { + const demo = document.getElementById('queue-demo'); + if (!demo) return; + const capacity = 64; + let read = 0, write = 0, nextId = 1; + let records = []; + const cells = demo.querySelector('#queue-cells'); + const status = demo.querySelector('#queue-status'); + const pause = demo.querySelector('#queue-pause'); + const size = demo.querySelector('#queue-size'); + const finish = demo.querySelector('#queue-finish'); + + function render(message) { + cells.replaceChildren(); + for (let offset = 0; offset < capacity; offset += 8) { + const record = records.find(item => (offset - item.start % capacity + capacity) % capacity < item.length); + const cell = document.createElement('div'); + cell.className = 'queue-cell'; + const label = document.createElement('span'); + label.className = 'queue-byte'; + label.textContent = `Byte ${offset}`; + const content = document.createElement('strong'); + content.textContent = 'Free'; + if (record) { + const header = offset === record.start % capacity; + cell.dataset.publisher = record.publisher; + cell.dataset.ready = String(record.ready); + content.textContent = `${record.publisher}${record.id} ${header ? 'H' : 'P'}`; + cell.title = `Message ${record.publisher}${record.id}: ${header ? 'header' : 'payload'}, ${record.ready ? 'ready' : 'unfinished'}`; + } + const marker = document.createElement('small'); + marker.textContent = [offset === read % capacity ? 'R' : '', offset === write % capacity ? 'W' : ''].filter(Boolean).join(' / '); + cell.append(label, content, marker); + cells.append(cell); + } + demo.querySelector('#queue-read').textContent = String(read); + demo.querySelector('#queue-write').textContent = String(write); + demo.querySelector('#queue-used').textContent = `${write - read} / ${capacity} B`; + finish.disabled = !records.some(record => !record.ready); + status.textContent = message; + } + + demo.querySelectorAll('[data-publish]').forEach(button => button.addEventListener('click', () => { + const payload = Number(size.value); + const length = Math.ceil((payload + 8) / 8) * 8; + if (write - read + length > capacity) { + render(`Full for this message: ${length} bytes needed, ${capacity - (write - read)} available. Nothing was reserved.`); + return; + } + const record = { id: nextId++, publisher: button.dataset.publish, start: write, length, ready: !pause.checked }; + records.push(record); + write += length; + const wrapped = record.start % capacity + length > capacity; + render(`${record.publisher}${record.id} reserved ${length} bytes (${payload} payload + 8 header)${wrapped ? ' across the physical end' : ''}. ${record.ready ? 'State 2: ready to receive in order.' : 'State 0: publication paused; the reservation stays occupied.'}`); + })); + + finish.addEventListener('click', () => { + const record = records.find(item => !item.ready); + if (!record) return; + record.ready = true; + render(`${record.publisher}${record.id} published state 2 after writing its payload and length. ReadOffset and WriteOffset did not change.`); + }); + + demo.querySelector('#queue-receive').addEventListener('click', () => { + const record = records[0]; + if (!record) { render('Empty queue. No message to receive.'); return; } + if (!record.ready) { + render(`${record.publisher}${record.id} is unfinished. Later ready messages cannot pass it. A paused live publisher is not expired by a timeout.`); + return; + } + records.shift(); + read += record.length; + render(`Received ${record.publisher}${record.id}: claimed state 2 → 1, copied payload, cleared the record, then advanced ReadOffset by ${record.length}. That space is reusable.`); + }); + + demo.querySelector('#queue-reset').addEventListener('click', () => { + read = write = 0; + nextId = 1; + records = []; + pause.checked = false; + render('Fresh demonstration queue. Real counters never reset while a queue is live.'); + }); + demo.hidden = false; + render('Empty queue. R and W mark physical read and write positions; both start at byte 0.'); +})(); From 8c1e4d5affe58c2ae69a874e22fc2b3dc5ebc88a Mon Sep 17 00:00:00 2001 From: Pedram Rezaei Date: Mon, 14 Sep 2026 15:45:58 -0700 Subject: [PATCH 4/4] Point installation guides and package documentation metadata to website --- README.md | 2 +- src/c/Cargo.toml | 1 + src/c/README.md | 14 ++------------ src/dotnet/Interprocess/Interprocess.csproj | 2 +- src/dotnet/README.md | 2 +- src/go/README.md | 2 +- src/node/README.md | 2 +- src/node/package.json | 2 +- src/python/README.md | 2 +- src/python/pyproject.toml | 2 +- src/rust/Cargo.toml | 1 + src/rust/README.md | 2 +- src/website/docs/c.html | 10 +++++++++- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index fec5e40..92431a2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Interprocess -[Website](https://cloudtoid.com) · [Documentation](https://cloudtoid.com/docs/) · [Languages and packages](#languages) · [Quick start](#quick-start) · [Performance](#performance) · [Protocol v3](https://cloudtoid.com/docs/protocol/) +[Website](https://cloudtoid.com) · [Documentation](https://cloudtoid.com/docs/) · [Languages and packages](#languages) · [Quick start](https://cloudtoid.com/docs/) · [Performance](#performance) · [Protocol v3](https://cloudtoid.com/docs/protocol/) [![NuGet](https://img.shields.io/nuget/v/Cloudtoid.Interprocess?label=NuGet)](https://www.nuget.org/packages/Cloudtoid.Interprocess) [![Rust](https://img.shields.io/crates/v/cloudtoid-interprocess?label=Rust)](https://crates.io/crates/cloudtoid-interprocess) diff --git a/src/c/Cargo.toml b/src/c/Cargo.toml index 7af5a31..9d9260f 100644 --- a/src/c/Cargo.toml +++ b/src/c/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true +documentation = "https://cloudtoid.com/docs/c/" authors.workspace = true description = "C ABI for Cloudtoid Interprocess shared-memory queues" diff --git a/src/c/README.md b/src/c/README.md index 57068d5..ffb0091 100644 --- a/src/c/README.md +++ b/src/c/README.md @@ -4,7 +4,7 @@ The C ABI exposes the shared Rust engine to C/C++ and other native callers. It interoperates with .NET protocol v3. -## Install the prebuilt SDK +## [Install the prebuilt SDK](https://cloudtoid.com/docs/c/#install) macOS Apple Silicon example, using the GitHub CLI. For other platforms, choose darwin-x64, linux-arm64, linux-x64, or win32-x64 in both archive names. See the [C guide](https://cloudtoid.com/docs/c/) for Windows setup. @@ -17,17 +17,7 @@ export PKG_CONFIG_PATH="$PWD/cloudtoid-sdk/lib/pkgconfig:$PKG_CONFIG_PATH" On Windows, extract the `win32-x64` archive and set `PKG_CONFIG_PATH` to its `lib/pkgconfig` directory. Add its `lib` directory to `PATH` for the DLL. Go on Windows also requires a cgo-compatible C compiler and pkg-config. -## Build from source - -From the repository root, with Rust and CMake installed: - -```sh -cmake -S src/c -B target/c-sdk -DCMAKE_INSTALL_PREFIX="$HOME/.local" -cmake --build target/c-sdk --config Release -cmake --install target/c-sdk --config Release -``` - -Link with `pkg-config --cflags --libs cloudtoid-interprocess`. On Unix, these flags embed the installed library directory as a runtime search path. Windows callers use the DLL and import library; add the DLL directory to `PATH` or place the DLL beside the executable. The C header documents status codes, timeout units, ownership, buffer truncation, and concurrent-close requirements. +[Build the SDK from source](https://cloudtoid.com/docs/c/#build-from-source). ## Example diff --git a/src/dotnet/Interprocess/Interprocess.csproj b/src/dotnet/Interprocess/Interprocess.csproj index 63384ec..550986b 100644 --- a/src/dotnet/Interprocess/Interprocess.csproj +++ b/src/dotnet/Interprocess/Interprocess.csproj @@ -16,7 +16,7 @@ A cross-platform shared memory queue for fast interprocess messaging. Copyright (c) Cloudtoid https://github.com/cloudtoid/interprocess/ - https://github.com/cloudtoid/interprocess/blob/main/README.md + https://cloudtoid.com/docs/dotnet/ true interprocess;interprocess-communication;ipc;shared-memory-communication;shared-memory-queue;shared-memory;memory-mapped-file;queue;cross-process-communication;cross-process true diff --git a/src/dotnet/README.md b/src/dotnet/README.md index 36293ba..9802e04 100644 --- a/src/dotnet/README.md +++ b/src/dotnet/README.md @@ -4,7 +4,7 @@ Exchange byte messages between processes on the same machine using a shared-memory queue. Multiple publishers and subscribers can connect to the same queue on Windows, Linux, or macOS. -## Install +## [Install](https://cloudtoid.com/docs/dotnet/#install) Requires .NET 10 or later and a 64-bit process. diff --git a/src/go/README.md b/src/go/README.md index a59d290..43b3e05 100644 --- a/src/go/README.md +++ b/src/go/README.md @@ -6,7 +6,7 @@ Shared-memory byte queues backed by the same Rust core as the C, Python, and Nod Install the [C SDK](https://cloudtoid.com/docs/c/), make its `pkgconfig` directory available through `PKG_CONFIG_PATH`, and, on Windows, add its DLL directory to `PATH`. On Unix, pkg-config embeds the installed library directory as a runtime search path. This package requires cgo, a C compiler, and `pkg-config`. -## Install +## [Install](https://cloudtoid.com/docs/go/#install) Install the C SDK first (see the C guide), then run in your Go module. Requires Go 1.24+, cgo enabled, a C compiler, and pkg-config. diff --git a/src/node/README.md b/src/node/README.md index dc468d6..0a15a27 100644 --- a/src/node/README.md +++ b/src/node/README.md @@ -4,7 +4,7 @@ Exchange bytes directly with Rust, C, Python, Go, and .NET processes through fast shared-memory queues. -## Install +## [Install](https://cloudtoid.com/docs/node/#install) Run in your Node.js project. Requires Node.js 18 or later; platform binaries install automatically. diff --git a/src/node/package.json b/src/node/package.json index 6fa798f..e273475 100644 --- a/src/node/package.json +++ b/src/node/package.json @@ -11,7 +11,7 @@ "LICENSE" ], "license": "MIT", - "homepage": "https://cloudtoid.com", + "homepage": "https://cloudtoid.com/docs/node/", "repository": { "type": "git", "url": "https://github.com/cloudtoid/interprocess.git", diff --git a/src/python/README.md b/src/python/README.md index b5783d0..e9d1abb 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -4,7 +4,7 @@ Fast shared-memory byte queues. Exchange messages directly with Rust, C, Go, Node.js, and .NET processes using the same v3 queue. -## Install +## [Install](https://cloudtoid.com/docs/python/#install) PyPI publishing is pending. Run in an activated Python 3.9+ virtual environment with Git, Rust, and a native linker installed. diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index c973340..1e183a6 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -16,7 +16,7 @@ classifiers = ["Programming Language :: Python :: 3", "Programming Language :: R [project.urls] Homepage = "https://cloudtoid.com" Repository = "https://github.com/cloudtoid/interprocess" -Documentation = "https://cloudtoid.com/docs/protocol/" +Documentation = "https://cloudtoid.com/docs/python/" [tool.maturin] python-source = "python" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 21a80fd..5e796bc 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true +documentation = "https://cloudtoid.com/docs/rust/" authors.workspace = true readme = "README.md" rust-version = "1.87" diff --git a/src/rust/README.md b/src/rust/README.md index c192d92..c8beeb3 100644 --- a/src/rust/README.md +++ b/src/rust/README.md @@ -4,7 +4,7 @@ Fast shared-memory byte queues for processes on the same machine. Exchange messages with Rust, C, Python, Node.js, Go, and .NET using the open v3 protocol. -## Install +## [Install](https://cloudtoid.com/docs/rust/#install) Run in your Cargo project. Requires Rust 1.87 or later. diff --git a/src/website/docs/c.html b/src/website/docs/c.html index dfa54e8..1f8a120 100644 --- a/src/website/docs/c.html +++ b/src/website/docs/c.html @@ -66,4 +66,12 @@

int32_t cip_last_error_kind(void)

Returns a cip_error_kind: CIP_NO_ERROR = 0, CIP_INVALID_ARGUMENT = 1, CIP_CAPACITY_MISMATCH = 2, CIP_PUBLISHER_LIMIT = 3, CIP_EXHAUSTED = 4, CIP_CORRUPT = 5, CIP_IO_ERROR = 6, or CIP_INTERNAL_ERROR = 7. Error state is diagnostic state, not a substitute for checking each function's return status.

Concurrency and shutdown

Handles support concurrent operations, but closing concurrently with an operation is unsafe. Use bounded receive timeouts, signal your worker to stop, join it, then close the handle. There is no cancellation object or batch-send function in the C ABI. Open after fork; inherited handles must not be used in the child.

-

Prebuilt SDKs contain shared libraries. For static linking, build cloudtoid-interprocess-ffi from source and define CIP_STATIC on Windows. See the source-build instructions.

+

Prebuilt SDKs contain shared libraries. For static linking, build cloudtoid-interprocess-ffi from source and define CIP_STATIC on Windows. See the source-build instructions.

+ +

Build from source

+

From the repository root, with Rust and CMake installed:

+
cmake -S src/c -B target/c-sdk -DCMAKE_INSTALL_PREFIX="$HOME/.local"
+cmake --build target/c-sdk --config Release
+cmake --install target/c-sdk --config Release
+

Link with pkg-config --cflags --libs cloudtoid-interprocess. On Unix, these flags embed the installed library directory as a runtime search path. On Windows, add the DLL directory to PATH or place the DLL beside the executable.

+

For a static library, run cargo build --release -p cloudtoid-interprocess-ffi and define CIP_STATIC when compiling Windows callers.