Skip to content
SubEtha PowerShell

SubEtha PowerShell

The PowerShell binding (subetha-pwrs)

subetha-pwrs gives PowerShell the memory-mapped primitives directly, with no C shim between the shell and the Rust. It ships as a PowerShell module named SubEtha rather than to crates.io, and runs on PowerShell 7.4 and later and on Windows PowerShell 5.1.

Import-Module SubEtha

This page is the reference. To install it and send a first message, start at SubEtha from PowerShell . For a task rather than a surface, the PowerShell how-to guides cover installing, choosing a structure, making it fast, runspaces and lifetimes, bridging two hosts and diagnosing a failure; how the binding works covers why it is shaped this way.

This page describes the surface by family. The complete surface, every name with every type, is generated from the built module and split across three pages:

  • What the values look like - real output, captured from a run, for the shapes a type name cannot convey.
  • Every cmdlet, in full - all 139, grouped by family with a contents list: each parameter, its type, whether it is required, its position, what it takes from the pipeline, what the cmdlet writes, and the properties and methods of whatever came back.
  • Every object, in full - all 121 object types, with each property and each method signature and return type.
  • Every enum - all 14, with their values.

Those three are produced by crates/subetha-pwrs/tools/Export-Reference.ps1 reading the module itself, so they cannot disagree with what ships. Regenerate them whenever the surface changes.

Every cmdlet also carries its own help, which is the faster lookup while you are in a shell: Get-Help New-SubEthaRing -Full gives a description, every parameter and an example.

The module also registers a drive: $shm:name = value publishes a value every process of the user reads as $shm:name, live and outliving the session. The shm: drive describes it.

What a call costs, and what follows

Measured on a Ryzen 9 7900X, the median of five runs, in nanoseconds per operation:

reached byPowerShell 7.6Windows PowerShell 5.1
an empty PowerShell loop203290
this binding, a method call1907651
this binding, a method call with an argument2656995
this binding, a property read368771
this binding, batched a thousand at a time4.53.0
a ring, one Push and one Pop per item48482013
a ring, 256 items per PushMany and PopMany405805
a ring, 256 items packed in one byte[] each way3922
the pipeline, one record per item through Send-SubEthaItem and Receive-SubEthaItem17127955

The same atomic load costs the Rust about 7 ns through the C ABI. What a method call costs is what the host’s own method invocation costs, and the two hosts differ on that more than anything in the library does: PowerShell 7 reads a property cheaply and calls a method dearly, Windows PowerShell the other way round. Three shapes cross less often, and the surface is built around them:

  • A batch call carries many operations over one call. FetchAddMany, SendMany, RecvMany, InsertMany, GetMany, Drain and their kin are all this shape.
  • A packed buffer carries many items in one byte[] with no object per item: PushPacked, SendPacked and PopPacked on the rings, ReadRange and WriteRange on the vector and the slab. Read whole, it is tens of nanoseconds an item in either host.
  • The pipeline is for a script that reads as a pipeline. Per record it costs about what a method call does in PowerShell 7 and several times that in Windows PowerShell.

bench/CallShapes.ps1 in the crate reproduces every row above on the reader’s own machine, in whichever host runs it.

Two layers, and the pipeline between them

A cmdlet invocation costs the pipeline’s per-record work. A method call on an object the module returned costs one native call into the library plus the host’s own method invocation, which the table above puts a number on. So the module is built in two layers:

  • Cmdlets obtain a structure. New-SubEthaRing creates the file when it does not exist and attaches to it when it does; Open-SubEthaRing attaches to one that must already exist. A path is resolved against the session’s current location. Every cmdlet also answers to a shorter name with the SE prefix: New-SERing.
  • Objects operate on it. What a cmdlet writes is an object of a SubEtha.* type. Its properties are what was fixed when the structure was obtained (the path, the capacity, the slot size), and its methods are the operations the Rust type offers, one method per operation, named in PascalCase after the Rust: $ring.RegisterProducer(), $ring.Send($producer, $bytes), $map.Insert(7, 70).
  • The pipeline moves items. Send-SubEthaItem -To $structure sends whatever is piped in and writes back what the structure refused, so a full ring hands the item back rather than dropping it. Receive-SubEthaItem -From $structure reads a structure out into the pipeline, everything waiting or up to -Count. Both call the object’s own methods through the engine, so they serve every structure that moves items; a script moving bulk items calls the many-item methods directly.

