Sipc 0.4.1
dotnet add package Sipc --version 0.4.1
NuGet\Install-Package Sipc -Version 0.4.1
<PackageReference Include="Sipc" Version="0.4.1" />
<PackageVersion Include="Sipc" Version="0.4.1" />
<PackageReference Include="Sipc" />
paket add Sipc --version 0.4.1
#r "nuget: Sipc, 0.4.1"
#:package Sipc@0.4.1
#addin nuget:?package=Sipc&version=0.4.1
#tool nuget:?package=Sipc&version=0.4.1
SIPC — Semaphore Interprocess Communications
A string-based communications library for .NET whose entire transport is counting semaphores.
No sockets, no named pipes, no memory-mapped files, no Win32 P/Invoke, no shared files. Just
System.Threading.Semaphore (cross-process) or SemaphoreSlim (in-process), behind one interface.
Peers form a mesh. Any peer can:
- Broadcast a string to every peer,
- Publish a string to a named channel (only subscribers receive it),
- SendTo a specific peer by name.
The library targets netstandard2.0, so it loads into .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5–10, Mono and Xamarin.
The trick: sending data over a primitive that carries no data
A semaphore has no payload — only a non-negative count. SIPC treats a binary semaphore as a
one-bit wire (count 0 = 0, count 1 = 1) and sends a whole word of BytesPerWord bytes
across BytesPerWord * 8 of them in parallel, exactly like a parallel bus. The default width is
4 bytes (32 data lines) — one round trip per 4 bytes:
data lines d0..d31 : one bit each (4 bytes wide by default; configurable 1..8)
clock : pulsed AFTER the data lines are set (happens-before barrier)
ack : pulsed by the receiver after it drains a word (stop-and-wait flow control)
Sending a word: wait(ack) → for each set bit release(d[i]) → release(clock).
Receiving: wait(clock) → for each i, bit = wait0(d[i]) → release(ack).
Because ack starts at 1 and everything else at 0, exactly one word is ever in flight, so a data
line never accumulates a count above 1 and the receiver reconstructs each word exactly. Widening the
bus (MeshOptions.BytesPerWord) trades more semaphores for proportionally fewer clock/ack round
trips — the expensive blocking operations.
On top of the word pipe:
| Layer | Responsibility |
|---|---|
| Word wire | one N-byte word ↔ N×8 bit-line semaphores + clock + ack |
| Framing | length-prefixed frames streamed as words (final word zero-padded); one inbox, many writers (guarded by an inbox-lock semaphore) |
| Envelope | type + source/dest slot + name + channel + text |
| Mesh | slot claiming, membership directory, routing, the public API |
Membership without shared memory
The mesh has a fixed number of slots (MaxPeers). Each peer owns one inbox and claims one slot
by name convention. Occupancy is tracked with one semaphore per slot. Since a semaphore can't be
peeked, occupancy is read non-destructively under a directory-lock semaphore:
if (occ.Wait(0)) { occ.Release(); /* was occupied */ } else { /* free */ }
Broadcast just enumerates occupied slots and sends the frame to each inbox — there is no
server. Peer names are learned actively (a Hello/HelloAck handshake at join) and passively
(every inbound frame carries its sender's slot and name).
Channels: strongly consistent, targeted publish
Each channel is modelled as its own occupancy namespace — effectively a mini-mesh dedicated to
one channel. Subscribe("x") joins that namespace by claiming a subscriber slot and opening a
per-channel inbox; Publish("x", …) enumerates the channel's live occupied slots (the same
authoritative occupancy read broadcast uses) and sends to each.
That makes publish both targeted — no traffic to uninterested peers — and strongly consistent:
once
Subscribe("x")returns, anyPublish("x")that begins afterward is guaranteed to reach that subscriber.
No gossiped subscription state, no version vectors, nothing to lag behind — the channel's directory
lock is the single linearization point. Unsubscribe frees the slot synchronously, so it stops
delivery immediately too. (The channel name is hashed into a fixed-length, cross-process-stable
semaphore namespace, so any string is a valid channel.)
The cost of exactness: each channel you're subscribed to uses one background reader thread and a
small set of semaphores (MeshOptions.MaxSubscribersPerChannel, default = MaxPeers). Publishing to
a channel costs no thread. This suits a handful-to-dozens of channels per peer, not thousands.
Quick start
using Sipc;
var options = new MeshOptions
{
MeshName = "chat",
MaxPeers = 32,
Provider = new NamedSemaphoreProvider(), // cross-process (Windows); see portability note
};
using var peer = new SemaphoreMesh("alice", options);
peer.MessageReceived += (_, e) =>
Console.WriteLine($"[{e.Scope}] {e.SenderName}: {e.Text}");
peer.Broadcast("hello mesh!"); // to everyone
peer.Subscribe("weather"); // join a channel (call repeatedly for several)
peer.Publish("weather", "it is sunny"); // only to peers subscribed to "weather"
peer.SendTo("bob", "psst"); // to one peer
For an in-process or fully portable mesh (any OS), swap the provider:
Provider = new LocalSemaphoreProvider(), // SemaphoreSlim, process-wide registry
Portability & limitations
- The library is netstandard2.0 and runs everywhere. The in-process provider
(
LocalSemaphoreProvider) works on every OS. - Cross-process (
NamedSemaphoreProvider) is Windows-only — not a SIPC limitation but a BCL one: .NET only implements named semaphores on Windows; on Linux/macOS a namedSystem.Threading.SemaphorethrowsPlatformNotSupportedException. There is no cross-process semaphore in the .NET base class library on Unix, so no semaphore-only library can be cross-process there. - Throughput is modest by design. Stop-and-wait means one round trip per word (4 bytes by
default); this is a correctness-first library for control/chat-scale strings, not a bulk data pipe.
Widening the bus via
MeshOptions.BytesPerWord(up to 8 bytes / 64 lines) is the speed knob. - Crash fragility. Semaphores have no ownership/abandonment semantics (unlike
Mutex). If a peer is killed mid-frame while holding an inbox lock, that inbox can desync; if it dies holding the directory lock, membership stalls until the mesh empties. Sends use timeouts so callers getfalse/diagnostics instead of hanging. A crashed peer's slot may linger as a "phantom" until all peers exit. Cooperative shutdown (Dispose) is clean.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net452 is compatible. net46 was computed. net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.5.2
- No dependencies.
-
.NETStandard 2.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Sipc:
| Package | Downloads |
|---|---|
|
Lockjaw.Core
The shared model behind Lockjaw: reads environment-prefixed settings from app.config and appsettings.json, resolves each key to the environment you have selected, and defines the snapshot the Lockjaw app publishes and clients read. Environments are open-ended — Development_ and Production_ are only the defaults, and any prefix you declare (Local_, Test_, Staging_) is picked up. Includes the editable settings catalog, overlay persistence that never rewrites your configuration files, and import/export. Most applications want Lockjaw.Client instead. Reference this package directly only when hosting the resolution yourself or building tooling on top of Lockjaw. |
GitHub repositories
This package is not used by any popular GitHub repositories.
0.3.0
- Channels are now strongly consistent. Each channel is its own occupancy namespace: Subscribe joins
it (claims a slot), Publish enumerates the channel's live subscribers and sends to each. Once
Subscribe returns, a later Publish is guaranteed to reach it — no more eventually-consistent window.
- Delivery stays targeted (only real subscribers get frames). New MeshOptions.MaxSubscribersPerChannel
(0 = inherit MaxPeers) caps subscribers per channel. Each subscribed channel uses one reader thread.
- Removed the subscription-gossip protocol (SubUpdate) and the SubVersion/Channels envelope fields.
- Wire format changed again: all peers in a mesh must run 0.3.0+.
0.2.0
- Subscription-scoped Publish: channel frames are sent only to subscribed peers (peers advertise a
versioned subscription snapshot), cutting channel traffic to uninterested peers.
- Fix: sending from a MessageReceived handler no longer deadlocks the mesh under load with 3+ peers.
Each peer now drains its inbox on a dedicated reader thread and runs handlers on a separate
dispatcher thread, so handler sends can't stall inbox draining.
- Prompt shutdown: new MeshOptions.ShutdownTimeoutMs (default 400ms) so leaving the mesh isn't held
up for the full SendTimeoutMs per already-departed peer.
- Wire format changed (Envelope gained SubVersion + Channels): all peers in a mesh must run 0.2.0+.
0.1.0
- Initial release. Semaphore-only mesh transport with configurable bus width (default 4 bytes/clock),
broadcast, channel pub/sub, and direct messaging.