A modern, high-performance, zero-allocation .NET binding for Eclipse Cyclone DDS, with idiomatic C# API.
See detailed technical overview.
Install the CycloneDDS.NET package from NuGet:
dotnet add package CycloneDDS.NETThis single package includes:
- Runtime Library: High-performance managed bindings (targets
net8.0; runs on .NET 8 and later). - Native Assets: Pre-compiled native runtime and IDL compiler for both Windows x64 (
ddsc.dll,idlc.exe) and Linux x64 (libddsc.so,idlc). NuGet selects the correct runtime asset per platform automatically. - Build Tools: Automatic C# code generation during build.
| Platform | Native runtime | Target requirement |
|---|---|---|
| Windows x64 | ddsc.dll | Visual C++ Redistributable for Visual Studio 2022 installed on the target machine. |
| Linux x64 | libddsc.so | A glibc-based distribution (e.g. Ubuntu/Debian). No extra runtime install needed. |
If you want to build the project from source or contribute:
Clone the repository (recursively, to get the native submodule):
git clone --recursive https://github.com/pjanec/CycloneDds.NET.git cd CycloneDds.NETBuild the native libraries for your platform:
- Windows (PowerShell):
.\build\native-win.ps1 - Linux (bash):
build/native-linux.sh Release
Then build and test the managed solution:
dotnet build CycloneDDS.NET.Core.slnf -c Release dotnet test CycloneDDS.NET.Core.slnf -c ReleaseOn Windows you can instead run the one-stop script
.\build\build-and-test.ps1, which builds the native artifacts (if missing), builds the solution, and runs the tests.- Windows (PowerShell):
Requirements:
- .NET SDK: the solution targets
net8.0, but building requires the .NET 10 SDK (the code uses C# 13 language features). Produced binaries still run on .NET 8+. - CMake 3.16+ on your
PATH. - Windows: Visual Studio 2022 with the C++ Desktop Development workload (for native compilation).
- Linux: a C toolchain and
patchelf— e.g.sudo apt-get install -y cmake build-essential patchelf.
- .NET SDK: the solution targets
- Zero-Allocation Writes: Custom marshaller writes directly to pooled buffers (ArrayPool) using a C-compatible memory layout.
- Zero-Copy Reads: Read directly from native DDS buffers using
ref structviews, bypassing deserialization. - Unified API: Single reader provides both safe managed objects and high-performance zero-copy views.
- Lazy Deserialization: Only pay the cost of deep-copying objects when you explicitly access
.Data.
- Code-First DSL: Define your data types entirely in C# using attributes (
[DdsTopic],[DdsKey],[DdsStruct],[DdsQos]). No need to write IDL files manually. - Automatic IDL Generation: The build tools automatically generate standard OMG IDL files from your C# classes, ensuring perfect interoperability with other DDS implementations (C++, Python, Java) and tools. See IDL Generation.
- Modern Language Integration: Full support for C# 12
[InlineArray](safe fixed-size arrays withoutunsafe), typed enums (enum E : byteemits@bit_bound(8)), and default topic naming based on namespaces. - Auto-Magic Type Discovery: Runtime automatically registers type descriptors based on your schema.
- IDL Import: Convert existing IDL files into C# DSL automatically using the
IdlImportertool. - 100% Native Compliance: Uses Cyclone DDS native serializer for wire compatibility.
- Auto-Magic Type Discovery: No manual IDL compilation or type registration required.
- Async/Await:
WaitDataAsyncfor non-blocking, task-based consumers. - Client-Side Filtering: High-performance predicates (
view => view.Id > 5) compiled to JIT code. - Instance Management: O(1) history lookup for keyed topics.
- Sender Tracking: Identify the source application (Computer, PID, custom app id) of every message.
- Modern C#: Events, Properties, and generic constraints instead of listeners and pointers.
- Partition Support: Isolate traffic using DDS partitions. Set a partition on a participant once and every reader/writer inherits it automatically, or override per-reader/writer with a named argument.
- Zero-Allocation WaitSet: Monitor 100+ readers on a single OS thread.
DdsWaitSet.Wait(Span<IDdsReader>, timeout, ct)never allocates in the hot path and supportsCancellationTokenfor instant, safe interruption.
Define your data using standard C# partial structs. The build tools generate the serialization logic automatically.
Use this for high-frequency data (1kHz+).
usingCycloneDDS.Schema;namespaceFactory.Monitoring;// Topic name defaults to namespace + class name ("Factory_Monitoring_SensorData") if omitted[DdsTopic]publicpartialstructSensorData{[DdsKey,DdsId(0)]publicintSensorId;[DdsId(1)]publicdoubleValue;// Fixed-size buffer (maps to char[32]). No heap allocation.[DdsId(2)]publicFixedString32LocationId;// Safe, zero-allocation fixed array using C# 12 [InlineArray] (no 'unsafe' needed!)[DdsId(3)]publicFloatBuffer8Measurements;// Byte-backed enum yields IDL @bit_bound(8) automatically for optimal native network usage[DdsId(4)]publicSensorStatusStatus;}[System.Runtime.CompilerServices.InlineArray(8)]publicstructFloatBuffer8{privatefloat_element0;}publicenumSensorStatus:byte{Offline,Online,Error}For scenarios requiring direct memory manipulation or porting legacy C/C++ structs, you can use unsafe fixed arrays. The runtime maps these directly to native memory with zero allocation.
[DdsTopic("CustomTopicNameForVideoFrame")]publicunsafepartialstructVideoFrame{[DdsKey]publicintFrameId;// Classic C# unsafe fixed-size bufferpublicfixedbytePixels[1920*1080*3];}Use this for business logic where convenience outweighs raw speed.
[DdsStruct]// Helper struct to be used in the topic data struct (can be nested)publicpartialstructGeoPoint{publicdoubleLat;publicdoubleLon;}[DdsTopic("LogEvents")][DdsManaged]// Opt-in to GC allocations for the whole typepublicpartialstructLogEvent{[DdsKey]publicintId;// Standard string (Heap allocated)publicstringMessage;// Standard List (Heap allocated)publicList<double>History;// Nested custom structpublicGeoPointOrigin;}You can define Quality of Service settings directly on the type using the [DdsQos] attribute. The Runtime automatically applies these settings when creating Writers and Readers for this topic.
[DdsTopic("MachineState")][DdsQos(Reliability=DdsReliability.Reliable,// Guarantee deliveryDurability=DdsDurability.TransientLocal,// Late joiners get the last valueHistoryKind=DdsHistoryKind.KeepLast,// Keep only recent dataHistoryDepth=1// Only the latest sample)]publicpartialstructMachineState{[DdsKey]publicintMachineId;publicStateEnumCurrentState;}usingFactory.Monitoring;usingvarparticipant=newDdsParticipant();// Auto-discovers topic type and its default name ("Factory_Monitoring_SensorData")usingvarwriter=newDdsWriter<SensorData>(participant);// Zero-allocation write pathvardata=newSensorData{SensorId=1,Value=25.5,LocationId=newFixedString32("Factory_A"),Status=SensorStatus.Online};// With C# 12, InlineArrays can be accessed directly by indexdata.Measurements[0]=1.0f;writer.Write(data);Reading uses a Scope pattern to ensure safety and zero-copy semantics. You "loan" the data, read it, and return it by disposing the scope.
usingFactory.Monitoring;usingvarreader=newDdsReader<SensorData>(participant);// POLL FOR DATA// Returns a "Loan" which manages native memoryusingvarloan=reader.Take(maxSamples:10);// Iterate received dataforeach(varsampleinloan){// `sample.IsValid` indicates whether a full payload is present.// IMPORTANT: even when `ValidData == 0` (lifecycle/metadata-only samples),// the middleware provides the native memory with the topic key fields populated.// Therefore `sample.Data` is safe to call for every sample and will return// a managed object where key fields are set and non-key fields are defaulted.// Always obtain the managed copy (safe for metadata-only samples too)vardata=sample.Data;if(sample.IsValid){// OPTION A: Simple (Managed)// `data` is a full managed copy populated from native memoryConsole.WriteLine($"Received: {data.SensorId} = {data.Value}");}else{// Lifecycle event (e.g., instance disposed). Key fields are available in `data`.Console.WriteLine($"Instance {data.SensorId} state: {sample.Info.InstanceState}");}// OPTION B: Fast (Zero-Copy) — you can still use AsView() when you only need// transient, zero-allocation access to the native buffer (stack-only).// var view = sample.AsView();}Bridge the gap between real-time DDS and .NET Tasks. No blocking threads required.
Console.WriteLine("Waiting for data...");// Efficiently waits using TaskCompletionSource (no polling loop)while(awaitreader.WaitDataAsync()){// Take all available datausingvarscope=reader.Take();foreach(varsampleinscope){awaitProcessAsync(sample);}}Filter data before you pay the cost of processing it. This implementation uses C# delegates but executes on the raw buffer view, allowing JIT optimizations to make it extremely fast.
// 1. Set a filter predicate on the Reader// Logic executes during iteration, skipping irrelevant samples instantly.// Since 'view' is a ref struct reading raw memory, this is Zero-Copy filtering.reader.SetFilter(view =>view.Value>100.0&&view.LocationId.ToString()=="Lab_1");// 2. Iterateusingvarscope=reader.Take();foreach(varhighValueSampleinscope){// Guaranteed to be > 100.0 and from Lab_1}// 3. Update filter dynamically at runtimereader.SetFilter(null);// Clear filterFor systems tracking many objects (fleets, tracks, sensors), efficiently query a specific object's history without iterating the entire database.
// 1. Create a key template for the object we care aboutvarkey=newSensorData{SensorId=5};// 2. Lookup the Handle (O(1) hashing)DdsInstanceHandlehandle=reader.LookupInstance(key);if(!handle.IsNil){// 3. Read history for ONLY Sensor 5// Ignores Sensor 1, 2, 3... Zero iteration overhead.usingvarhistory=reader.ReadInstance(handle,maxSamples:100);foreach(varsnapshotinhistory){Plot(snapshot.Value);}}Identify exactly which application instance sent a message. Essential for multi-process debugging.
varconfig=newSenderIdentityConfig{AppDomainId=1,AppInstanceId=100};// Enable tracking BEFORE creating writersparticipant.EnableSenderTracking(config);// Now, every writer created by this participant automatically broadcasts identityusingvarwriter=newDdsWriter<LogEvent>(participant,"Logs");// Enable tracking on the readerreader.EnableSenderTracking(participant.SenderRegistry);usingvarscope=reader.Take();for(inti=0;i<scope.Count;i++){// O(1) Lookup of sender info// Returns: ComputerName, ProcessName, ProcessId, AppDomainId, etc.varsender=scope.GetSender(i);varmsg=scope[i];if(sender!=null){Console.WriteLine($"[{sender.ComputerName} : PID {sender.ProcessId}] says: {msg.Message}");}}Know when peers connect or disconnect using standard C# Events.
// Writer Sidewriter.PublicationMatched+=(s,status)=>{if(status.CurrentCountChange>0)Console.WriteLine($"Subscriber connected! Total: {status.CurrentCount}");elseConsole.WriteLine("Subscriber lost.");};// Reliable Startup (Wait for Discovery)// Solves the "Lost First Message" problemawaitwriter.WaitForReaderAsync(TimeSpan.FromSeconds(5));writer.Write(newMessage("Hello"));// Guaranteed to have a routeProperly manage the lifecycle of data instances in the Global Data Space.
varkey=newSensorData{SensorId=1};// 1. Data is invalid/deleted// Readers receive InstanceState = NOT_ALIVE_DISPOSEDwriter.DisposeInstance(key);// 2. Writer is shutting down (graceful disconnect)// Readers receive InstanceState = NOT_ALIVE_NO_WRITERS (if ownership exclusive)writer.UnregisterInstance(key);DDS partitions let you divide a domain into named logical channels. Readers and writers only communicate within the same partition, making it easy to run multiple isolated subsystems on the same DDS domain (e.g. separate a monitoring plane from a control plane, or multiplex tenants).
// All readers and writers created from this participant will use "monitoring" automatically.usingvarparticipant=newDdsParticipant(domainId:0,defaultPartition:"monitoring");// Topic name comes from [DdsTopic("SensorData")] — no need to repeat it.usingvarreader=newDdsReader<SensorData>(participant);usingvarwriter=newDdsWriter<SensorData>(participant);usingvarparticipant=newDdsParticipant(0,defaultPartition:"*");// wildcard default// This writer specifically targets the "control" partition.usingvarcontrolWriter=newDdsWriter<SensorData>(participant,"SensorData",partition:"control");// This reader stays on the default "*" partition — sees everything.usingvarbroadcastReader=newDdsReader<SensorData>(participant);per-reader / per-writer partition → participant.DefaultPartition → (no partition)
DdsWaitSet provides a native-backed mechanism for sleeping on many readers simultaneously on a single OS thread. This is ideal for monitoring applications that track 100+ topics and do not want the overhead of spawning a background Task per reader.
usingvarparticipant=newDdsParticipant(0,defaultPartition:"*");// Create readers for every topic you want to monitorusingvartempReader=newDdsReader<TemperatureEvent>(participant);usingvarpressReader=newDdsReader<PressureEvent>(participant);usingvarstatusReader=newDdsReader<MachineStatus>(participant);// Create WaitSet and attach all readersusingvarwaitset=newDdsWaitSet(participant);waitset.Attach(tempReader);waitset.Attach(pressReader);waitset.Attach(statusReader);// Pre-allocate result buffer once — no allocation inside the loopIDdsReader[]triggered=newIDdsReader[16];varcts=newCancellationTokenSource();while(!cts.IsCancellationRequested){// Blocks until at least one reader has data, or the timeout expires, or ct is cancelled.// Zero allocation in this hot path.intcount=waitset.Wait(triggered.AsSpan(),timeout:TimeSpan.FromSeconds(1),cts.Token);for(inti=0;i<count;i++){switch(triggered[i]){caseDdsReader<TemperatureEvent>r:using(varloan=r.Take()){/* handle temp */}break;caseDdsReader<PressureEvent>r:using(varloan=r.Take()){/* handle pressure */}break;caseDdsReader<MachineStatus>r:using(varloan=r.Take()){/* handle status */}break;}}}Readers can be added or removed while the WaitSet is not waiting, making the monitored set dynamic:
// Start watching a new topic at runtimevarnewReader=newDdsReader<AlarmEvent>(participant);waitset.Attach(newReader);// Stop watching (and dispose the reader when no longer needed)waitset.Detach(newReader);newReader.Dispose();Pass a CancellationToken to Wait to interrupt the blocking native call safely from any thread:
cts.Cancel();// triggers the native guard condition, unblocks Wait() instantly| Operation | Allocation | Notes |
|---|---|---|
Wait(...) hot path | 0 Bytes | ArrayPool rent inside; result written into caller's Span |
Attach / Detach | Small (one-time) | GCHandle + dictionary entry per reader |
| Cancellation callback | 0 Bytes | Triggers native guard condition via P/Invoke |
If you have existing DDS systems defined in IDL, you can generate the corresponding C# DSL automatically.
# Import IDL to C#
CycloneDDS.IdlImporter MySystem.idl ./src/GeneratedThis generates C# [DdsTopic] structs that are binary-compatible with your existing system.
See IDL Import Guide for advanced usage including multi-module support.
A complete "Hello World" example that demonstrates creating a topic, publishing, and subscribing in a single application can be found in examples/HelloWorld.
This example is designed to verify the NuGet package installation and basic functionality using the locally built package.
To run it:
- Build the packages:
.\build\pack.ps1 - Run the example:
cd examples/HelloWorld dotnet run
The CycloneDDS.NET package bundles these internal components:
- Managed Libraries:
CycloneDDS.Core,CycloneDDS.Schema,CycloneDDS.CodeGen,CycloneDDS.Runtime - Native Assets (Windows x64):
ddsc.dll(Cyclone DDS),idlc.exe(IDL Compiler),cycloneddsidljson.dll(IDL JSON plugin) - Native Assets (Linux x64):
libddsc.so(Cyclone DDS),idlc(IDL Compiler),libcycloneddsidljson.so(IDL JSON plugin)
| Feature | Allocation Cost | Performance Note |
|---|---|---|
| Write | 0 Bytes | Uses ArrayPool + NativeArena |
| Read (View) | 0 Bytes | Uses .AsView() + Ref Structs |
| Read (Managed) | Allocates | Uses .Data (Deep Copy) |
| Take (Polling) | 0 Bytes | Uses Loaned Buffers |
| Filtering | 0 Bytes | Manual loop filtering with Views |
| Sender Lookup | 0 Bytes | O(1) Dictionary Lookup |
| Async Wait | ~80 Bytes | One Task per await cycle |
| WaitSet.Wait | 0 Bytes | Span output + ArrayPool rent; no heap in hot path |
Built for speed. Designed for developers.