Sipc 0.4.1

dotnet add package Sipc --version 0.4.1
                    
NuGet\Install-Package Sipc -Version 0.4.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sipc" Version="0.4.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sipc" Version="0.4.1" />
                    
Directory.Packages.props
<PackageReference Include="Sipc" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sipc --version 0.4.1
                    
#r "nuget: Sipc, 0.4.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sipc@0.4.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sipc&version=0.4.1
                    
Install as a Cake Addin
#tool nuget:?package=Sipc&version=0.4.1
                    
Install as a Cake Tool

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, any Publish("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 named System.Threading.Semaphore throws PlatformNotSupportedException. 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 get false/diagnostics instead of hanging. A crashed peer's slot may linger as a "phantom" until all peers exit. Cooperative shutdown (Dispose) is clean.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .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.

Version Downloads Last Updated
0.4.1 218 8/6/2026
0.4.0 88 8/5/2026
0.3.0 419 7/31/2026
0.2.0 100 7/31/2026
0.1.0 98 7/30/2026

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.