What crosses

Rust sidePowerShell side
a byte payloadbyte[], pinned where it lies as an argument and filled through one pin as a result; a string argument is taken as its UTF-8, and any other array of numbers is converted
a value of a declared sizebyte[] of exactly that size
an unsigned integer key or valueulong
a count, an index, a capacityulong in, ulong out (uint where the Rust is 32-bit)
a memory ordering, a locale, a stamp kind, a shapean enum under SubEtha.: [SubEtha.MemoryOrder]::Relaxed, or its name as a string
a tuplea small record class: SubEtha.StampedItem, SubEtha.ClockReading, SubEtha.Entry, SubEtha.Scan, SubEtha.Endpoint and the rest
a guard the Rust returnsan object that gives it back: SubEtha.Hold, SubEtha.PermitHold, SubEtha.LeaseHold, SubEtha.SlabPin, SubEtha.MapPin, SubEtha.LanedPin, SubEtha.LaneClaim
an absent answer$null

Items packed end to end cross as one byte[]: PushPacked and SendPacked take a buffer and an item length, PopPacked answers a SubEtha.PackedItems holding the count and the bytes at a slot’s full width each.

The conventions

  • A refusal is an answer, not a fault. A push that does not fit returns $false; a pop with nothing to take returns $null; a sweep that freed nothing returns 0; a sample that kept nothing returns $null. Only a genuine fault is an error.
  • A method fails with an exception; a cmdlet writes an error record, non-terminating unless the cmdlet cannot go on, so -ErrorAction works the PowerShell way. Every error id starts with SubEtha: SubEthaOpen when a structure could not be obtained, SubEthaArgument for an argument that cannot be right, SubEthaOperation for a failure underneath, SubEthaLagged when a subscriber fell far enough behind that what it asked for was overwritten, SubEthaContended when a lease or a lane is held by somebody else, SubEthaWrongLane when a key belongs to another lane, SubEthaKeyAbsent when no lane holds a key, SubEthaNotOwner and SubEthaLease from the lease, and SubEthaReleased for a pin, hold or claim already given back.
  • Anything holding a resource is disposable. A hold, a permit, a pin or a lane claim gives its resource back on Release(), on Dispose(), and when the garbage collector finalizes it, so a script that leaves a block early does not strand a lock. Every object frees its mapping the same way, and IsDisposed says whether it has.
  • A value that is not measured yet is $null, which is a different answer from zero.
  • -ErrorAction Stop on a cmdlet turns a refusal to obtain into an exception, which is what a test or a script that cannot go on wants.

The front door

Four cmdlets that pick the shape underneath from what is described, rather than asking the caller to choose one. Reach for these first; the families below give more control when it is wanted.

CmdletObjectWhat it is
New-SubEthaChannelSubEtha.ChannelA queue between processes that can be waited on. Recv answers $null at once when there is nothing there; RecvFor waits up to a timeout in seconds, sleeping rather than spinning. SendFor is the same on the other side.
New-SubEthaWorkQueue, Open-SubEthaWorkQueueSubEtha.WorkQueueWork one process owns and others take from when idle. The owner pushes and pops at the cheap end it has to itself; a thief opens it and steals from the other end.
New-SubEthaKvMapSubEtha.KvMapA lookup table between processes, from one unsigned integer to another.
New-SubEthaAdaptiveQueueSubEtha.AdaptiveQueueThe one that picks from what happens rather than what was declared. It counts the sizes it is sent and moves between a ring and a work-stealing deque while running; Shape(), Traffic() and MaybeChangeShape() show and drive it.

KvMap.Insert answers whether the key was new rather than what it held, because that is what the map underneath reports, and there is no way to take a key out because it has no removal. HashMap is the one to reach for when entries have to go away.

New-SubEthaQosPolicy writes down what a stream needs, from settings or from a -Preset (Streaming, ReliablePubSub, PersistentLog), and its Snapshot() says where the bytes should live and whether the ordering must change. Ordering is set by the caller and never inferred from the traffic, because whether a reader needs one sender’s order or every sender’s order is something only the application knows.

Rings and channels

