Sipc 0.2.0
See the version list below for details.
dotnet add package Sipc --version 0.2.0
NuGet\Install-Package Sipc -Version 0.2.0
<PackageReference Include="Sipc" Version="0.2.0" />
<PackageVersion Include="Sipc" Version="0.2.0" />
<PackageReference Include="Sipc" />
paket add Sipc --version 0.2.0
#r "nuget: Sipc, 0.2.0"
#:package Sipc@0.2.0
#addin nuget:?package=Sipc&version=0.2.0
#tool nuget:?package=Sipc&version=0.2.0
SIPC — Semaphore Inter-Process 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).
Subscription-scoped publish
Publish sends a channel frame only to peers that are subscribed to that channel, so channel
traffic never touches uninterested peers. Peers advertise their subscription set to each other:
Subscribe/Unsubscribe broadcast a versioned snapshot (on a pool thread, so they never block), the
Hello/HelloAck handshake carries the current set at join, and each receiver keeps a per-peer view
used to scope future publishes. The monotonic version lets receivers apply snapshots in order
regardless of scheduling.
Subscription state is eventually consistent: a message published in the brief window before a new
subscription has propagated may not reach that subscriber. Receivers still filter by their own live
subscription set as the final authority (so an unsubscribe is honored immediately on the receive
side). If you need delivery that never depends on propagation timing, use Broadcast and filter in
your handler.
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 | 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. |
-
.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.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.