diff --git a/Iceoryx2.sln b/Iceoryx2.sln index 32a0c80..b0495b1 100644 --- a/Iceoryx2.sln +++ b/Iceoryx2.sln @@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskCommunication", "exampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArrayOfStructs", "examples\ArrayOfStructs\ArrayOfStructs.csproj", "{3F880AD9-6564-44C4-BCE0-4B841AD46F4A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blackboard", "examples\Blackboard\Blackboard.csproj", "{9AE203AB-E8BB-45F0-8754-A7F63974931B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -283,6 +285,18 @@ Global {3F880AD9-6564-44C4-BCE0-4B841AD46F4A}.Release|x64.Build.0 = Release|Any CPU {3F880AD9-6564-44C4-BCE0-4B841AD46F4A}.Release|x86.ActiveCfg = Release|Any CPU {3F880AD9-6564-44C4-BCE0-4B841AD46F4A}.Release|x86.Build.0 = Release|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Debug|x64.ActiveCfg = Debug|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Debug|x64.Build.0 = Debug|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Debug|x86.ActiveCfg = Debug|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Debug|x86.Build.0 = Debug|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Release|Any CPU.Build.0 = Release|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Release|x64.ActiveCfg = Release|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Release|x64.Build.0 = Release|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Release|x86.ActiveCfg = Release|Any CPU + {9AE203AB-E8BB-45F0-8754-A7F63974931B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -305,5 +319,6 @@ Global {B8D63E2A-B1F4-47A4-8793-B117D9649369} = {BC3A1E43-FC68-4656-A0AC-705B63F41D3C} {7F8E9D0A-1B2C-3D4E-5F6A-7B8C9D0E1F2A} = {BC3A1E43-FC68-4656-A0AC-705B63F41D3C} {3F880AD9-6564-44C4-BCE0-4B841AD46F4A} = {BC3A1E43-FC68-4656-A0AC-705B63F41D3C} + {9AE203AB-E8BB-45F0-8754-A7F63974931B} = {BC3A1E43-FC68-4656-A0AC-705B63F41D3C} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 3da3b95..629d06e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ C# / .NET bindings for iceoryx2 - Zero-Copy Lock-Free IPC * ✅ **Event API** - Complete notifier/listener implementation with blocking/timed waits * ✅ **Request-Response API** - Complete client/server RPC with verified FFI signatures +* ✅ **Blackboard API** - Key-value store pattern for shared state monitoring * ✅ **Complex Data Types** - Full support for custom structs with sequential layout * ✅ **Async/Await Support** - Modern async methods for all blocking operations with CancellationToken @@ -38,7 +39,7 @@ and provide idiomatic C# APIs with full memory safety. * 🧹 **Memory-safe** - Automatic resource management via SafeHandle and IDisposable * 🎯 **Idiomatic C#** - Builder pattern, Result types, LINQ-friendly APIs * 🔧 **Cross-platform** - Works on Linux, macOS, and Windows -* 📦 **Multiple patterns** - Publish-Subscribe, Event, and Request-Response communication +* 📦 **Multiple patterns** - Publish-Subscribe, Event, Request-Response, and Blackboard communication * ⚡ **Async/Await** - Full async support with CancellationToken for modern C# applications * 🔍 **Service Discovery** - Dynamically discover and monitor running services * 🌐 **Domain Isolation** - Separate communication groups for multi-tenant deployments @@ -77,13 +78,14 @@ iceoryx2 uses **shared memory** for true zero-copy communication: ### Services and Communication Patterns iceoryx2 organizes communication through **services**. Each service has a unique -name and supports one of three communication patterns: +name and supports one of four communication patterns: | Pattern | Description | Use Case | |---------|-------------|----------| | **Publish-Subscribe** | Many-to-many data distribution | Sensor data, telemetry, state broadcasts | | **Event** | Lightweight notifications with event IDs | Wake-up signals, state changes, triggers | | **Request-Response** | Client-server RPC | Commands, queries, configuration updates | +| **Blackboard** | Shared key-value store with latest values | State monitoring, configuration sharing, sensor fusion | ### Nodes @@ -331,6 +333,18 @@ cd examples/Event dotnet run -- listener ``` +**Blackboard Example:** + +```bash +# Terminal 1 - Run creator +cd examples/Blackboard +dotnet run -- creator + +# Terminal 2 - Run opener +cd examples/Blackboard +dotnet run -- opener +``` + ### Alternative: Use the Build Script A convenience build script is provided that handles all steps: @@ -371,6 +385,7 @@ iceoryx2-csharp/ │ │ ├── PublishSubscribe/ # Pub/Sub messaging pattern │ │ ├── Event/ # Event-based communication │ │ ├── RequestResponse/ # Request-Response (RPC) pattern +│ │ ├── Blackboard/ # Blackboard key-value store pattern │ │ └── Types/ # Common types and utilities │ └── Iceoryx2.Reactive/ # Reactive Extensions support ├── examples/ # C# examples @@ -378,6 +393,7 @@ iceoryx2-csharp/ │ ├── ComplexDataTypes/ # Complex struct example │ ├── Event/ # Event API example │ ├── RequestResponse/ # Request-Response RPC example +│ ├── Blackboard/ # Blackboard key-value store example │ ├── AsyncPubSub/ # Async/await patterns example │ ├── WaitSetMultiplexing/ # Event multiplexing with WaitSet │ └── ServiceDiscovery/ # Service discovery and monitoring diff --git a/examples/Blackboard/Blackboard.csproj b/examples/Blackboard/Blackboard.csproj new file mode 100644 index 0000000..aa938f3 --- /dev/null +++ b/examples/Blackboard/Blackboard.csproj @@ -0,0 +1,37 @@ + + + + Exe + enable + true + net8.0;net9.0 + + + + + + ../../src/Iceoryx2/bin/$(Configuration)/$(TargetFramework) + + + + + + + + + + + + + + + diff --git a/examples/Blackboard/Program.cs b/examples/Blackboard/Program.cs new file mode 100644 index 0000000..eb38d9b --- /dev/null +++ b/examples/Blackboard/Program.cs @@ -0,0 +1,300 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2; +using Iceoryx2.Blackboard; +using System; +using System.Runtime.InteropServices; + +namespace Blackboard; + +/// +/// Key type for blackboard entries - represents different sensors +/// +public enum SensorKey : int +{ + Temperature = 0, + Humidity = 1, + Pressure = 2 +} + +/// +/// Value type for sensor data +/// +[StructLayout(LayoutKind.Sequential)] +public struct SensorData +{ + public double Value; + public long Timestamp; + public int Quality; +} + +class Program +{ + const string SERVICE_NAME = "Blackboard/Sensors"; + + static void Main(string[] args) + { + if (args.Length == 0 || (args[0] != "creator" && args[0] != "opener")) + { + Console.WriteLine("Usage: Blackboard [creator|opener]"); + Console.WriteLine(""); + Console.WriteLine(" creator - Create the blackboard and write sensor data"); + Console.WriteLine(" opener - Open the blackboard and read sensor data"); + return; + } + + if (args[0] == "creator") + { + RunCreator(); + } + else + { + RunOpener(); + } + } + + static void RunCreator() + { + Console.WriteLine("Starting blackboard creator..."); + + // Create node + var nodeResult = NodeBuilder.New() + .Name("blackboard_creator") + .Create(); + + if (!nodeResult.IsOk) + { + Console.WriteLine($"Failed to create node: {nodeResult}"); + return; + } + + using var node = nodeResult.Unwrap(); + + // Define initial entries + var entries = new[] + { + new BlackboardEntry( + SensorKey.Temperature, + new SensorData { Value = 20.0, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Quality = 100 }), + new BlackboardEntry( + SensorKey.Humidity, + new SensorData { Value = 50.0, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Quality = 100 }), + new BlackboardEntry( + SensorKey.Pressure, + new SensorData { Value = 1013.25, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Quality = 100 }) + }; + + // Create blackboard service with key comparer + var serviceResult = node.ServiceBuilder() + .Blackboard((a, b) => a == b) + .Create(SERVICE_NAME, entries); + + if (!serviceResult.IsOk) + { + Console.WriteLine($"Failed to create blackboard service: {serviceResult}"); + return; + } + + using var service = serviceResult.Unwrap(); + + // Create writer + var writerResult = service.CreateWriter(); + if (!writerResult.IsOk) + { + Console.WriteLine($"Failed to create writer: {writerResult}"); + return; + } + + using var writer = writerResult.Unwrap(); + + Console.WriteLine("Blackboard created. Writing sensor data..."); + Console.WriteLine("Press Ctrl+C to stop."); + + var random = new Random(); + int iteration = 0; + + // Main loop - update sensor values using node.Wait() for proper signal handling + while (node.Wait(TimeSpan.FromSeconds(1)) == NodeWaitResult.Ok) + { + iteration++; + + // Update temperature sensor + { + var entryResult = writer.Entry(SensorKey.Temperature); + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var data = new SensorData + { + Value = 20.0 + random.NextDouble() * 10.0, // 20-30 degrees + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Quality = 100 + }; + var updateResult = entry.Update(data); + if (updateResult.IsOk) + { + Console.WriteLine($"[{iteration}] Temperature: {data.Value:F2}C"); + } + } + } + + // Update humidity sensor + { + var entryResult = writer.Entry(SensorKey.Humidity); + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var data = new SensorData + { + Value = 40.0 + random.NextDouble() * 40.0, // 40-80% + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Quality = 95 + }; + var updateResult = entry.Update(data); + if (updateResult.IsOk) + { + Console.WriteLine($"[{iteration}] Humidity: {data.Value:F2}%"); + } + } + } + + // Update pressure sensor + { + var entryResult = writer.Entry(SensorKey.Pressure); + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var data = new SensorData + { + Value = 1010.0 + random.NextDouble() * 20.0, // 1010-1030 hPa + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Quality = 98 + }; + var updateResult = entry.Update(data); + if (updateResult.IsOk) + { + Console.WriteLine($"[{iteration}] Pressure: {data.Value:F2} hPa"); + } + } + } + + Console.WriteLine(); + } + + Console.WriteLine("Shutting down creator..."); + } + + static void RunOpener() + { + Console.WriteLine("Starting blackboard opener..."); + + // Create node + var nodeResult = NodeBuilder.New() + .Name("blackboard_opener") + .Create(); + + if (!nodeResult.IsOk) + { + Console.WriteLine($"Failed to create node: {nodeResult}"); + return; + } + + using var node = nodeResult.Unwrap(); + + // Open blackboard service + var serviceResult = node.ServiceBuilder() + .Blackboard((a, b) => a == b) + .Open(SERVICE_NAME); + + if (!serviceResult.IsOk) + { + Console.WriteLine($"Failed to open blackboard service: {serviceResult}"); + Console.WriteLine("Make sure the creator is running first."); + return; + } + + using var service = serviceResult.Unwrap(); + + // Create reader + var readerResult = service.CreateReader(); + if (!readerResult.IsOk) + { + Console.WriteLine($"Failed to create reader: {readerResult}"); + return; + } + + using var reader = readerResult.Unwrap(); + + Console.WriteLine("Blackboard opened. Reading sensor data..."); + Console.WriteLine("Press Ctrl+C to stop."); + + int iteration = 0; + + // Main loop - read sensor values using node.Wait() for proper signal handling + while (node.Wait(TimeSpan.FromMilliseconds(500)) == NodeWaitResult.Ok) + { + iteration++; + + Console.WriteLine($"--- Reading #{iteration} ---"); + + // Read temperature + { + var entryResult = reader.Entry(SensorKey.Temperature); + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var valueResult = entry.Get(); + if (valueResult.IsOk) + { + var data = valueResult.Unwrap(); + Console.WriteLine($" Temperature: {data.Value:F2}C (quality: {data.Quality})"); + } + } + } + + // Read humidity + { + var entryResult = reader.Entry(SensorKey.Humidity); + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var valueResult = entry.Get(); + if (valueResult.IsOk) + { + var data = valueResult.Unwrap(); + Console.WriteLine($" Humidity: {data.Value:F2}% (quality: {data.Quality})"); + } + } + } + + // Read pressure + { + var entryResult = reader.Entry(SensorKey.Pressure); + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var valueResult = entry.Get(); + if (valueResult.IsOk) + { + var data = valueResult.Unwrap(); + Console.WriteLine($" Pressure: {data.Value:F2} hPa (quality: {data.Quality})"); + } + } + } + + Console.WriteLine(); + } + + Console.WriteLine("Shutting down opener..."); + } +} \ No newline at end of file diff --git a/examples/Blackboard/README.md b/examples/Blackboard/README.md new file mode 100644 index 0000000..dc285c2 --- /dev/null +++ b/examples/Blackboard/README.md @@ -0,0 +1,76 @@ +# Blackboard Example + +This example demonstrates the **blackboard pattern** in iceoryx2, which provides a shared key-value store for inter-process communication. Multiple processes can read and write data to specific keys in the blackboard, making it ideal for scenarios where multiple components need to access the latest state of various data points. + +## Use Case + +The example simulates a sensor monitoring system with three sensors: +- **Temperature** sensor (20-30°C) +- **Humidity** sensor (40-80%) +- **Pressure** sensor (1010-1030 hPa) + +Each sensor reading includes a value, timestamp, and quality indicator. + +## How It Works + +The blackboard pattern allows: +- One **creator** process that initializes the blackboard with sensor entries and continuously updates them +- Multiple **opener** processes that can read the current sensor values independently + +Unlike publish-subscribe, the blackboard provides access to the **latest value** rather than a stream of all updates. This makes it efficient for state monitoring where only the current reading matters. + +## Running the Example + +### Terminal 1 - Start the Creator +```bash +dotnet run -- creator +``` + +The creator will: +1. Create a blackboard service named "Blackboard/Sensors" +2. Initialize three sensor entries (Temperature, Humidity, Pressure) +3. Update sensor values every second with random data + +### Terminal 2 - Start the Opener +```bash +dotnet run -- opener +``` + +The opener will: +1. Connect to the existing blackboard service +2. Read all sensor values every 500ms +3. Display the current readings + +You can run multiple openers simultaneously to see independent access to the same data. + +## Key Concepts + +### Blackboard Entry +Each entry consists of a key-value pair: +- **Key**: `SensorKey` enum (Temperature, Humidity, Pressure) +- **Value**: `SensorData` struct containing measurement data + +### Writer Operations +```csharp +var entry = writer.Entry(SensorKey.Temperature); +entry.Update(newData); +``` + +### Reader Operations +```csharp +var entry = reader.Entry(SensorKey.Temperature); +var data = entry.Get(); +``` + +## Comparison with Pub-Sub + +| Blackboard | Publish-Subscribe | +|------------|-------------------| +| Latest value only | All messages | +| Multiple independent keys | Single message type | +| Direct key access | Sequential message stream | +| Ideal for state monitoring | Ideal for event streams | + +## Stopping the Example + +Press `Ctrl+C` in either terminal to stop the respective process. The blackboard service will remain available as long as the creator is running. diff --git a/src/Iceoryx2.Reactive/ReaderExtensions.cs b/src/Iceoryx2.Reactive/ReaderExtensions.cs new file mode 100644 index 0000000..f8c3b25 --- /dev/null +++ b/src/Iceoryx2.Reactive/ReaderExtensions.cs @@ -0,0 +1,166 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2.Blackboard; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Iceoryx2.Reactive; + +/// +/// Provides extension methods to integrate iceoryx2 Blackboard Reader with Reactive Extensions (Rx). +/// Enables declarative, composable, and asynchronous data stream processing using IObservable<T>. +/// +public static class ReaderExtensions +{ + /// + /// Converts a blackboard entry into an IObservable<TValue> stream that polls for value changes. + /// This enables declarative Rx-style programming with LINQ operators (Where, Select, Buffer, etc.). + /// + /// The type of the key. + /// The type of the value. + /// The blackboard reader. + /// The key to observe. + /// Optional polling interval (default: 10ms). Lower values reduce latency but increase CPU usage. + /// Optional cancellation token to stop the observable stream. + /// An IObservable<TValue> that emits the current value on each poll. + /// + /// + /// using var subscription = reader.AsObservable<MyKey, MyValue>(key) + /// .DistinctUntilChanged() + /// .Subscribe(value => Console.WriteLine($"Value changed: {value}")); + /// + /// + public static IObservable AsObservable( + this Reader reader, + TKey key, + TimeSpan? pollingInterval = null, + CancellationToken cancellationToken = default) + where TKey : unmanaged + where TValue : unmanaged + { + if (reader == null) + throw new ArgumentNullException(nameof(reader)); + + var interval = pollingInterval ?? TimeSpan.FromMilliseconds(10); + + return new ReaderObservable(reader, key, interval, cancellationToken); + } + + /// + /// Converts a blackboard entry into an async enumerable stream (IAsyncEnumerable<TValue>). + /// This enables use with C# 8.0+ async streams and await foreach. + /// + /// The type of the key. + /// The type of the value. + /// The blackboard reader. + /// The key to observe. + /// Optional polling interval (default: 10ms). + /// Optional cancellation token to stop the stream. + /// An IAsyncEnumerable<TValue> that yields the current value on each poll. + /// + /// + /// await foreach (var value in reader.AsAsyncEnumerable<MyKey, MyValue>(key, token)) + /// { + /// Console.WriteLine($"Current value: {value}"); + /// } + /// + /// + public static async IAsyncEnumerable AsAsyncEnumerable( + this Reader reader, + TKey key, + TimeSpan? pollingInterval = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + where TKey : unmanaged + where TValue : unmanaged + { + if (reader == null) + throw new ArgumentNullException(nameof(reader)); + + var interval = pollingInterval ?? TimeSpan.FromMilliseconds(10); + + while (!cancellationToken.IsCancellationRequested) + { + var entryResult = reader.Entry(key); + + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var valueResult = entry.Get(); + + if (valueResult.IsOk) + { + yield return valueResult.Unwrap(); + } + } + + await Task.Delay(interval, cancellationToken); + } + } + + /// + /// Converts a blackboard entry into an async enumerable stream that only yields when the value changes. + /// + /// The type of the key. + /// The type of the value. + /// The blackboard reader. + /// The key to observe. + /// Optional equality comparer for detecting changes. + /// Optional polling interval (default: 10ms). + /// Optional cancellation token to stop the stream. + /// An IAsyncEnumerable<TValue> that yields only when the value changes. + public static async IAsyncEnumerable AsDistinctAsyncEnumerable( + this Reader reader, + TKey key, + IEqualityComparer? equalityComparer = null, + TimeSpan? pollingInterval = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + where TKey : unmanaged + where TValue : unmanaged + { + if (reader == null) + throw new ArgumentNullException(nameof(reader)); + + var comparer = equalityComparer ?? EqualityComparer.Default; + var interval = pollingInterval ?? TimeSpan.FromMilliseconds(10); + var hasLastValue = false; + TValue lastValue = default; + + while (!cancellationToken.IsCancellationRequested) + { + var entryResult = reader.Entry(key); + + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var valueResult = entry.Get(); + + if (valueResult.IsOk) + { + var currentValue = valueResult.Unwrap(); + + if (!hasLastValue || !comparer.Equals(lastValue, currentValue)) + { + lastValue = currentValue; + hasLastValue = true; + yield return currentValue; + } + } + } + + await Task.Delay(interval, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2.Reactive/ReaderObservable.cs b/src/Iceoryx2.Reactive/ReaderObservable.cs new file mode 100644 index 0000000..6f73cf6 --- /dev/null +++ b/src/Iceoryx2.Reactive/ReaderObservable.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2.Blackboard; +using System; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; + +namespace Iceoryx2.Reactive; + +/// +/// Internal implementation of IObservable<TValue> for iceoryx2 Blackboard Reader. +/// Continuously polls the reader and pushes the current value to observers. +/// +/// The type of the key. +/// The type of the value. +internal sealed class ReaderObservable : IObservable + where TKey : unmanaged + where TValue : unmanaged +{ + private readonly Reader _reader; + private readonly TKey _key; + private readonly TimeSpan _pollingInterval; + private readonly CancellationToken _cancellationToken; + + public ReaderObservable(Reader reader, TKey key, TimeSpan pollingInterval, CancellationToken cancellationToken) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _key = key; + _pollingInterval = pollingInterval; + _cancellationToken = cancellationToken; + } + + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + throw new ArgumentNullException(nameof(observer)); + + var cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken); + + // Start polling task + var pollingTask = Task.Run(async () => + { + try + { + while (!cts.Token.IsCancellationRequested) + { + try + { + // Try to get the entry and read the value + var entryResult = _reader.Entry(_key); + + if (entryResult.IsOk) + { + using var entry = entryResult.Unwrap(); + var valueResult = entry.Get(); + + if (valueResult.IsOk) + { + // Push the value to the observer + observer.OnNext(valueResult.Unwrap()); + } + else + { + observer.OnError(new InvalidOperationException("Failed to read blackboard value")); + break; + } + } + else + { + // On error, notify observer and complete + observer.OnError(new InvalidOperationException("Failed to access blackboard entry")); + break; + } + } + catch (Exception ex) + { + observer.OnError(ex); + break; + } + + // Wait for next polling interval + await Task.Delay(_pollingInterval, cts.Token); + } + } + catch (OperationCanceledException) + { + // Expected on cancellation - complete gracefully + } + finally + { + observer.OnCompleted(); + } + }, cts.Token); + + // Return a disposable that cancels the polling task + return Disposable.Create(() => + { + cts.Cancel(); + try + { + pollingTask.Wait(TimeSpan.FromSeconds(1)); + } + catch (AggregateException) + { + // Expected if task was cancelled + } + cts.Dispose(); + }); + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Blackboard/BlackboardHelpers.cs b/src/Iceoryx2/Blackboard/BlackboardHelpers.cs new file mode 100644 index 0000000..1aeb6b6 --- /dev/null +++ b/src/Iceoryx2/Blackboard/BlackboardHelpers.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; + +namespace Iceoryx2.Blackboard; + +/// +/// Provides helper methods for blackboard operations. +/// +internal static class BlackboardHelpers +{ + /// + /// Gets the alignment for a given type. + /// For primitive types, returns the type size. + /// For struct types, returns the Pack value from StructLayoutAttribute if specified, otherwise IntPtr.Size. + /// + /// The type to get alignment for (must be unmanaged). + /// The size of the type in bytes. + /// The alignment value in bytes. + public static ulong GetAlignment(ulong typeSize) where T : unmanaged + { + if (typeof(T).IsPrimitive) + { + return typeSize; + } + else + { + var layoutAttr = typeof(T).StructLayoutAttribute; + if (layoutAttr != null && layoutAttr.Pack > 0) + { + return (ulong)layoutAttr.Pack; + } + else + { + return (ulong)IntPtr.Size; + } + } + } +} diff --git a/src/Iceoryx2/Blackboard/BlackboardService.cs b/src/Iceoryx2/Blackboard/BlackboardService.cs new file mode 100644 index 0000000..99a7820 --- /dev/null +++ b/src/Iceoryx2/Blackboard/BlackboardService.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2.SafeHandles; +using System; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.Blackboard; + +/// +/// Represents a blackboard service port factory. +/// The blackboard pattern provides a shared-memory key-value repository that can be +/// modified by one writer and read by many readers. +/// +/// The type of keys in the blackboard. +public sealed class BlackboardService : IDisposable + where TKey : unmanaged +{ + private readonly SafeBlackboardServiceHandle _handle; + private readonly Func _keyComparer; + private bool _disposed; + + internal BlackboardService(IntPtr handle, Func keyComparer) + { + _handle = new SafeBlackboardServiceHandle(handle); + _keyComparer = keyComparer; + } + + /// + /// Creates a new writer for this blackboard service. + /// Note: Only one writer is allowed per blackboard service. + /// + /// A Result containing the writer or an error. + public Result, Iox2Error> CreateWriter() + { + ThrowIfDisposed(); + + var handlePtr = _handle.DangerousGetHandle(); + var writerBuilderHandle = iox2_port_factory_blackboard_writer_builder( + ref handlePtr, + IntPtr.Zero); + + if (writerBuilderHandle == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.WriterCreationFailed); + } + + var result = iox2_port_factory_writer_builder_create( + writerBuilderHandle, + IntPtr.Zero, + out var writerHandle); + + if (result != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.WriterCreationFailed); + } + + return Result, Iox2Error>.Ok(new Writer(writerHandle, _keyComparer)); + } + + /// + /// Creates a new reader for this blackboard service. + /// Multiple readers are allowed per blackboard service. + /// + /// A Result containing the reader or an error. + public Result, Iox2Error> CreateReader() + { + ThrowIfDisposed(); + + var handlePtr = _handle.DangerousGetHandle(); + var readerBuilderHandle = iox2_port_factory_blackboard_reader_builder( + ref handlePtr, + IntPtr.Zero); + + if (readerBuilderHandle == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.ReaderCreationFailed); + } + + var result = iox2_port_factory_reader_builder_create( + readerBuilderHandle, + IntPtr.Zero, + out var readerHandle); + + if (result != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.ReaderCreationFailed); + } + + return Result, Iox2Error>.Ok(new Reader(readerHandle, _keyComparer)); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(BlackboardService)); + } + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + if (!_disposed) + { + _handle?.Dispose(); + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Blackboard/BlackboardServiceBuilder.cs b/src/Iceoryx2/Blackboard/BlackboardServiceBuilder.cs new file mode 100644 index 0000000..f7823ac --- /dev/null +++ b/src/Iceoryx2/Blackboard/BlackboardServiceBuilder.cs @@ -0,0 +1,345 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.Blackboard; + +/// +/// Represents a key-value entry to be added to a blackboard service during creation. +/// +/// The key type (must be unmanaged). +/// The value type (must be unmanaged). +public sealed class BlackboardEntry + where TKey : unmanaged + where TValue : unmanaged +{ + /// + /// Gets the key for this entry. + /// + public TKey Key { get; } + + /// + /// Gets the initial value for this entry. + /// + public TValue InitialValue { get; } + + /// + /// Creates a new blackboard entry. + /// + /// The key for this entry. + /// The initial value for this entry. + public BlackboardEntry(TKey key, TValue initialValue) + { + Key = key; + InitialValue = initialValue; + } +} + +/// +/// Builder for creating or opening blackboard services. +/// +/// The type of keys in the blackboard (must be unmanaged). +/// +/// Thread Safety: This class is not thread-safe. Instances should not be accessed concurrently from multiple threads. +/// Each instance should be used from a single thread, or external synchronization must be provided. +/// +public sealed class BlackboardServiceBuilder : IDisposable + where TKey : unmanaged +{ + private readonly Node _node; + private readonly Func _keyComparer; + + // Store the delegate to prevent garbage collection + private iox2_service_blackboard_key_eq_cmp_func? _nativeKeyComparer; + private GCHandle _keyComparerHandle; + private bool _disposed; + + internal BlackboardServiceBuilder(Node node, Func keyComparer) + { + _node = node; + _keyComparer = keyComparer; + } + + /// + /// Opens an existing blackboard service. + /// + /// The name of the service. + /// A Result containing the blackboard service or an error. + /// Thrown when serviceName is null. + /// Thrown when serviceName is empty or whitespace. + public unsafe Result, Iox2Error> Open(string serviceName) + { + ArgumentNullException.ThrowIfNull(serviceName); + if (string.IsNullOrWhiteSpace(serviceName)) + { + throw new ArgumentException("Service name cannot be empty or whitespace.", nameof(serviceName)); + } + + // Create service name + var serviceNameResult = iox2_service_name_new( + IntPtr.Zero, + serviceName, + serviceName.Length, + out var serviceNameHandle); + + if (serviceNameResult != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + try + { + var serviceNamePtr = iox2_cast_service_name_ptr(serviceNameHandle); + + // Get service builder + var nodeHandle = _node._handle.DangerousGetHandle(); + var serviceBuilderHandle = iox2_node_service_builder( + ref nodeHandle, + IntPtr.Zero, + serviceNamePtr); + + if (serviceBuilderHandle == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + // Get blackboard opener builder + var blackboardOpenerHandle = iox2_service_builder_blackboard_opener(serviceBuilderHandle); + + if (blackboardOpenerHandle == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + // Set key type details + var keyTypeName = ServiceBuilder.GetRustCompatibleTypeName(); + var keyTypeSize = (ulong)sizeof(TKey); + var keyTypeAlignment = BlackboardHelpers.GetAlignment(keyTypeSize); + + var keyResult = iox2_service_builder_blackboard_opener_set_key_type_details( + ref blackboardOpenerHandle, + keyTypeName, + keyTypeName.Length, + keyTypeSize, + keyTypeAlignment); + + if (keyResult != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + // Open the service + var result = iox2_service_builder_blackboard_open( + blackboardOpenerHandle, + IntPtr.Zero, + out var portFactoryHandle); + + if (result != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + return Result, Iox2Error>.Ok( + new BlackboardService(portFactoryHandle, _keyComparer)); + } + finally + { + iox2_service_name_drop(serviceNameHandle); + } + } + + /// + /// Creates a new blackboard service with the specified entries. + /// + /// The value type for entries. + /// The name of the service. + /// The key-value entries to add to the blackboard. + /// A Result containing the blackboard service or an error. + /// Thrown when serviceName or entries is null. + /// Thrown when serviceName is empty/whitespace or entries is empty. + public unsafe Result, Iox2Error> Create( + string serviceName, + IEnumerable> entries) + where TValue : unmanaged + { + ArgumentNullException.ThrowIfNull(serviceName); + ArgumentNullException.ThrowIfNull(entries); + + if (string.IsNullOrWhiteSpace(serviceName)) + { + throw new ArgumentException("Service name cannot be empty or whitespace.", nameof(serviceName)); + } + + var entryList = entries.ToList(); + if (entryList.Count == 0) + { + throw new ArgumentException("At least one entry is required to create a blackboard service.", nameof(entries)); + } + + // Create service name + var serviceNameResult = iox2_service_name_new( + IntPtr.Zero, + serviceName, + serviceName.Length, + out var serviceNameHandle); + + if (serviceNameResult != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + try + { + var serviceNamePtr = iox2_cast_service_name_ptr(serviceNameHandle); + + // Get service builder + var nodeHandle = _node._handle.DangerousGetHandle(); + var serviceBuilderHandle = iox2_node_service_builder( + ref nodeHandle, + IntPtr.Zero, + serviceNamePtr); + + if (serviceBuilderHandle == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + // Get blackboard creator builder + var blackboardCreatorHandle = iox2_service_builder_blackboard_creator(serviceBuilderHandle); + + if (blackboardCreatorHandle == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + // Set key type details + var keyTypeName = ServiceBuilder.GetRustCompatibleTypeName(); + var keyTypeSize = (ulong)sizeof(TKey); + var keyTypeAlignment = BlackboardHelpers.GetAlignment(keyTypeSize); + + var keyResult = iox2_service_builder_blackboard_creator_set_key_type_details( + ref blackboardCreatorHandle, + keyTypeName, + keyTypeName.Length, + keyTypeSize, + keyTypeAlignment); + + if (keyResult != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + // Set key comparison function + _nativeKeyComparer = CreateNativeKeyComparer(); + _keyComparerHandle = GCHandle.Alloc(_nativeKeyComparer); + iox2_service_builder_blackboard_creator_set_key_eq_comparison_function( + ref blackboardCreatorHandle, + _nativeKeyComparer); + + // Add entries - keep memory alive until service is created + var valueTypeName = ServiceBuilder.GetRustCompatibleTypeName(); + var valueTypeSize = (ulong)sizeof(TValue); + var valueTypeAlignment = BlackboardHelpers.GetAlignment(valueTypeSize); + + // Collect all allocated memory to free after service creation + var allocatedMemory = new List(); + + try + { + foreach (var entry in entryList) + { + // Allocate memory for key and value + var keyPtr = Marshal.AllocHGlobal(sizeof(TKey)); + var valuePtr = Marshal.AllocHGlobal(sizeof(TValue)); + allocatedMemory.Add(keyPtr); + allocatedMemory.Add(valuePtr); + + // Copy key and value to unmanaged memory using unsafe pointer operations + // (Marshal.StructureToPtr doesn't work with enums and some value types) + var key = entry.Key; + var value = entry.InitialValue; + *(TKey*)keyPtr = key; + *(TValue*)valuePtr = value; + + // Note: iox2_service_builder_blackboard_creator_add returns void + iox2_service_builder_blackboard_creator_add( + ref blackboardCreatorHandle, + keyPtr, + valuePtr, + null, // No release callback - we manage memory ourselves + valueTypeName, + valueTypeName.Length, + valueTypeSize, + valueTypeAlignment); + } + + // Create the service + var result = iox2_service_builder_blackboard_create( + blackboardCreatorHandle, + IntPtr.Zero, + out var portFactoryHandle); + + if (result != IOX2_OK) + { + return Result, Iox2Error>.Err(Iox2Error.BlackboardServiceCreationFailed); + } + + return Result, Iox2Error>.Ok( + new BlackboardService(portFactoryHandle, _keyComparer)); + } + finally + { + // Free all allocated memory after service creation + foreach (var ptr in allocatedMemory) + { + Marshal.FreeHGlobal(ptr); + } + } + } + finally + { + iox2_service_name_drop(serviceNameHandle); + } + } + + private unsafe iox2_service_blackboard_key_eq_cmp_func CreateNativeKeyComparer() + { + return (lhs, rhs) => + { + // Use unsafe pointer dereference instead of Marshal.PtrToStructure + // (Marshal.PtrToStructure doesn't work with enums and some value types) + var leftKey = *(TKey*)lhs; + var rightKey = *(TKey*)rhs; + return _keyComparer(leftKey, rightKey); + }; + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + if (!_disposed) + { + if (_keyComparerHandle.IsAllocated) + { + _keyComparerHandle.Free(); + } + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Blackboard/EntryHandle.cs b/src/Iceoryx2/Blackboard/EntryHandle.cs new file mode 100644 index 0000000..859d76e --- /dev/null +++ b/src/Iceoryx2/Blackboard/EntryHandle.cs @@ -0,0 +1,89 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2.SafeHandles; +using System; +using System.Runtime.InteropServices; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.Blackboard; + +/// +/// A read-only entry handle for accessing a value in the blackboard. +/// +/// The type of the key. +/// The type of the value. +public sealed class EntryHandle : IDisposable + where TKey : unmanaged + where TValue : unmanaged +{ + private readonly SafeEntryHandleHandle _handle; + private readonly TKey _key; + private bool _disposed; + + internal EntryHandle(IntPtr handle, TKey key) + { + _handle = new SafeEntryHandleHandle(handle); + _key = key; + } + + /// + /// Gets the key associated with this entry handle. + /// + public TKey Key => _key; + + /// + /// Gets the current value from the blackboard entry. + /// + /// A Result containing the current value or an error. + public unsafe Result Get() + { + ThrowIfDisposed(); + + var valueSize = (ulong)sizeof(TValue); + var valueAlignment = BlackboardHelpers.GetAlignment(valueSize); + + // Stack allocate for small fixed-size value + TValue* valuePtr = stackalloc TValue[1]; + + var handlePtr = _handle.DangerousGetHandle(); + // Note: iox2_entry_handle_get returns void + iox2_entry_handle_get( + ref handlePtr, + (IntPtr)valuePtr, + valueSize, + valueAlignment); + + // Use unsafe pointer dereference + return Result.Ok(*valuePtr); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(EntryHandle)); + } + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + if (!_disposed) + { + _handle?.Dispose(); + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Blackboard/EntryHandleMut.cs b/src/Iceoryx2/Blackboard/EntryHandleMut.cs new file mode 100644 index 0000000..0d47d07 --- /dev/null +++ b/src/Iceoryx2/Blackboard/EntryHandleMut.cs @@ -0,0 +1,120 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2.SafeHandles; +using System; +using System.Runtime.InteropServices; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.Blackboard; + +/// +/// A mutable entry handle for modifying a value in the blackboard. +/// +/// The type of the key. +/// The type of the value. +public sealed class EntryHandleMut : IDisposable + where TKey : unmanaged + where TValue : unmanaged +{ + private readonly SafeEntryHandleMutHandle _handle; + private readonly TKey _key; + private bool _disposed; + + internal EntryHandleMut(IntPtr handle, TKey key) + { + _handle = new SafeEntryHandleMutHandle(handle); + _key = key; + } + + /// + /// Gets the key associated with this entry handle. + /// + public TKey Key => _key; + + /// + /// Updates the entry value by copying the provided value. + /// + /// The new value to set. + /// A Result indicating success or an error. + public unsafe Result Update(TValue value) + { + ThrowIfDisposed(); + + var valueSize = (ulong)sizeof(TValue); + var valueAlignment = BlackboardHelpers.GetAlignment(valueSize); + + // Stack allocate for small fixed-size value + TValue* valuePtr = stackalloc TValue[1]; + *valuePtr = value; + + var handlePtr = _handle.DangerousGetHandle(); + // Note: iox2_entry_handle_mut_update_with_copy returns void + iox2_entry_handle_mut_update_with_copy( + ref handlePtr, + (IntPtr)valuePtr, + valueSize, + valueAlignment); + + return Result.Ok(Unit.Value); + } + + /// + /// Loans an uninitialized entry value for in-place construction. + /// This is useful when you want to construct the value directly in shared memory. + /// + /// A Result containing the loaned entry value or an error. + public unsafe Result, Iox2Error> LoanUninit() + { + ThrowIfDisposed(); + + var valueSize = (ulong)sizeof(TValue); + var valueAlignment = BlackboardHelpers.GetAlignment(valueSize); + + var handlePtr = _handle.DangerousGetHandle(); + // Note: iox2_entry_handle_mut_loan_uninit returns void + iox2_entry_handle_mut_loan_uninit( + handlePtr, + IntPtr.Zero, + out var entryValueHandle, + valueSize, + valueAlignment); + + if (entryValueHandle == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.EntryAccessFailed); + } + + return Result, Iox2Error>.Ok( + new EntryValue(entryValueHandle, _key)); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(EntryHandleMut)); + } + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + if (!_disposed) + { + _handle?.Dispose(); + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Blackboard/EntryValue.cs b/src/Iceoryx2/Blackboard/EntryValue.cs new file mode 100644 index 0000000..2b0c610 --- /dev/null +++ b/src/Iceoryx2/Blackboard/EntryValue.cs @@ -0,0 +1,139 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; +using System.Runtime.InteropServices; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.Blackboard; + +/// +/// Represents a loaned entry value that can be written to and then committed to the blackboard. +/// This is useful for in-place construction of values in shared memory. +/// +/// The type of the key. +/// The type of the value. +public sealed class EntryValue : IDisposable + where TKey : unmanaged + where TValue : unmanaged +{ + private IntPtr _handle; + private readonly TKey _key; + private bool _disposed; + private bool _committed; + + internal EntryValue(IntPtr handle, TKey key) + { + _handle = handle; + _key = key; + } + + /// + /// Gets the key associated with this entry value. + /// + public TKey Key => _key; + + /// + /// Gets a mutable reference to the payload for in-place modification. + /// + /// A reference to the payload. + public unsafe ref TValue PayloadMut() + { + ThrowIfDisposed(); + ThrowIfCommitted(); + + iox2_entry_value_mut(ref _handle, out var payloadPtr); + if (payloadPtr == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to get payload pointer from entry value."); + } + return ref *(TValue*)payloadPtr; + } + + /// + /// Writes a value to the loaned memory. + /// + /// The value to write. + public unsafe void Write(TValue value) + { + ThrowIfDisposed(); + ThrowIfCommitted(); + + iox2_entry_value_mut(ref _handle, out var payloadPtr); + if (payloadPtr == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to get payload pointer from entry value."); + } + // Use unsafe pointer dereference instead of Marshal.StructureToPtr + *(TValue*)payloadPtr = value; + } + + /// + /// Commits the entry value to the blackboard. + /// After calling this method, the entry value is no longer usable. + /// + /// A Result containing a new mutable entry handle or an error. + public Result, Iox2Error> Update() + { + ThrowIfDisposed(); + ThrowIfCommitted(); + + // Note: iox2_entry_value_update returns void + iox2_entry_value_update( + _handle, + IntPtr.Zero, + out var entryHandleMutPtr); + + _committed = true; + _handle = IntPtr.Zero; // Handle is consumed by update + + if (entryHandleMutPtr == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.EntryAccessFailed); + } + + return Result, Iox2Error>.Ok( + new EntryHandleMut(entryHandleMutPtr, _key)); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(EntryValue)); + } + } + + private void ThrowIfCommitted() + { + if (_committed) + { + throw new InvalidOperationException("Entry value has already been committed."); + } + } + + /// + /// Releases all resources used by the . + /// If the entry value has not been committed, it will be dropped without updating the blackboard. + /// + public void Dispose() + { + if (!_disposed) + { + if (!_committed && _handle != IntPtr.Zero) + { + iox2_entry_value_drop(_handle); + } + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Blackboard/Reader.cs b/src/Iceoryx2/Blackboard/Reader.cs new file mode 100644 index 0000000..2fbec0f --- /dev/null +++ b/src/Iceoryx2/Blackboard/Reader.cs @@ -0,0 +1,101 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2.SafeHandles; +using System; +using System.Runtime.InteropServices; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.Blackboard; + +/// +/// A reader for accessing values in a blackboard service. +/// Multiple readers can exist per blackboard service. +/// +/// The type of keys in the blackboard. +/// +/// Thread Safety: This class is not thread-safe. Each reader instance should be used from a single thread only. +/// Multiple reader instances can safely access the same blackboard concurrently from different threads. +/// +public sealed class Reader : IDisposable + where TKey : unmanaged +{ + private readonly SafeReaderHandle _handle; + private readonly Func _keyComparer; + private bool _disposed; + + internal Reader(IntPtr handle, Func keyComparer) + { + _handle = new SafeReaderHandle(handle); + _keyComparer = keyComparer; + } + + /// + /// Gets a read-only entry handle for the specified key and value type. + /// The entry handle can be used to read values from the blackboard. + /// + /// The value type for this key. + /// The key to get the entry handle for. + /// A Result containing the entry handle or an error. + public unsafe Result, Iox2Error> Entry(TKey key) + where TValue : unmanaged + { + ThrowIfDisposed(); + + var valueTypeName = ServiceBuilder.GetRustCompatibleTypeName(); + var valueTypeSize = (ulong)sizeof(TValue); + var valueTypeAlignment = BlackboardHelpers.GetAlignment(valueTypeSize); + + // Stack allocate - matches how Writer passes key directly + TKey* keyPtr = stackalloc TKey[1]; + keyPtr[0] = key; + + var handlePtr = _handle.DangerousGetHandle(); + var result = iox2_reader_entry( + ref handlePtr, + IntPtr.Zero, + out var entryHandlePtr, + (IntPtr)keyPtr, + valueTypeName, + valueTypeName.Length, + valueTypeSize, + valueTypeAlignment); + + if (result != IOX2_OK || entryHandlePtr == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.EntryAccessFailed); + } + + return Result, Iox2Error>.Ok( + new EntryHandle(entryHandlePtr, key)); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Reader)); + } + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + if (!_disposed) + { + _handle?.Dispose(); + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Blackboard/Writer.cs b/src/Iceoryx2/Blackboard/Writer.cs new file mode 100644 index 0000000..6c69f52 --- /dev/null +++ b/src/Iceoryx2/Blackboard/Writer.cs @@ -0,0 +1,101 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using Iceoryx2.SafeHandles; +using System; +using System.Runtime.InteropServices; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.Blackboard; + +/// +/// A writer for modifying values in a blackboard service. +/// Only one writer can exist per blackboard service. +/// +/// The type of keys in the blackboard. +/// +/// Thread Safety: This class is not thread-safe. The writer instance should be used from a single thread only. +/// The underlying blackboard service ensures exclusive write access - only one writer can exist at a time. +/// +public sealed class Writer : IDisposable + where TKey : unmanaged +{ + private readonly SafeWriterHandle _handle; + private readonly Func _keyComparer; + private bool _disposed; + + internal Writer(IntPtr handle, Func keyComparer) + { + _handle = new SafeWriterHandle(handle); + _keyComparer = keyComparer; + } + + /// + /// Gets a mutable entry handle for the specified key and value type. + /// The entry handle can be used to update values in the blackboard. + /// + /// The value type for this key. + /// The key to get the entry handle for. + /// A Result containing the mutable entry handle or an error. + public unsafe Result, Iox2Error> Entry(TKey key) + where TValue : unmanaged + { + ThrowIfDisposed(); + + var valueTypeName = ServiceBuilder.GetRustCompatibleTypeName(); + var valueTypeSize = (ulong)sizeof(TValue); + var valueTypeAlignment = BlackboardHelpers.GetAlignment(valueTypeSize); + + // Stack allocate - matches how C example passes &key directly + TKey* keyPtr = stackalloc TKey[1]; + keyPtr[0] = key; + + var handlePtr = _handle.DangerousGetHandle(); + var result = iox2_writer_entry( + ref handlePtr, + IntPtr.Zero, + out var entryHandleMutPtr, + (IntPtr)keyPtr, + valueTypeName, + valueTypeName.Length, + valueTypeSize, + valueTypeAlignment); + + if (result != IOX2_OK || entryHandleMutPtr == IntPtr.Zero) + { + return Result, Iox2Error>.Err(Iox2Error.EntryAccessFailed); + } + + return Result, Iox2Error>.Ok( + new EntryHandleMut(entryHandleMutPtr, key)); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Writer)); + } + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + if (!_disposed) + { + _handle?.Dispose(); + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/ErrorHandling/BlackboardServiceCreationError.cs b/src/Iceoryx2/ErrorHandling/BlackboardServiceCreationError.cs new file mode 100644 index 0000000..649af63 --- /dev/null +++ b/src/Iceoryx2/ErrorHandling/BlackboardServiceCreationError.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +namespace Iceoryx2.ErrorHandling +{ + /// + /// Represents an error that occurred during blackboard service creation. + /// Blackboard services provide key-value storage patterns for shared memory communication. + /// + /// + /// Common causes: + /// + /// Service already exists with incompatible settings + /// Invalid service name or configuration + /// Maximum number of blackboard services reached + /// Insufficient shared memory + /// No entries provided when creating the blackboard + /// + /// + public class BlackboardServiceCreationError : Iox2Error + { + /// + /// Gets the name of the blackboard service that failed to create, if available. + /// + public string? ServiceName { get; } + + /// + /// Gets the error kind for pattern matching. + /// + public override Iox2ErrorKind Kind => Iox2ErrorKind.BlackboardServiceCreationFailed; + + /// + /// Gets additional details about why blackboard service creation failed. + /// + public override string? Details { get; } + + /// + /// Gets a human-readable error message including service name if available. + /// + public override string Message + { + get + { + var msg = ServiceName != null + ? $"Failed to create blackboard service '{ServiceName}'" + : "Failed to create blackboard service"; + return Details != null ? $"{msg}. Details: {Details}" : $"{msg}."; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the blackboard service that failed to create. + /// Optional details about the error. + public BlackboardServiceCreationError(string? serviceName, string? details = null) + { + ServiceName = serviceName; + Details = details; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/ErrorHandling/EntryAccessError.cs b/src/Iceoryx2/ErrorHandling/EntryAccessError.cs new file mode 100644 index 0000000..122e171 --- /dev/null +++ b/src/Iceoryx2/ErrorHandling/EntryAccessError.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +namespace Iceoryx2.ErrorHandling +{ + /// + /// Represents an error that occurred when accessing a blackboard entry. + /// + public class EntryAccessError : Iox2Error + { + /// + /// Gets the error kind for pattern matching. + /// + public override Iox2ErrorKind Kind => Iox2ErrorKind.EntryAccessFailed; + + /// + /// Gets additional details about the error. + /// + public override string? Details { get; } + + /// + /// Gets a human-readable error message. + /// + public override string Message + { + get + { + var msg = "Failed to access blackboard entry"; + return Details != null ? $"{msg}. Details: {Details}" : $"{msg}."; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Optional details about the error. + public EntryAccessError(string? details = null) + { + Details = details; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/ErrorHandling/Iox2ErrorKind.cs b/src/Iceoryx2/ErrorHandling/Iox2ErrorKind.cs index d9867c3..f51bfc3 100644 --- a/src/Iceoryx2/ErrorHandling/Iox2ErrorKind.cs +++ b/src/Iceoryx2/ErrorHandling/Iox2ErrorKind.cs @@ -69,6 +69,14 @@ public enum Iox2ErrorKind ConnectionUpdateFailed, /// Service discovery failed. ServiceListFailed, + /// Blackboard service creation failed. + BlackboardServiceCreationFailed, + /// Blackboard writer creation failed. + WriterCreationFailed, + /// Blackboard reader creation failed. + ReaderCreationFailed, + /// Blackboard entry access failed. + EntryAccessFailed, /// Unknown error. Unknown } diff --git a/src/Iceoryx2/ErrorHandling/ReaderCreationError.cs b/src/Iceoryx2/ErrorHandling/ReaderCreationError.cs new file mode 100644 index 0000000..eb4f248 --- /dev/null +++ b/src/Iceoryx2/ErrorHandling/ReaderCreationError.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +namespace Iceoryx2.ErrorHandling +{ + /// + /// Represents an error that occurred during blackboard reader creation. + /// + public class ReaderCreationError : Iox2Error + { + /// + /// Gets the error kind for pattern matching. + /// + public override Iox2ErrorKind Kind => Iox2ErrorKind.ReaderCreationFailed; + + /// + /// Gets additional details about the error. + /// + public override string? Details { get; } + + /// + /// Gets a human-readable error message. + /// + public override string Message + { + get + { + var msg = "Failed to create blackboard reader"; + return Details != null ? $"{msg}. Details: {Details}" : $"{msg}."; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Optional details about the error. + public ReaderCreationError(string? details = null) + { + Details = details; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/ErrorHandling/WriterCreationError.cs b/src/Iceoryx2/ErrorHandling/WriterCreationError.cs new file mode 100644 index 0000000..bb2a9dd --- /dev/null +++ b/src/Iceoryx2/ErrorHandling/WriterCreationError.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +namespace Iceoryx2.ErrorHandling +{ + /// + /// Represents an error that occurred during blackboard writer creation. + /// + public class WriterCreationError : Iox2Error + { + /// + /// Gets the error kind for pattern matching. + /// + public override Iox2ErrorKind Kind => Iox2ErrorKind.WriterCreationFailed; + + /// + /// Gets additional details about the error. + /// + public override string? Details { get; } + + /// + /// Gets a human-readable error message. + /// + public override string Message + { + get + { + var msg = "Failed to create blackboard writer"; + return Details != null ? $"{msg}. Details: {Details}" : $"{msg}."; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Optional details about the error. + public WriterCreationError(string? details = null) + { + Details = details; + } + } +} \ No newline at end of file diff --git a/src/Iceoryx2/Iox2Error.cs b/src/Iceoryx2/Iox2Error.cs index ffc8c70..5c0283d 100644 --- a/src/Iceoryx2/Iox2Error.cs +++ b/src/Iceoryx2/Iox2Error.cs @@ -72,6 +72,10 @@ public static Iox2Error FromKind(Iox2ErrorKind kind, string? details = null) Iox2ErrorKind.WaitSetAttachmentFailed => new WaitSetAttachmentError(details), Iox2ErrorKind.WaitSetRunFailed => new WaitSetRunError(details), Iox2ErrorKind.ConnectionUpdateFailed => new ConnectionUpdateError(details), + Iox2ErrorKind.BlackboardServiceCreationFailed => new BlackboardServiceCreationError(null, details), + Iox2ErrorKind.WriterCreationFailed => new WriterCreationError(details), + Iox2ErrorKind.ReaderCreationFailed => new ReaderCreationError(details), + Iox2ErrorKind.EntryAccessFailed => new EntryAccessError(details), Iox2ErrorKind.Unknown => new UnknownError(details), _ => new UnknownError(details) }; @@ -154,6 +158,18 @@ public static Iox2Error FromKind(Iox2ErrorKind kind, string? details = null) /// Gets a instance for backward compatibility. public static Iox2Error ConnectionUpdateFailed => new ConnectionUpdateError(); + /// Gets a instance for backward compatibility. + public static Iox2Error BlackboardServiceCreationFailed => new BlackboardServiceCreationError(null); + + /// Gets a instance for backward compatibility. + public static Iox2Error WriterCreationFailed => new WriterCreationError(); + + /// Gets a instance for backward compatibility. + public static Iox2Error ReaderCreationFailed => new ReaderCreationError(); + + /// Gets an instance for backward compatibility. + public static Iox2Error EntryAccessFailed => new EntryAccessError(); + /// Gets an instance for backward compatibility. public static Iox2Error Unknown => new UnknownError(); } \ No newline at end of file diff --git a/src/Iceoryx2/Native/Iox2NativeMethods.cs b/src/Iceoryx2/Native/Iox2NativeMethods.cs index 43e150d..4fd8390 100644 --- a/src/Iceoryx2/Native/Iox2NativeMethods.cs +++ b/src/Iceoryx2/Native/Iox2NativeMethods.cs @@ -611,6 +611,16 @@ internal delegate iox2_callback_progression_e iox2_service_list_callback( IntPtr static_config_ptr, IntPtr callback_context); + // ======================================== + // Node Wait Failure Enum + // ======================================== + + internal enum iox2_node_wait_failure_e + { + INTERRUPT = IOX2_OK + 1, + TERMINATION_REQUEST = IOX2_OK + 2 + } + // ======================================== // Node API // ======================================== @@ -618,6 +628,16 @@ internal delegate iox2_callback_progression_e iox2_service_list_callback( [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] internal static extern void iox2_node_drop(IntPtr node_handle); + /// + /// Waits for the specified cycle time to pass. + /// Returns IOX2_OK on successful wait, or an error code if interrupted or termination was requested. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_node_wait( + ref IntPtr node_handle, + ulong cycle_time_sec, + uint cycle_time_nsec); + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr iox2_node_service_builder( ref IntPtr node_handle, // Pass by reference - C expects pointer to handle @@ -1400,4 +1420,305 @@ internal static extern bool iox2_waitset_attachment_id_has_missed_deadline( [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr iox2_listener_get_file_descriptor(ref IntPtr listener_handle); + + // ======================================== + // Service Builder Blackboard API + // ======================================== + + /// + /// Creates a blackboard creator service builder from a generic service builder. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr iox2_service_builder_blackboard_creator(IntPtr service_builder_handle); + + /// + /// Creates a blackboard opener service builder from a generic service builder. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr iox2_service_builder_blackboard_opener(IntPtr service_builder_handle); + + /// + /// Sets the key type details for the blackboard creator. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_service_builder_blackboard_creator_set_key_type_details( + ref IntPtr service_builder_handle, + [MarshalAs(UnmanagedType.LPUTF8Str)] string type_name, + int type_name_len, + ulong type_size, + ulong type_alignment); + + /// + /// Sets the key type details for the blackboard opener. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_service_builder_blackboard_opener_set_key_type_details( + ref IntPtr service_builder_handle, + [MarshalAs(UnmanagedType.LPUTF8Str)] string type_name, + int type_name_len, + ulong type_size, + ulong type_alignment); + + /// + /// Delegate for key equality comparison in blackboard. + /// + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + internal delegate bool iox2_service_blackboard_key_eq_cmp_func(IntPtr lhs, IntPtr rhs); + + /// + /// Sets the key equality comparison function for the blackboard creator. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_service_builder_blackboard_creator_set_key_eq_comparison_function( + ref IntPtr service_builder_handle, + iox2_service_blackboard_key_eq_cmp_func key_eq_func); + + /// + /// Delegate for releasing value pointer passed to add. + /// + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void iox2_service_blackboard_creator_add_release_callback(IntPtr value_ptr); + + /// + /// Adds a key-value entry to the blackboard creator. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_service_builder_blackboard_creator_add( + ref IntPtr service_builder_handle, + IntPtr key_ptr, + IntPtr value_ptr, + iox2_service_blackboard_creator_add_release_callback? release_callback, + [MarshalAs(UnmanagedType.LPUTF8Str)] string value_type_name, + int value_type_name_len, + ulong value_size, + ulong value_alignment); + + /// + /// Sets the maximum number of readers for the blackboard creator. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_service_builder_blackboard_creator_set_max_readers( + ref IntPtr service_builder_handle, + UIntPtr value); + + /// + /// Sets the maximum number of readers for the blackboard opener. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_service_builder_blackboard_opener_set_max_readers( + ref IntPtr service_builder_handle, + UIntPtr value); + + /// + /// Sets the maximum number of nodes for the blackboard creator. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_service_builder_blackboard_creator_set_max_nodes( + ref IntPtr service_builder_handle, + UIntPtr value); + + /// + /// Sets the maximum number of nodes for the blackboard opener. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_service_builder_blackboard_opener_set_max_nodes( + ref IntPtr service_builder_handle, + UIntPtr value); + + /// + /// Opens an existing blackboard service. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_service_builder_blackboard_open( + IntPtr service_builder_handle, + IntPtr port_factory_struct_ptr, + out IntPtr port_factory_handle); + + /// + /// Creates a new blackboard service. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_service_builder_blackboard_create( + IntPtr service_builder_handle, + IntPtr port_factory_struct_ptr, + out IntPtr port_factory_handle); + + // ======================================== + // Port Factory Blackboard API + // ======================================== + + /// + /// Drops the blackboard port factory handle. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_port_factory_blackboard_drop(IntPtr port_factory_handle); + + /// + /// Creates a writer builder from the blackboard port factory. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr iox2_port_factory_blackboard_writer_builder( + ref IntPtr port_factory_handle, + IntPtr writer_builder_struct_ptr); + + /// + /// Creates a reader builder from the blackboard port factory. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr iox2_port_factory_blackboard_reader_builder( + ref IntPtr port_factory_handle, + IntPtr reader_builder_struct_ptr); + + // ======================================== + // Writer Builder API + // ======================================== + + /// + /// Creates a writer from the writer builder. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_port_factory_writer_builder_create( + IntPtr writer_builder_handle, + IntPtr writer_struct_ptr, + out IntPtr writer_handle); + + // ======================================== + // Writer API + // ======================================== + + /// + /// Drops the writer handle. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_writer_drop(IntPtr writer_handle); + + /// + /// Gets a mutable entry handle for a key from the writer. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_writer_entry( + ref IntPtr writer_handle, + IntPtr entry_handle_struct_ptr, + out IntPtr entry_handle_mut, + IntPtr key_ptr, + [MarshalAs(UnmanagedType.LPUTF8Str)] string value_type_name, + int value_type_name_len, + ulong value_size, + ulong value_alignment); + + // ======================================== + // Entry Handle Mut API + // ======================================== + + /// + /// Drops the mutable entry handle. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_handle_mut_drop(IntPtr entry_handle_mut); + + /// + /// Updates the entry value by copying data. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_handle_mut_update_with_copy( + ref IntPtr entry_handle_mut, + IntPtr value_ptr, + ulong value_size, + ulong value_alignment); + + /// + /// Loans an uninitialized entry value for writing. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_handle_mut_loan_uninit( + IntPtr entry_handle_mut, + IntPtr entry_value_struct_ptr, + out IntPtr entry_value_handle, + ulong value_size, + ulong value_alignment); + + // ======================================== + // Entry Value API (for loan-based updates) + // ======================================== + + /// + /// Gets a mutable pointer to the entry value's payload. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_value_mut( + ref IntPtr entry_value_handle, + out IntPtr payload_ptr); + + /// + /// Updates the entry with the loaned value. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_value_update( + IntPtr entry_value_handle, + IntPtr entry_handle_struct_ptr, + out IntPtr entry_handle_mut); + + /// + /// Drops the entry value handle without updating. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_value_drop(IntPtr entry_value_handle); + + // ======================================== + // Reader Builder API + // ======================================== + + /// + /// Creates a reader from the reader builder. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_port_factory_reader_builder_create( + IntPtr reader_builder_handle, + IntPtr reader_struct_ptr, + out IntPtr reader_handle); + + // ======================================== + // Reader API + // ======================================== + + /// + /// Drops the reader handle. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_reader_drop(IntPtr reader_handle); + + /// + /// Gets an entry handle for a key from the reader. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int iox2_reader_entry( + ref IntPtr reader_handle, + IntPtr entry_handle_struct_ptr, + out IntPtr entry_handle, + IntPtr key_ptr, + [MarshalAs(UnmanagedType.LPUTF8Str)] string value_type_name, + int value_type_name_len, + ulong value_size, + ulong value_alignment); + + // ======================================== + // Entry Handle API (read-only) + // ======================================== + + /// + /// Drops the entry handle. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_handle_drop(IntPtr entry_handle); + + /// + /// Gets the current value from the entry handle. + /// + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void iox2_entry_handle_get( + ref IntPtr entry_handle, + IntPtr value_ptr, + ulong value_size, + ulong value_alignment); } \ No newline at end of file diff --git a/src/Iceoryx2/Node.cs b/src/Iceoryx2/Node.cs index d594ae3..706918a 100644 --- a/src/Iceoryx2/Node.cs +++ b/src/Iceoryx2/Node.cs @@ -19,6 +19,27 @@ namespace Iceoryx2; +/// +/// Represents the result of a node wait operation. +/// +public enum NodeWaitResult +{ + /// + /// The wait completed successfully after the specified cycle time. + /// + Ok, + + /// + /// The wait was interrupted by a signal. + /// + Interrupt, + + /// + /// A termination request was received. + /// + TerminationRequest +} + /// /// Represents a node in the Iceoryx2 system. /// The node serves as a central entry point and is linked to a specific process within the Iceoryx2 ecosystem. @@ -112,6 +133,36 @@ public ServiceBuilder ServiceBuilder() return new ServiceBuilder(this); } + /// + /// Waits for the specified duration while handling system signals properly. + /// This is the recommended way to wait in a loop instead of Thread.Sleep. + /// + /// The duration to wait. + /// A result indicating whether the wait completed successfully or was interrupted. + public NodeWaitResult Wait(TimeSpan cycleTime) + { + ThrowIfDisposed(); + + var seconds = (ulong)cycleTime.TotalSeconds; + var nanoseconds = (uint)((cycleTime.TotalSeconds - seconds) * 1_000_000_000); + + var nodeHandle = _handle.DangerousGetHandle(); + var result = Iox2NativeMethods.iox2_node_wait(ref nodeHandle, seconds, nanoseconds); + + if (result == Iox2NativeMethods.IOX2_OK) + { + return NodeWaitResult.Ok; + } + + var errorCode = (Iox2NativeMethods.iox2_node_wait_failure_e)result; + return errorCode switch + { + Iox2NativeMethods.iox2_node_wait_failure_e.INTERRUPT => NodeWaitResult.Interrupt, + Iox2NativeMethods.iox2_node_wait_failure_e.TERMINATION_REQUEST => NodeWaitResult.TerminationRequest, + _ => NodeWaitResult.Interrupt + }; + } + /// /// Lists all available services in the system. /// diff --git a/src/Iceoryx2/SafeHandles/SafeBlackboardServiceHandle.cs b/src/Iceoryx2/SafeHandles/SafeBlackboardServiceHandle.cs new file mode 100644 index 0000000..b18a270 --- /dev/null +++ b/src/Iceoryx2/SafeHandles/SafeBlackboardServiceHandle.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.SafeHandles; + +/// +/// Safe handle for blackboard service port factory resources. +/// Ensures proper cleanup of native resources when disposed. +/// +internal sealed class SafeBlackboardServiceHandle : SafeIox2Handle +{ + /// + /// Initializes a new instance of the class. + /// + public SafeBlackboardServiceHandle() : base() + { + } + + /// + /// Initializes a new instance with the specified handle. + /// + public SafeBlackboardServiceHandle(IntPtr handle) : base(handle) + { + } + + /// + /// Releases the native blackboard service handle. + /// + /// true if the handle was released successfully; otherwise, false. + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + iox2_port_factory_blackboard_drop(handle); + } + return true; + } +} \ No newline at end of file diff --git a/src/Iceoryx2/SafeHandles/SafeEntryHandleHandle.cs b/src/Iceoryx2/SafeHandles/SafeEntryHandleHandle.cs new file mode 100644 index 0000000..a652c07 --- /dev/null +++ b/src/Iceoryx2/SafeHandles/SafeEntryHandleHandle.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.SafeHandles; + +/// +/// Safe handle for blackboard read-only entry handle resources. +/// Ensures proper cleanup of native resources when disposed. +/// +internal sealed class SafeEntryHandleHandle : SafeIox2Handle +{ + /// + /// Initializes a new instance of the class. + /// + public SafeEntryHandleHandle() : base() + { + } + + /// + /// Initializes a new instance with the specified handle. + /// + public SafeEntryHandleHandle(IntPtr handle) : base(handle) + { + } + + /// + /// Releases the native entry handle. + /// + /// true if the handle was released successfully; otherwise, false. + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + iox2_entry_handle_drop(handle); + } + return true; + } +} \ No newline at end of file diff --git a/src/Iceoryx2/SafeHandles/SafeEntryHandleMutHandle.cs b/src/Iceoryx2/SafeHandles/SafeEntryHandleMutHandle.cs new file mode 100644 index 0000000..7329725 --- /dev/null +++ b/src/Iceoryx2/SafeHandles/SafeEntryHandleMutHandle.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.SafeHandles; + +/// +/// Safe handle for blackboard mutable entry handle resources. +/// Ensures proper cleanup of native resources when disposed. +/// +internal sealed class SafeEntryHandleMutHandle : SafeIox2Handle +{ + /// + /// Initializes a new instance of the class. + /// + public SafeEntryHandleMutHandle() : base() + { + } + + /// + /// Initializes a new instance with the specified handle. + /// + public SafeEntryHandleMutHandle(IntPtr handle) : base(handle) + { + } + + /// + /// Releases the native mutable entry handle. + /// + /// true if the handle was released successfully; otherwise, false. + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + iox2_entry_handle_mut_drop(handle); + } + return true; + } +} \ No newline at end of file diff --git a/src/Iceoryx2/SafeHandles/SafeReaderHandle.cs b/src/Iceoryx2/SafeHandles/SafeReaderHandle.cs new file mode 100644 index 0000000..3606887 --- /dev/null +++ b/src/Iceoryx2/SafeHandles/SafeReaderHandle.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.SafeHandles; + +/// +/// Safe handle for blackboard reader resources. +/// Ensures proper cleanup of native resources when disposed. +/// +internal sealed class SafeReaderHandle : SafeIox2Handle +{ + /// + /// Initializes a new instance of the class. + /// + public SafeReaderHandle() : base() + { + } + + /// + /// Initializes a new instance with the specified handle. + /// + public SafeReaderHandle(IntPtr handle) : base(handle) + { + } + + /// + /// Releases the native reader handle. + /// + /// true if the handle was released successfully; otherwise, false. + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + iox2_reader_drop(handle); + } + return true; + } +} \ No newline at end of file diff --git a/src/Iceoryx2/SafeHandles/SafeWriterHandle.cs b/src/Iceoryx2/SafeHandles/SafeWriterHandle.cs new file mode 100644 index 0000000..8c1d08e --- /dev/null +++ b/src/Iceoryx2/SafeHandles/SafeWriterHandle.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache Software License 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0, or the MIT license +// which is available at https://opensource.org/licenses/MIT. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +using System; +using static Iceoryx2.Native.Iox2NativeMethods; + +namespace Iceoryx2.SafeHandles; + +/// +/// Safe handle for blackboard writer resources. +/// Ensures proper cleanup of native resources when disposed. +/// +internal sealed class SafeWriterHandle : SafeIox2Handle +{ + /// + /// Initializes a new instance of the class. + /// + public SafeWriterHandle() : base() + { + } + + /// + /// Initializes a new instance with the specified handle. + /// + public SafeWriterHandle(IntPtr handle) : base(handle) + { + } + + /// + /// Releases the native writer handle. + /// + /// true if the handle was released successfully; otherwise, false. + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + iox2_writer_drop(handle); + } + return true; + } +} \ No newline at end of file diff --git a/src/Iceoryx2/ServiceBuilder.cs b/src/Iceoryx2/ServiceBuilder.cs index ce88f17..0891086 100644 --- a/src/Iceoryx2/ServiceBuilder.cs +++ b/src/Iceoryx2/ServiceBuilder.cs @@ -11,6 +11,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT using System; +using System.Collections.Generic; namespace Iceoryx2; @@ -52,6 +53,29 @@ public RequestResponse.RequestResponseServiceBuilder Reques return new RequestResponse.RequestResponseServiceBuilder(_node); } + /// + /// Creates a blackboard service builder with the specified key comparer. + /// + /// The type of keys in the blackboard (must be unmanaged). + /// A function to compare two keys for equality. + /// A blackboard service builder. + public Blackboard.BlackboardServiceBuilder Blackboard(Func keyComparer) + where TKey : unmanaged + { + return new Blackboard.BlackboardServiceBuilder(_node, keyComparer); + } + + /// + /// Creates a blackboard service builder using the default equality comparer. + /// + /// The type of keys in the blackboard (must be unmanaged). + /// A blackboard service builder. + public Blackboard.BlackboardServiceBuilder Blackboard() + where TKey : unmanaged + { + return new Blackboard.BlackboardServiceBuilder(_node, EqualityComparer.Default.Equals); + } + /// /// Gets a Rust-compatible type name for cross-language interoperability. /// Maps .NET types to their Rust equivalents for iceoryx2 type matching.