CmdletObjectWhat it is
New-SubEthaRing, Open-SubEthaRingSubEtha.RingThe adaptive ring: producers and consumers register for an id, payloads larger than a slot go through SendFrame and RecvFrame, and the shape changes under the traffic. Built with -Stamps Counter it marks each item with the order its sender made it in. With -Managed it also runs a shape sidecar of its own, scanning every -ScanIntervalUs microseconds, 250 when not given, and SidecarMorphs() counts the morphs that sidecar made.
New-SubEthaSpscRingSubEtha.SpscRingOne writer, one reader.
New-SubEthaBroadcastRingSubEtha.BroadcastRingOne writer, many readers, each seeing everything.
New-SubEthaPubSubSubEtha.PubSub, SubEtha.SubscriberKeeps the last N and tells a slow reader what it lost, with an error named SubEthaLagged. Subscribe() and SubscribeFrom($position) hand out readers.
New-SubEthaMpscPoolSubEtha.MpscPoolMany writers, one reader: the Producers and the Consumer in one object.
New-SubEthaMpmcGridSubEtha.MpmcGridMany of each: the Producers and the Consumers.
New-SubEthaLamportPairSubEtha.LamportProducer, SubEtha.LamportConsumerThe Lamport pair, written as two objects in that order: $p, $c = New-SubEthaLamportPair ....

The constructors hand out every end together because the shape is what makes them correct; -Open on the pool, the grid and the pair attaches to one that exists.

Rings that change themselves

CmdletObjectWhat it is
New-SubEthaCapacityRingSubEtha.CapacityRingResizes with MorphTo without losing what is in flight: a reader keeps reading the superseded backing until it has drained. Prewarm, WarmCapacity, WarmHits and StalePops report the backing held ready, the morphs that took it, and the reads served from a superseded one.
New-SubEthaLocaleRingSubEtha.LocaleRingMoves between Anon, File and ShmFs with MigrateTo without its senders and readers reconnecting, carrying what it holds across the move.

Both take -Stamped, and report OrderingMode() as a SubEtha.OrderingMode: Unordered, MergeByStamp or MergeStrict.

Both also take -Managed, which starts a sidecar of the ring’s own scanning every -ScanIntervalUs microseconds. Neither names a default interval, so a managed one needs it, and -ScanIntervalUs without -Managed is refused. A managed CapacityRing doubles when 85 percent full and halves when 10 percent full, within 64 to 65536 slots and no sooner than 100 ms after its last resize; SidecarMorphs() and SidecarPrewarms() count what it did. A managed LocaleRing moves to the locale RequestLocale last asked for, no sooner than 250 ms after its last move, and SidecarMigrations() counts the moves. On a managed LocaleRing, MigrateTo is also what its sidecar is asked to keep, so the sidecar does not move the ring back.

The sidecar

One sidecar serves the process: a scan thread per NUMA node drains the observation ring of every registered object into its stats and, for an object registered with a policy, asks that policy which tag the object should run at.

Cmdlet or methodObjectWhat it is
$object.Observe(), $object.Observe($policy)SubEtha.RegistrationRegisters the object with the process’s sidecar. Stats() reads what the sidecar has drained as a SubEtha.InstanceStats, Tag() the tag the object runs at, and Close() or Dispose() unregisters it. An object has one registration at a time.
New-SubEthaAdaptiveSubEtha.AdaptiveAn adaptive object of the script’s own: Record puts an operation in its ring, and Tag() is what its policy last answered.
Get-SubEthaSidecarSubEtha.SidecarStatusHow many objects are registered, the most it holds at once, and its scan threads.
Set-SubEthaSidecar -MaxInstancesSubEtha.SidecarStatus with -PassThruThe most objects the sidecar holds at once, 10,000 until changed. Observe past it is refused.
Invoke-SubEthaSidecarScanSubEtha.SidecarStatus with -PassThruHas every scan thread scan now and waits for it, so what was recorded before the call has been counted.

Twenty-six structures take Observe: Arena, Atomic, BitVec, BlockedBloomFilter, BloomFilter, BroadcastRing, CountMinSketch, EpochBarrier, FenceClock, Graph, HandleTable, HashMap, Heartbeat, Histogram, HyperLogLog, LeaderElection, OwnerLease, RWLock, RateLimiter, Reservoir, Ring, Semaphore, TimePointTile, TopologyMap, Universal and VersionChain, and Adaptive does too. Every structure keeps its own layout whatever tag a policy answers, so on a structure a policy moves a tag nothing reads; a policy that decides something is one on an Adaptive, whose script reads Tag() to choose how it works.

A policy is a ScriptBlock taking the stats and the current tag and answering one tag, an integer from 0 to 4294967295, or nothing to stay. It runs on the sidecar’s scan thread in a runspace its registration opens for it, so it sees none of the registering session’s variables, and it is asked after every scan that drained something new. One that throws, or answers anything else, leaves the tag where it was and is counted by PolicyErrors(), with LastPolicyError() holding the error record.

Order

ObjectWhat it is
SubEtha.OrderedReceiverDelivers a ring’s items in the order their senders made them, as SubEtha.StampedItem records. Built by $ring.OrderedReceiver($consumer), which refuses a ring with no stamps rather than returning nothing forever. It picks its own strategy and Strategy() says which.
SubEtha.ReorderWindowThe same window over items from anywhere else, from New-SubEthaReorderWindow: push with a stamp, take the smallest first, and the window widens itself when something arrives further out of order than it covered.

OrderedReceiver.Recv answers $null for two different reasons, the window still filling and the ring being empty, so Drain is the shape to reach for: it takes the ring and the held-back tail in one call.

Shared state

Atomic (Load, Store, FetchAdd, FetchSub, the bitwise operations, Swap and CompareExchange, each taking a SubEtha.MemoryOrder), Cell, Vec, Slab, HashMap, BTreeMap, LinkedList, Deque, Stack, Arena, Region and FrameRegion, each from its New-SubEtha... and Open-SubEtha... pair.

Region.Snapshot() copies every slot into one byte[] in one pass. The seqlocked families (Vec, Slab) have ReadRange and WriteRange instead, which move a run of elements packed end to end while honoring the retry on every one.

State with a history

CmdletObjectWhat it is
New-SubEthaVersionChainSubEtha.VersionChainOne value’s versions. ReadAt($version) gets the value as it stood then.
New-SubEthaVersionedSlabSubEtha.VersionedSlab, SubEtha.SlabPinNumbered slots each keeping their recent history, read through a pin from Pin() that fixes one epoch for the whole slab. History reports each version with the epochs it was current between.
New-SubEthaVersionedMapSubEtha.VersionedMap, SubEtha.MapPinAn ordered index from one unsigned integer to another, scanned through a pin that sees one unchanging view while writers carry on. Scan answers SubEtha.Entry records; ScanFrom also says where to carry on.
New-SubEthaLanedMapSubEtha.LanedMap, SubEtha.LaneClaim, SubEtha.LanedPinThat map split across several trees so several writers work at once. A writer claims a lane with ClaimLane() or ClaimLaneFor($key); reading takes none, and a scan merges every lane in key order.
New-SubEthaEpochsSubEtha.EpochsThe epoch table the above share.

Two names here mean what the Rust says rather than what they sound like. VoidEpoch is not reclamation: it undoes the writes stamped at exactly one epoch and makes what they superseded current again, for a writer that died partway through. Sweep and SweepSlot are the ones that reclaim, and a live pin holds the horizon still, so a sweep during a scan frees only what was already unreachable when the scan began.

A key belongs to one lane of a LanedMap for its whole life, because a lane is a separate tree. Writing it through another is an error named SubEthaWrongLane naming the right one, rather than a report of a row gone that no reader has stopped seeing.

Coordination

CmdletObjectWhat it is
New-SubEthaRWLockSubEtha.RWLock, SubEtha.HoldRead and write holds as objects rather than tokens. ReadFor and WriteFor give up after a timeout in seconds and answer $null; TryRead and TryWrite answer $null at once.
New-SubEthaSemaphoreSubEtha.Semaphore, SubEtha.PermitHoldA counting semaphore across processes. AcquireFor gives up after a timeout.
New-SubEthaCondvar, Wait-SubEthaConditionSubEtha.CondvarWhose condition, a script block, really is run from inside the wait. A script block runs only on the pipeline thread, so the wait is a cmdlet rather than a method: Wait-SubEthaCondition -Path $path -Until { $ready } -Timeout 5.
New-SubEthaLazyValueSubEtha.LazyValueComputed once across every process: Claim, Publish, Wait.
New-SubEthaOwnerLeaseSubEtha.OwnerLease, SubEtha.LeaseHoldOne process owns a small value and another takes it over when that one dies.
New-SubEthaHeartbeat, New-SubEthaEpochBarrier, New-SubEthaLeaderElection, New-SubEthaHolderTable, New-SubEthaFenceClock, New-SubEthaSharedArc, New-SubEthaNotifierSetSubEtha.Heartbeat, SubEtha.EpochBarrier, SubEtha.LeaderElection, SubEtha.HolderTable, SubEtha.FenceClock, SubEtha.SharedArc, SubEtha.NotifierSet, SubEtha.NotifierThe rest of the coordination surface. A barrier comes from a heartbeat’s Barrier($path) or from the cmdlet, which opens the heartbeat table itself.

