Skip to content

Latest commit

History

213 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

FastCycloneDDS C# Bindings

CINuGetLicense: MIT

A modern, high-performance, zero-allocation .NET binding for Eclipse Cyclone DDS, with idiomatic C# API.

See detailed technical overview.

Installation

Using the NuGet Package (Recommended)

Install the CycloneDDS.NET package from NuGet:

dotnet add package CycloneDDS.NET

This 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.

Supported platforms

PlatformNative runtimeTarget requirement
Windows x64ddsc.dllVisual C++ Redistributable for Visual Studio 2022 installed on the target machine.
Linux x64libddsc.soA glibc-based distribution (e.g. Ubuntu/Debian). No extra runtime install needed.

Working with Source Code

If you want to build the project from source or contribute:

  1. Clone the repository (recursively, to get the native submodule):

    git clone --recursive https://github.com/pjanec/CycloneDds.NET.git
    cd CycloneDds.NET
  2. Build 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 Release

    On 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.

  3. 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.

Key Features

🚀 Performance Core

  • 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 struct views, 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.

🧬 Schema & Interoperability

  • 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 without unsafe), typed enums (enum E : byte emits @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 IdlImporter tool.
  • 100% Native Compliance: Uses Cyclone DDS native serializer for wire compatibility.

🛠️ Developer Experience

  • Auto-Magic Type Discovery: No manual IDL compilation or type registration required.
  • Async/Await:WaitDataAsync for 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.

📡 Partitioning & Monitoring

  • 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 supports CancellationToken for instant, safe interruption.

1. Defining Data (The Schema)

Define your data using standard C# partial structs. The build tools generate the serialization logic automatically.

High-Performance Schema (Zero Alloc)

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}

Unmanaged Schema (Unsafe Fixed Buffers)

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];}

Convenient Schema (Managed Types)

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;}

Configuration & QoS

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;}

2. Basic Usage

Publishing

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);

Subscribing (Polling)

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();}

3. Async/Await (Modern Loop)

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);}}

4. Advanced Filtering

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 filter

5. Instance Management (Keyed Topics)

For 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);}}

6. Sender Tracking (Identity)

Identify exactly which application instance sent a message. Essential for multi-process debugging.

Sender Configuration

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");

Receiver Usage

// 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}");}}

7. Status & Discovery

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 route

8. Lifecycle (Dispose & Unregister)

Properly 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);

9. Partitions

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).

Set a partition on the participant (inherited by all readers/writers)

// 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);

Override the partition per reader / writer

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);

Resolution order

per-reader / per-writer partition → participant.DefaultPartition → (no partition)

10. WaitSet — Efficient Multi-Reader Monitoring

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.

Basic usage

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;}}}

Attach / Detach at runtime

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();

CancellationToken

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

Performance characteristics

OperationAllocationNotes
Wait(...) hot path0 BytesArrayPool rent inside; result written into caller's Span
Attach / DetachSmall (one-time)GCHandle + dictionary entry per reader
Cancellation callback0 BytesTriggers native guard condition via P/Invoke

11. Legacy IDL Import

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/Generated

This generates C# [DdsTopic] structs that are binary-compatible with your existing system. See IDL Import Guide for advanced usage including multi-module support.


Examples

Hello World

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:

  1. Build the packages: .\build\pack.ps1
  2. Run the example:
    cd examples/HelloWorld
    dotnet run

Dependencies

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)

Performance Characteristics

FeatureAllocation CostPerformance Note
Write0 BytesUses ArrayPool + NativeArena
Read (View)0 BytesUses .AsView() + Ref Structs
Read (Managed)AllocatesUses .Data (Deep Copy)
Take (Polling)0 BytesUses Loaned Buffers
Filtering0 BytesManual loop filtering with Views
Sender Lookup0 BytesO(1) Dictionary Lookup
Async Wait~80 BytesOne Task per await cycle
WaitSet.Wait0 BytesSpan output + ArrayPool rent; no heap in hot path

Built for speed. Designed for developers.

About

.NET C# bindings for cyclone dds, code-first, high performance, zero-allocation

Resources

Contributing

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages