using Iceoryx2;
// Create a node
var nodeResult = NodeBuilder.New()
.Name("my_node")
.Create();
if (!nodeResult.IsOk)
{
Console.WriteLine($"Failed to create node: {nodeResult}");
return;
}
using var node = nodeResult.Unwrap();
// Open or create a service for pub/sub
var serviceResult = node.ServiceBuilder()
.PublishSubscribe<int>()
.Open("MyService");
if (!serviceResult.IsOk)
{
Console.WriteLine($"Failed to open service: {serviceResult}");
return;
}
using var service = serviceResult.Unwrap();
// Publisher example
var publisherResult = service.CreatePublisher();
if (!publisherResult.IsOk)
{
Console.WriteLine($"Failed to create publisher: {publisherResult}");
return;
}
using var publisher = publisherResult.Unwrap();
var sampleResult = publisher.Loan();
if (!sampleResult.IsOk)
{
Console.WriteLine($"Failed to loan sample: {sampleResult}");
return;
}
using var sample = sampleResult.Unwrap();
sample.Payload = 42;
var sendResult = sample.Send();
if (!sendResult.IsOk)
{
Console.WriteLine($"Failed to send: {sendResult}");
}
// Subscriber example
var subscriberResult = service.CreateSubscriber();
if (!subscriberResult.IsOk)
{
Console.WriteLine($"Failed to create subscriber: {subscriberResult}");
return;
}
using var subscriber = subscriberResult.Unwrap();
var receiveResult = subscriber.Receive();
if (!receiveResult.IsOk)
{
Console.WriteLine($"Failed to receive: {receiveResult}");
return;
}
var receivedSample = receiveResult.Unwrap();
if (receivedSample != null)
{
Console.WriteLine($"Received: {receivedSample.Payload}");
}The bindings provide true zero-copy access to shared memory through ref
returns, eliminating intermediate copies when reading or writing payload data.
using var publisher = service.CreatePublisher().Unwrap();
using var sample = publisher.Loan<MyData>().Unwrap();
// Direct zero-copy access to shared memory
ref var payload = ref sample.GetPayloadRef();
payload.Field1 = 42;
payload.Field2 = 3.14;
sample.Send();using var subscriber = service.CreateSubscriber().Unwrap();
var result = subscriber.Receive<MyData>();
if (result.IsOk)
{
using var sample = result.Unwrap();
if (sample != null)
{
// Direct zero-copy read from shared memory
ref readonly var payload = ref sample.GetPayloadRefReadOnly();
Console.WriteLine($"Field1: {payload.Field1}, Field2: {payload.Field2}");
}
}Sample<T> provides three ways to access payload data:
-
Payloadproperty - Copies data (backward compatible)sample.Payload = new MyData { Field1 = 42 }; // Copy on write var data = sample.Payload; // Copy on read
-
GetPayloadRef()- Zero-copy mutable access (loaned samples only)ref var payload = ref sample.GetPayloadRef(); payload.Field1 = 42; // Direct modification in shared memory
-
GetPayloadRefReadOnly()- Zero-copy read-only access (any sample)ref readonly var payload = ref sample.GetPayloadRefReadOnly(); Console.WriteLine(payload.Field1); // Direct read from shared memory
Performance Benefits:
- ✅ No marshaling overhead
- ✅ No intermediate allocations
- ✅ Direct memory access
- ✅ Ideal for large structs or high-frequency updates
using Iceoryx2;
using Iceoryx2.Event;
// Create a node
var nodeResult = NodeBuilder.New()
.Name("event_node")
.Create();
if (!nodeResult.IsOk)
{
Console.WriteLine($"Failed to create node: {nodeResult}");
return;
}
using var node = nodeResult.Unwrap();
// Open or create an event service
var serviceResult = node.ServiceBuilder()
.Event()
.Open("MyEventService");
if (!serviceResult.IsOk)
{
Console.WriteLine($"Failed to open event service: {serviceResult}");
return;
}
using var service = serviceResult.Unwrap();
// Notifier example (event sender)
var notifierResult = service.CreateNotifier(defaultEventId: new EventId(100));
if (!notifierResult.IsOk)
{
Console.WriteLine($"Failed to create notifier: {notifierResult}");
return;
}
using var notifier = notifierResult.Unwrap();
var notifyResult = notifier.Notify(new EventId(5));
if (!notifyResult.IsOk)
{
Console.WriteLine($"Failed to notify: {notifyResult}");
}
// Listener example (event receiver)
var listenerResult = service.CreateListener();
if (!listenerResult.IsOk)
{
Console.WriteLine($"Failed to create listener: {listenerResult}");
return;
}
using var listener = listenerResult.Unwrap();
// Non-blocking wait
var tryWaitResult = listener.TryWait();
if (!tryWaitResult.IsOk)
{
Console.WriteLine($"Failed to wait: {tryWaitResult}");
return;
}
var eventId = tryWaitResult.Unwrap();
if (eventId.HasValue)
{
Console.WriteLine($"Received event: {eventId.Value}");
}
// Timed wait (1 second timeout)
var timedWaitResult = listener.TimedWait(TimeSpan.FromSeconds(1));
if (!timedWaitResult.IsOk)
{
Console.WriteLine($"Failed to wait: {timedWaitResult}");
return;
}
var timedEventId = timedWaitResult.Unwrap();
if (timedEventId.HasValue)
{
Console.WriteLine($"Received event: {timedEventId.Value}");
}
else
{
Console.WriteLine("Timeout - no event received");
}
// Blocking wait
var blockingWaitResult = listener.BlockingWait();
if (!blockingWaitResult.IsOk)
{
Console.WriteLine($"Failed to wait: {blockingWaitResult}");
return;
}
var blockingEventId = blockingWaitResult.Unwrap();
Console.WriteLine($"Received event: {blockingEventId}");The WaitSet enables efficient monitoring of multiple event sources
simultaneously without polling. It uses OS-level primitives
(epoll on Linux, kqueue on macOS) to wake only when events arrive.
- No CPU polling - Uses OS-level event notification, not busy loops
- Multiple sources - Monitor many listeners in a single wait call
- Signal handling - Built-in support for graceful shutdown (Ctrl+C)
- Async integration - Run WaitSet in background task
using Iceoryx2;
// Create node and event services
var node = NodeBuilder.New().Create().Unwrap();
var service1 = node.ServiceBuilder()
.Event()
.Open("events1")
.Unwrap();
var service2 = node.ServiceBuilder()
.Event()
.Open("events2")
.Unwrap();
// Create listeners
using var listener1 = service1.CreateListener().Unwrap();
using var listener2 = service2.CreateListener().Unwrap();
// Create WaitSet with signal handling
using var waitset = WaitSetBuilder.New()
.SignalHandling(SignalHandlingMode.TerminationAndInterrupt)
.Create()
.Unwrap();
// Attach listeners to WaitSet
using var guard1 = waitset.AttachNotification(listener1).Unwrap();
using var guard2 = waitset.AttachNotification(listener2).Unwrap();
// Event processing callback
CallbackProgression OnEvent(WaitSetAttachmentId attachmentId)
{
if (attachmentId.HasEventFrom(guard1))
{
// CRITICAL: Consume ALL pending events to avoid busy loop
while (listener1.TryWait().Unwrap() is { } eventId)
{
Console.WriteLine($"Service 1 event: {eventId.Value}");
}
}
else if (attachmentId.HasEventFrom(guard2))
{
while (listener2.TryWait().Unwrap() is { } eventId)
{
Console.WriteLine($"Service 2 event: {eventId.Value}");
}
}
return CallbackProgression.Continue;
}
// Run event loop (blocks until signal received)
waitset.WaitAndProcess(OnEvent);The WaitSet wakes when events are available. You must consume ALL pending events in your callback:
// ❌ WRONG: Only consumes one event - causes busy loop!
if (attachmentId.HasEventFrom(guard))
{
var eventId = listener.TryWait().Unwrap();
Console.WriteLine($"Event: {eventId}");
}
// ✅ CORRECT: Consume all pending events
if (attachmentId.HasEventFrom(guard))
{
while (true)
{
var result = listener.TryWait();
if (!result.IsOk) break;
var eventIdOpt = result.Unwrap();
if (!eventIdOpt.HasValue) break;
Console.WriteLine($"Event: {eventIdOpt.Value}");
}
}If events aren't fully consumed, the file descriptor remains ready and the WaitSet immediately wakes again, creating a CPU-burning busy loop.
The Request-Response API provides a complete client-server RPC implementation with support for both convenience methods and zero-copy operations.
using Iceoryx2;
using Iceoryx2.RequestResponse;
using System.Runtime.InteropServices;
// Define request and response types
[StructLayout(LayoutKind.Sequential)]
public struct AddRequest
{
public int Value;
}
[StructLayout(LayoutKind.Sequential)]
public struct AddResponse
{
public int Sum;
}
// Create a node
var nodeResult = NodeBuilder.New()
.Name("rpc_node")
.Create();
if (!nodeResult.IsOk)
{
Console.WriteLine($"Failed to create node: {nodeResult}");
return;
}
using var node = nodeResult.Unwrap();
// Open or create a request-response service
var serviceResult = node.ServiceBuilder()
.RequestResponse<AddRequest, AddResponse>()
.Open("AddService");
if (!serviceResult.IsOk)
{
Console.WriteLine($"Failed to open service: {serviceResult}");
return;
}
using var service = serviceResult.Unwrap();
// Client example - send request and wait for response
var clientResult = service.CreateClient();
if (!clientResult.IsOk)
{
Console.WriteLine($"Failed to create client: {clientResult}");
return;
}
using var client = clientResult.Unwrap();
// Option 1: SendCopy() - Convenience method that copies data
var pendingResult = client.SendCopy(new AddRequest { Value = 42 });
if (!pendingResult.IsOk)
{
Console.WriteLine($"Failed to send request: {pendingResult}");
return;
}
using var pending = pendingResult.Unwrap();
// Wait for response with timeout (non-blocking, timed, or blocking)
var responseResult = pending.TimedReceive(TimeSpan.FromSeconds(2));
if (!responseResult.IsOk)
{
Console.WriteLine($"Failed to receive response: {responseResult}");
return;
}
var response = responseResult.Unwrap();
if (response != null)
{
using (response)
{
Console.WriteLine($"Response sum: {response.Payload.Sum}");
}
}
else
{
Console.WriteLine("Request timed out");
}
// Option 2: Loan() - Zero-copy method for better performance
var loanResult = client.Loan();
if (loanResult.IsOk)
{
using var request = loanResult.Unwrap();
request.Payload = new AddRequest { Value = 42 };
var sendResult = request.Send();
if (sendResult.IsOk)
{
using var pendingResponse = sendResult.Unwrap();
// Handle response...
}
}
// Server example - receive request and send response
var serverResult = service.CreateServer();
if (!serverResult.IsOk)
{
Console.WriteLine($"Failed to create server: {serverResult}");
return;
}
using var server = serverResult.Unwrap();
while (true)
{
var requestResult = server.Receive();
if (!requestResult.IsOk)
{
Console.WriteLine($"Failed to receive request: {requestResult}");
break;
}
var request = requestResult.Unwrap();
if (request != null)
{
using (request)
{
int value = request.Payload.Value;
// Option 1: SendCopyResponse() - Convenience method
var sendResult = request.SendCopyResponse(new AddResponse { Sum = value + 100 });
if (!sendResult.IsOk)
{
Console.WriteLine($"Failed to send response: {sendResult}");
}
// Option 2: LoanResponse() - Zero-copy method
// var loanResult = request.LoanResponse();
// if (loanResult.IsOk)
// {
// using var response = loanResult.Unwrap();
// response.Payload = new AddResponse { Sum = value + 100 };
// response.Send();
// }
}
}
Thread.Sleep(100); // Small delay between checks
}Key Features:
- ✅ Fully verified FFI signatures matching the C API exactly
- ✅ Both convenience methods (
SendCopy,SendCopyResponse) and zero-copy methods (Loan,LoanResponse) - ✅ Three response waiting modes: non-blocking (
TryReceive), timed (TimedReceive), and blocking (BlockingReceive) - ✅ Proper memory management with automatic cleanup
- ✅ Type-safe request/response handling with generic types
The bindings support complex data types using sequential layout:
using System.Runtime.InteropServices;
using Iceoryx2;
[StructLayout(LayoutKind.Sequential)]
[Iox2Type("TransmissionData")] // Optional: specify custom type name
public struct TransmissionData
{
public int X;
public int Y;
public double Value;
}
// Use with publish-subscribe
var service = node.ServiceBuilder()
.PublishSubscribe<TransmissionData>()
.Open("ComplexDataService")
.Unwrap();
using var publisher = service.CreatePublisher().Unwrap();
using var sample = publisher.Loan().Unwrap();
sample.Payload = new TransmissionData
{
X = 10,
Y = 20,
Value = 3.14
};
sample.Send();iceoryx2 provides extensive configuration options for fine-tuning service behavior. These settings control memory usage, buffer sizes, and performance characteristics.
var service = node.ServiceBuilder()
.PublishSubscribe<MyData>()
// Maximum number of subscribers that can connect (default: 8)
.MaxSubscribers(16)
// Maximum number of publishers that can connect (default: 2)
.MaxPublishers(4)
// Subscriber buffer size - how many samples each subscriber can hold (default: 2)
.SubscriberMaxBufferSize(10)
// Maximum samples a subscriber can borrow at once (default: 2)
.SubscriberMaxBorrowedSamples(3)
// History size for late-joining subscribers (default: 0)
.HistorySize(5)
// Enable safe overflow - oldest sample replaced when buffer full (default: true)
.EnableSafeOverflow(true)
.Open("MyService")
.Unwrap();var publisher = service.CreatePublisherBuilder()
// Maximum samples the publisher can loan at once (default: 2)
.MaxLoanedSamples(4)
.Create()
.Unwrap();var subscriber = service.CreateSubscriberBuilder()
// Override the buffer size for this specific subscriber
.BufferSize(20)
.Create()
.Unwrap();| Setting | Low Memory | High Throughput | Reliability |
|---|---|---|---|
SubscriberMaxBufferSize |
1-2 | 10+ | 5+ |
HistorySize |
0 | 0 | 5+ |
MaxLoanedSamples |
1 | 4+ | 2 |
EnableSafeOverflow |
true | true | false |
Key Considerations:
- Memory Usage: Each subscriber buffer consumes
buffer_size × payload_sizebytes - History Size: Useful for late-joining subscribers, but increases memory
- Safe Overflow: When enabled, slow subscribers won't block fast publishers
- Loaned Samples: Higher values allow more concurrent writes but use more memory
The C# bindings provide full async/await support for all blocking operations, enabling modern asynchronous programming patterns with proper cancellation support.
- Non-blocking - Operations yield to the thread pool instead of blocking threads
- Composable - Use
Task.WhenAll(),Task.WhenAny()for concurrent operations - Cancellable - All async methods accept
CancellationTokenfor cooperative cancellation - Efficient - Better thread pool utilization compared to polling with
Thread.Sleep()
All classes with blocking operations provide async equivalents:
// Synchronous methods (block the calling thread)
Result<Response<T>?, Iox2Error> TryReceive()
Result<Response<T>?, Iox2Error> TimedReceive(TimeSpan timeout)
Result<Response<T>, Iox2Error> BlockingReceive()
// Asynchronous methods (yield to thread pool)
Task<Result<Response<T>?, Iox2Error>> ReceiveAsync(TimeSpan timeout, CancellationToken ct = default)
Task<Result<Response<T>, Iox2Error>> ReceiveAsync(CancellationToken ct = default)// Synchronous methods (block the calling thread)
Result<EventId?, Iox2Error> TryWait()
Result<EventId?, Iox2Error> TimedWait(TimeSpan timeout)
Result<EventId, Iox2Error> BlockingWait()
// Asynchronous methods (offload to background thread)
Task<Result<EventId?, Iox2Error>> WaitAsync(TimeSpan timeout, CancellationToken ct = default)
Task<Result<EventId, Iox2Error>> WaitAsync(CancellationToken ct = default)// Synchronous method (non-blocking poll)
Result<Sample<T>?, Iox2Error> Receive<T>()
// Asynchronous methods (poll with yielding to thread pool)
Task<Result<Sample<T>?, Iox2Error>> ReceiveAsync<T>(TimeSpan timeout, CancellationToken ct = default)
Task<Result<Sample<T>, Iox2Error>> ReceiveAsync<T>(CancellationToken ct = default)Note: Subscriber async methods use polling (every 10ms) since the native API doesn't provide blocking receive. However, they yield to the thread pool efficiently.
using System;
using System.Threading;
using System.Threading.Tasks;
using Iceoryx2;
using Iceoryx2.RequestResponse;
public async Task RunClientAsync(CancellationToken cancellationToken = default)
{
// Create node and service (same as sync version)
var node = NodeBuilder.New()
.Name("async_client")
.Create()
.Unwrap();
using var service = node.ServiceBuilder()
.RequestResponse<ulong, MyResponse>()
.Open("MyService")
.Unwrap();
using var client = service.CreateClient().Unwrap();
// Send request
var sendResult = client.SendCopy(42ul);
using var pendingResponse = sendResult.Unwrap();
// Wait for response asynchronously with timeout
var responseResult = await pendingResponse.ReceiveAsync(
TimeSpan.FromSeconds(2),
cancellationToken);
if (responseResult.IsOk)
{
var response = responseResult.Unwrap();
if (response != null)
{
using (response)
{
Console.WriteLine($"Received: {response.Payload}");
}
}
else
{
Console.WriteLine("Request timed out");
}
}
}using System;
using System.Threading;
using System.Threading.Tasks;
using Iceoryx2;
public async Task RunListenerAsync(CancellationToken cancellationToken = default)
{
var node = NodeBuilder.New()
.Name("async_listener")
.Create()
.Unwrap();
using var service = node.ServiceBuilder()
.Event()
.Open("MyEvents")
.Unwrap();
using var listener = service.CreateListener().Unwrap();
// Wait for events asynchronously
while (!cancellationToken.IsCancellationRequested)
{
var result = await listener.WaitAsync(
TimeSpan.FromSeconds(5),
cancellationToken);
if (result.IsOk)
{
var eventId = result.Unwrap();
if (eventId.HasValue)
{
Console.WriteLine($"Received event: {eventId.Value}");
}
else
{
Console.WriteLine("Timeout - no event");
}
}
}
}1. Use async methods in async contexts:
// ✅ GOOD: Async all the way
public async Task ProcessDataAsync()
{
var response = await pendingResponse.ReceiveAsync(TimeSpan.FromSeconds(1));
// ... process response
}
// ❌ BAD: Blocking in async method
public async Task ProcessDataAsync()
{
var response = pendingResponse.TimedReceive(TimeSpan.FromSeconds(1)); // Blocks!
}2. Always pass CancellationToken:
// ✅ GOOD: Cancellable operation
public async Task WorkAsync(CancellationToken ct)
{
var response = await pendingResponse.ReceiveAsync(TimeSpan.FromSeconds(10), ct);
}
// ⚠️ OK but less flexible: No cancellation
public async Task WorkAsync()
{
var response = await pendingResponse.ReceiveAsync(TimeSpan.FromSeconds(10));
}3. Use ConfigureAwait(false) in libraries:
// In library code, avoid capturing SynchronizationContext
var response = await pendingResponse
.ReceiveAsync(timeout, ct)
.ConfigureAwait(false);4. Combine with Task composition:
// Wait for multiple responses concurrently
var tasks = new[]
{
pending1.ReceiveAsync(timeout, ct),
pending2.ReceiveAsync(timeout, ct),
pending3.ReceiveAsync(timeout, ct)
};
var responses = await Task.WhenAll(tasks);
// Or race for the first response
var firstResponse = await Task.WhenAny(tasks);The C# bindings follow .NET naming conventions:
- Classes use PascalCase (e.g.,
Node,ServiceBuilder,EventService) - Methods use PascalCase (e.g.,
Create(),OpenOrCreate(),Notify()) - Properties use PascalCase (e.g.,
Name,Id,Payload) - Internal/Native types use the original C naming with
iox2_prefix - Result pattern uses
IsOkproperty andUnwrap()method for error handling
All fallible operations return a Result<T, Iox2Error> type:
var result = node.ServiceBuilder().Event().Open("MyService");
// Check for success
if (!result.IsOk)
{
Console.WriteLine($"Error: {result}");
return;
}
// Unwrap the value (only call after checking IsOk)
using var service = result.Unwrap();The bindings use a fluent builder pattern for configuration:
var node = NodeBuilder.New()
.Name("my_node")
.Create()
.Unwrap();
var service = node.ServiceBuilder()
.PublishSubscribe<int>()
.Open("MyService")
.Unwrap();
var publisher = service.CreatePublisher()
.Unwrap();The Service Discovery API allows you to dynamically discover running services and inspect their configurations. This is useful for monitoring, debugging, and building dynamic service-aware applications.
using Iceoryx2;
// Create a node (required to access service discovery)
using var node = NodeBuilder.New()
.Name("discovery_node")
.Create()
.Unwrap();
// List all running services
var services = node.List().Unwrap();
Console.WriteLine($"Found {services.Count} service(s):");
foreach (var service in services)
{
Console.WriteLine($" Service: {service.Name}");
Console.WriteLine($" ID: {service.Id}");
Console.WriteLine($" Pattern: {service.MessagingPattern}");
}Each service provides detailed configuration based on its messaging pattern:
foreach (var service in services)
{
switch (service.MessagingPattern)
{
case MessagingPattern.PublishSubscribe:
var pubSubConfig = service.PublishSubscribeConfig;
Console.WriteLine($" Max Publishers: {pubSubConfig.MaxPublishers}");
Console.WriteLine($" Max Subscribers: {pubSubConfig.MaxSubscribers}");
Console.WriteLine($" History Size: {pubSubConfig.HistorySize}");
Console.WriteLine($" Buffer Size: {pubSubConfig.SubscriberMaxBufferSize}");
break;
case MessagingPattern.Event:
var eventConfig = service.EventConfig;
Console.WriteLine($" Max Notifiers: {eventConfig.MaxNotifiers}");
Console.WriteLine($" Max Listeners: {eventConfig.MaxListeners}");
Console.WriteLine($" Max Event ID: {eventConfig.EventIdMaxValue}");
break;
case MessagingPattern.RequestResponse:
var rpcConfig = service.RequestResponseConfig;
Console.WriteLine($" Max Clients: {rpcConfig.MaxClients}");
Console.WriteLine($" Max Servers: {rpcConfig.MaxServers}");
break;
}
}- Monitoring dashboards - Display real-time service status
- Service health checks - Verify expected services are running
- Dynamic routing - Route messages based on available services
- Debugging - Inspect service configurations to diagnose issues
- Service mesh integration - Build service registries and load balancers
The C# bindings implement proper memory management with multiple layers of safety:
- All native resources implement
IDisposable- ensures cleanup even if exceptions occur - Use
usingstatements to ensure proper cleanup of resources SafeHandletypes protect against resource leaks and race conditions- Automatic finalization for cleanup if
Dispose()is not called (though explicit disposal is recommended) - No manual memory management required - the bindings handle all FFI marshalling
// ✅ GOOD: Using statement ensures disposal
using var node = NodeBuilder.New().Create().Unwrap();
using var service = node.ServiceBuilder().Event().Open("MyService").Unwrap();
using var notifier = service.CreateNotifier().Unwrap();
// ✅ GOOD: Explicit disposal in try-finally
var node = NodeBuilder.New().Create().Unwrap();
try
{
// Use node...
}
finally
{
node.Dispose();
}
// ❌ BAD: No disposal - relies on finalizer (slower, not deterministic)
var node = NodeBuilder.New().Create().Unwrap();
// ... use node without disposing- ✅ Publish-Subscribe - One-to-many data distribution with zero-copy
- ✅ Event - Lightweight notification system with custom event IDs
- ✅ Request-Response - Client-server RPC with async response handling
- 🚧 Pipeline - Coming soon
- ✅ macOS (tested on Apple Silicon and Intel)
- ✅ Linux (x86_64, ARM64)
- ✅ Windows (x86_64)
- ✅ Primitive types - int, uint, long, ulong, float, double, bool
- ✅ Complex types - Structs with
[StructLayout(LayoutKind.Sequential)] - ✅ Custom type names - Use
[Iox2Type("name")]attribute ⚠️ Zero-copy - Requires sequential layout and unmanaged types
If you get a DllNotFoundException, ensure:
- The native library is built:
cargo build --release --package iceoryx2-ffi-c - The library is in one of these locations:
- Same directory as your executable
- System library path (
/usr/lib,/usr/local/lib, etc.) - Path specified in
LD_LIBRARY_PATH(Linux),DYLD_LIBRARY_PATH(macOS), orPATH(Windows)
If services can't connect, verify type names match:
// Use Iox2Type attribute to ensure consistent naming
[Iox2Type("MyData")]
public struct MyData { ... }For complex types, the bindings automatically generate length-prefixed names
(e.g., 16TransmissionData for a 16-character struct name). Primitive types use
Rust naming (i32, u64, etc.).
- Ensure all resources use
usingstatements or are properly disposed - Don't access samples after calling
Send()orDispose() - Use
Result<T, E>pattern - always checkIsOkbefore callingUnwrap()
The repository includes several complete examples:
Location: examples/PublishSubscribe/
Demonstrates basic pub/sub pattern with primitive types:
- Publisher sends incrementing counter values
- Subscriber receives and displays values
- Shows proper resource management with
usingstatements
Location: examples/Event/
Demonstrates event-based communication:
- Notifier sends events with custom event IDs (0-11)
- Listener receives events with timeout support
- Shows three wait modes: non-blocking, timed, and blocking
Location: examples/ComplexDataTypes/
Demonstrates zero-copy sharing of complex structs:
- Defines custom
TransmissionDatastruct - Shows struct layout and type naming
- Demonstrates cross-process struct sharing
Location: examples/RequestResponse/
Demonstrates client-server RPC pattern with fully verified C API compatibility:
- Client sends
AddRequestmessages with integer values - Server maintains a running sum and responds with
AddResponse - Shows async response handling with three wait modes (non-blocking, timed, blocking)
- Demonstrates both
SendCopy()convenience method andLoan()/LoanResponse()for zero-copy - FFI signatures verified to exactly match the C API for reliable operation
Location: examples/AsyncPubSub/
Demonstrates modern async/await patterns for publish-subscribe:
- Async publisher using
await Task.Delay()instead of blocking - Async subscriber with timeout using
ReceiveAsync() - Async subscriber blocking until data arrives
- Multiple concurrent subscribers processing data in parallel
- Proper cancellation support with
CancellationToken - Shows best practices for async IPC in modern C# applications
Run with:
# Terminal 1 - Async publisher
cd examples/AsyncPubSub
dotnet run --framework net9.0 publisher
# Terminal 2 - Async subscriber with timeout
dotnet run --framework net9.0 subscriber
# Or try other modes: blocking, multi
dotnet run --framework net9.0 blocking
dotnet run --framework net9.0 multiLocation: examples/WaitSetMultiplexing/
Demonstrates efficient event multiplexing using WaitSet:
- Monitor multiple event services in a single wait call
- Uses OS-level primitives (epoll/kqueue) for efficient waiting
- Signal handling for graceful shutdown (Ctrl+C)
- Shows proper event consumption pattern to avoid busy loops
Run with:
# Terminal 1 - Wait on multiple services
cd examples/WaitSetMultiplexing
dotnet run --framework net9.0 wait service1 service2
# Terminal 2 - Send events
dotnet run --framework net9.0 notify 42 service1
dotnet run --framework net9.0 notify 100 service2Location: examples/ServiceDiscovery/
Demonstrates dynamic service discovery:
- List all running services in the system
- Inspect service configurations (publishers, subscribers, buffer sizes)
- Identify messaging patterns (Pub/Sub, Event, Request-Response)
- Useful for monitoring and debugging
Run with:
# First, start some services in another terminal (e.g., PublishSubscribe example)
# Then run the discovery example:
cd examples/ServiceDiscovery
dotnet run --framework net9.0