The waiting forms that take a timeout sleep rather than spin, so a long wait costs no processor, and they never wait past the deadline even if whoever holds the lock never gives it back.

A lease changes hands two ways and a caller has to know them apart. A process whose id is lower than the owner’s takes it on the spot, beaten or not, which is what settles who leads when several processes start together. A process whose id is higher waits out the grace period, counted in epochs the holders step themselves with TickEpoch; nothing steps it on its own. Every method that names a process takes a pid argument that defaults to this process; pass another only to stand in for a process that is not here.

Probabilistic

BloomFilter, BlockedBloomFilter, CountMinSketch, HyperLogLog, Histogram, RateLimiter, BitVec and Reservoir.

Measure-SubEthaBloomSize -Items 10000 -FalsePositiveRate 0.01 works the bits and hash count out from the item count and the rate of wrong yeses that can be lived with, for either filter (-Blocked for the one whose bits for one item share a cache line), and Measure-SubEthaSketchSize -Epsilon 0.01 -Delta 0.01 does the same for the sketch. Reservoir keeps a bounded unbiased sample of a stream of any length; a refused value there is how the sample stays unbiased.

BitVec.Toggle answers the new value, while Set and Clear answer the previous one.

Specialist

ObjectWhat it is
SubEtha.HandleTableValues reached by a handle carrying both the place and how often it has been reused, so a handle to a removed value does not follow the place to its new occupant.
SubEtha.TimePointTileSixteen values each stamped with the version it was written at; Visible($version) answers what a reader at that version sees.
SubEtha.TowerValues reached by a path of uint down through levels, where each level checks that it still points at the next number in the path and refuses at the first that does not.
SubEtha.GraphNodes and the directed edges between them, in mapped files; Neighbors answers SubEtha.Neighbor records.
SubEtha.TopologyMapCounts who sends to whom and reads the shape off those counts as a SubEtha.Topology: PointToPoint, BroadcastTree or AllToAllMesh. Reading a recommendation does not publish it.
SubEtha.UniversalA set stored as a list while that is quicker to walk and moved to a map when it is not; Strategy() says which.

Reaching another machine

New-SubEthaSensSender and New-SubEthaSensReceiver are the two ends of a link that keeps working as the network gets worse. The link sends more than the items, so a reader rebuilds what was lost without asking again, and it changes between the sliding code and the block code as the measured loss moves, without either end reconnecting.

$reader = New-SubEthaSensReceiver -LocalPort 9000 -MaxItemSize 1024
$writer = New-SubEthaSensSender -PeerHost 10.0.0.5 -PeerPort 9000 -MaxItemSize 1024
$writer.SendMany(@('one', 'two'))

# Items do not arrive one at a time. Poll answers whatever the link
# could rebuild this time round, which may be nothing.
foreach ($item in $reader.Poll()) { ... }

Two things a reader has to know. Under the block code items are grouped in eights and none of a group is delivered until enough of it has arrived, so a run shorter than a group waits. And SensSender.Loss() is $null until the reader has reported anything, which is not the same as zero.

The bridges

New-SubEthaTcpBridgeClient with New-SubEthaTcpBridgeServer, and New-SubEthaQuicBridgeClient with New-SubEthaQuicBridgeServer, carry a whole ring to another host. Each takes the ring’s path and geometry and opens its own handle on it, so the ring object a script holds and the bridge are two handles on one file. Run($items) on a client and AcceptOne() on a server each block until the other end finishes.

A PowerShell module ships as one artifact with no way to ask for an extra, so both bridges are always built in; Get-SubEthaTransport lists sens, tcp and quic.

A QUIC certificate comes from New-SubEthaSelfSignedCert -Name $name as a SubEtha.Certificate holding the certificate and its key as byte[]: the reading end holds both and proves itself with them, the sending end holds the certificate alone and checks the reading end against it. Carrying it between hosts is the caller’s to arrange.

Sensing

Eight objects that are fed measurements and answer what they worked out from them. They hold no shared memory and touch no network: they are the arithmetic a transport does on its own numbers, which is why they are worth having whether or not a SubEtha link produced the numbers. Each comes from its New-SubEtha... cmdlet.

ObjectWhat it answers
SubEtha.LossKindWhether a loss came from noise or from a queue overflowing, as a SubEtha.LossClass. The two want opposite responses, so reading one as the other is expensive.
SubEtha.LossBurstsWhether losses arrive alone or in runs, and how long a run lasts.
SubEtha.TimingJitter, spacing, and whether delay is climbing. TrendDebiased takes a constant difference between two clocks out.
SubEtha.RoundTripShapeWhether round trips fall into two groups, which is what a radio retry looks like from outside.
SubEtha.PeriodicityWhether interference arrives on a beat, and when the next spike is due.
SubEtha.CapacityThe narrowest link on the path and what is free on it, from probes sent in pairs and in trains.
SubEtha.ForecastWhat the next interval is likely to carry.
SubEtha.PathChangesWhether the route moved under the traffic, which otherwise reads as congestion.

Every one answers $null rather than a number until it has seen enough to say anything.

Values that ride beside a pointer

ObjectWhat it is
SubEtha.TinyBloomA whole bloom filter in one machine word; its Bits property is the whole state, and New-SubEthaTinyBloom -Bits $n rebuilds it. About eight keys.
SubEtha.FineBloomThe same in four words, for about sixty-four keys.
SubEtha.ClockWall-clock time that still orders two events sharing a reading. Merge is what a receiver does with a sender’s clock so the received event orders after its cause. A clock never changes; every operation answers a new one.
SubEtha.CausalClockOne count per participant, answering before, after, equal, or concurrent.

The pointer types themselves are not here. Each holds a raw pointer or a reference count into Rust memory, and a script has no such value to point at.

Every cmdlet

Every cmdlet the module exports, with what it obtains. Each also answers to a short name with the SE prefix, so New-SubEthaRing is New-SERing. Get-Help <name> -Full gives the parameters and an example for any of them.

CmdletObtains
New-SubEthaAdaptiveQueueSubEtha.AdaptiveQueue
New-SubEthaArena, Open-SubEthaArenaSubEtha.Arena
New-SubEthaAtomic, Open-SubEthaAtomicSubEtha.Atomic
New-SubEthaBitVec, Open-SubEthaBitVecSubEtha.BitVec
New-SubEthaBlockedBloomFilter, Open-SubEthaBlockedBloomFilterSubEtha.BlockedBloomFilter
New-SubEthaBloomFilter, Open-SubEthaBloomFilterSubEtha.BloomFilter
New-SubEthaBroadcastRing, Open-SubEthaBroadcastRingSubEtha.BroadcastRing
New-SubEthaBTreeMap, Open-SubEthaBTreeMapSubEtha.BTreeMap
New-SubEthaCapacitySubEtha.Capacity
New-SubEthaCapacityRing, Open-SubEthaCapacityRingSubEtha.CapacityRing
New-SubEthaCausalClockSubEtha.CausalClock
New-SubEthaCell, Open-SubEthaCellSubEtha.Cell
New-SubEthaChannel, Open-SubEthaChannelSubEtha.Channel
New-SubEthaClockSubEtha.Clock
New-SubEthaCondvar, Open-SubEthaCondvarSubEtha.Condvar
New-SubEthaCountMinSketch, Open-SubEthaCountMinSketchSubEtha.CountMinSketch
New-SubEthaDeque, Open-SubEthaDequeSubEtha.Deque
New-SubEthaEpochBarrier, Open-SubEthaEpochBarrierSubEtha.EpochBarrier
New-SubEthaEpochs, Open-SubEthaEpochsSubEtha.Epochs
New-SubEthaFenceClock, Open-SubEthaFenceClockSubEtha.FenceClock
New-SubEthaFineBloomSubEtha.FineBloom
New-SubEthaForecastSubEtha.Forecast
New-SubEthaFrameRegion, Open-SubEthaFrameRegionSubEtha.FrameRegion
Get-SubEthaTransportSystem.String
New-SubEthaGraph, Open-SubEthaGraphSubEtha.Graph
New-SubEthaHandleTable, Open-SubEthaHandleTableSubEtha.HandleTable
New-SubEthaHashMap, Open-SubEthaHashMapSubEtha.HashMap
New-SubEthaHeartbeat, Open-SubEthaHeartbeatSubEtha.Heartbeat
New-SubEthaHistogram, Open-SubEthaHistogramSubEtha.Histogram
New-SubEthaHolderTable, Open-SubEthaHolderTableSubEtha.HolderTable
New-SubEthaHyperLogLog, Open-SubEthaHyperLogLogSubEtha.HyperLogLog
New-SubEthaKvMapSubEtha.KvMap
New-SubEthaLamportPairSubEtha.LamportConsumer, SubEtha.LamportProducer
New-SubEthaLanedMap, Open-SubEthaLanedMapSubEtha.LanedMap
New-SubEthaLazyValue, Open-SubEthaLazyValueSubEtha.LazyValue
New-SubEthaLeaderElection, Open-SubEthaLeaderElectionSubEtha.LeaderElection
New-SubEthaLinkedList, Open-SubEthaLinkedListSubEtha.LinkedList
New-SubEthaLocaleRing, Open-SubEthaLocaleRingSubEtha.LocaleRing
New-SubEthaLossBurstsSubEtha.LossBursts
New-SubEthaLossKindSubEtha.LossKind
New-SubEthaLruCache, Open-SubEthaLruCacheSubEtha.LruCache
Measure-SubEthaBloomSizeSubEtha.BloomSize
Measure-SubEthaSketchSizeSubEtha.SketchSize
New-SubEthaMpmcGridSubEtha.MpmcGrid
New-SubEthaMpscPoolSubEtha.MpscPool
New-SubEthaNotifierSetSubEtha.NotifierSet
New-SubEthaOwnerLease, Open-SubEthaOwnerLeaseSubEtha.OwnerLease
New-SubEthaPathChangesSubEtha.PathChanges
New-SubEthaPeriodicitySubEtha.Periodicity
New-SubEthaPubSub, Open-SubEthaPubSubSubEtha.PubSub
New-SubEthaQosPolicySubEtha.QosPolicy
New-SubEthaQuicBridgeClientSubEtha.QuicBridgeClient
New-SubEthaQuicBridgeServerSubEtha.QuicBridgeServer
New-SubEthaRateLimiter, Open-SubEthaRateLimiterSubEtha.RateLimiter
Receive-SubEthaItemSystem.Byte[]
New-SubEthaRegion, Open-SubEthaRegionSubEtha.Region
New-SubEthaReorderWindowSubEtha.ReorderWindow
New-SubEthaReservoir, Open-SubEthaReservoirSubEtha.Reservoir
New-SubEthaRing, Open-SubEthaRingSubEtha.Ring
New-SubEthaRoundTripShapeSubEtha.RoundTripShape
New-SubEthaRWLock, Open-SubEthaRWLockSubEtha.RWLock
New-SubEthaSelfSignedCertSubEtha.Certificate
New-SubEthaSemaphore, Open-SubEthaSemaphoreSubEtha.Semaphore
Send-SubEthaItem
New-SubEthaSensReceiverSubEtha.SensReceiver
New-SubEthaSensSenderSubEtha.SensSender
New-SubEthaSharedArc, Open-SubEthaSharedArcSubEtha.SharedArc
New-SubEthaSlab, Open-SubEthaSlabSubEtha.Slab
New-SubEthaSpscRing, Open-SubEthaSpscRingSubEtha.SpscRing
New-SubEthaStack, Open-SubEthaStackSubEtha.Stack
New-SubEthaTcpBridgeClientSubEtha.TcpBridgeClient
New-SubEthaTcpBridgeServerSubEtha.TcpBridgeServer
New-SubEthaTimePointTile, Open-SubEthaTimePointTileSubEtha.TimePointTile
New-SubEthaTimingSubEtha.Timing
New-SubEthaTinyBloomSubEtha.TinyBloom
New-SubEthaTopologyMap, Open-SubEthaTopologyMapSubEtha.TopologyMap
New-SubEthaTower, Open-SubEthaTowerSubEtha.Tower
New-SubEthaUniversal, Open-SubEthaUniversalSubEtha.Universal
New-SubEthaVec, Open-SubEthaVecSubEtha.Vec
New-SubEthaVersionChain, Open-SubEthaVersionChainSubEtha.VersionChain
New-SubEthaVersionedMap, Open-SubEthaVersionedMapSubEtha.VersionedMap
New-SubEthaVersionedSlab, Open-SubEthaVersionedSlabSubEtha.VersionedSlab
Wait-SubEthaConditionSystem.Boolean
New-SubEthaWorkQueue, Open-SubEthaWorkQueueSubEtha.WorkQueue

Threads and lifetimes

Every method call on one object is serialized by the object, so an object handed to another runspace or thread is used safely, and the bridges’ tests run a server’s AcceptOne in a second runspace beside the client’s Run. A call that waits holds its object until it returns, so whatever ends the wait arrives through another object, and a server’s LocalAddr() is read before its AcceptOne starts. A hold, a pin or a claim can be released from any thread, including the finalizer’s; every value the module holds is safe to send between threads, and the build refuses one that is not.

An OrderedReceiver, a SlabPin, a MapPin, a LanedPin or a LaneClaim keeps the structure it came from alive for as long as it lives, so disposing the structure first is safe: the mapping goes when the last of them does. Inside the binding the borrowed guard is declared before the handle that keeps its target alive, because Rust drops fields in declaration order.

Building and testing

The binding is written against a framework whose attributes turn a Rust struct into a cmdlet or a class, and whose build tool generates the managed shell around the library for both hosts:

cd crates/subetha-pwrs
cargo pwrs build --release          # target/pwrs/SubEtha/
cargo pwrs test --release           # cargo test, the build, then Pester in pwsh and Windows PowerShell

The Pester suites in tests/ cover every family, in both hosts. They reach every cmdlet, class, enum and method the module exports, and a gate suite fails the run when one of those names is exercised nowhere, when a cmdlet has no SE alias resolving to it, or when a synopsis is not a whole sentence. Others attach a second time through every Open- and reset form, drive a second process of the same host over shared structures, and run several runspaces at once against one object and against handles of their own.

Only the native library under runtimes/<rid>/native/ differs per platform, so each is built on its own machine and cargo pwrs merge target/pwrs/SubEtha <folder built elsewhere> folds them into one module. Every machine needs the same cargo pwrs version as well as the same checkout, because the manifest gained fields between releases of the tool and merge refuses two that differ. cargo install --list on each host settles which version it has; the binary’s file date does not, because the newest file can be the oldest version.

Built from one commit, everything but the native comes out byte-identical whichever machine builds it: the manifest, the format file, the script, the help and both shells. Built on Windows in pwsh 7.6.6, on Linux in pwsh 7.6.5 and on FreeBSD in pwsh 7.5.5, every one of those files matched, so any platform’s build can be the folder the others are merged into (how the binding works ).

All 183 tests pass on each of four platforms, each running the folder it built:

PlatformHostNative
Windows x64pwsh 7.6.6 and Windows PowerShell 5.1win-x64/subetha_pwrs.dll
Linux x64pwsh 7.6.5linux-x64/libsubetha_pwrs.so
macOS arm64pwsh 7.6.5osx-arm64/libsubetha_pwrs.dylib
FreeBSD x64pwsh 7.5.5 on .NET 9freebsd-x64/libsubetha_pwrs.so

The folder built on Linux in pwsh 7.6.5 passes the same 183 there in pwsh 7.5.11 and 7.4.20 as well.

FreeBSD is the one that needs arranging, for two reasons that are not this module’s. Building it needs PWRS_TOOLSET=5.3.0, because the C# compiler the tool fetches by default wants .NET 10 and FreeBSD packages nothing past 9. Running the suite needs Pester’s platform check answered: Pester decides the platform from three booleans that are all false there and throws rather than guessing. It reads them with Get-Variable, which resolves through the scope chain, so a global $IsLinux answers it without modifying Pester. cargo pwrs test imports whatever PWRS_PESTER_PATH names in place of Pester, so a module that sets the global and then imports Pester lets the command run unchanged:

# PesterIsLinuxShim.psm1
Set-Variable -Name IsLinux -Value $true -Scope Global -Force
Import-Module Pester -RequiredVersion 5.7.1 -Global -ErrorAction Stop
export PWRS_TOOLSET=5.3.0
export PWRS_PESTER_PATH="$HOME/PesterIsLinuxShim.psm1"
cargo pwrs test --release