https://github.com/pedrosakuma/sbesourcegenerator
Roslyn incremental source generator that converts FIX SBE XML schemas into zero-allocation C# structs with explicit memory layout. AOT-compatible, zero dependencies.
https://github.com/pedrosakuma/sbesourcegenerator
binary-encoding code-generation csharp dotnet fix-protocol high-performance roslyn sbe source-generator zero-allocation
Last synced: 2 days ago
JSON representation
Roslyn incremental source generator that converts FIX SBE XML schemas into zero-allocation C# structs with explicit memory layout. AOT-compatible, zero dependencies.
- Host: GitHub
- URL: https://github.com/pedrosakuma/sbesourcegenerator
- Owner: pedrosakuma
- License: mit
- Created: 2024-04-04T23:29:15.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2026-04-25T02:41:33.000Z (3 days ago)
- Last Synced: 2026-04-25T04:24:42.032Z (3 days ago)
- Topics: binary-encoding, code-generation, csharp, dotnet, fix-protocol, high-performance, roslyn, sbe, source-generator, zero-allocation
- Language: C#
- Homepage: https://www.nuget.org/packages/SbeSourceGenerator/
- Size: 1.14 MB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# SBE Code Generator for C#
[](https://github.com/pedrosakuma/SbeSourceGenerator/actions/workflows/ci.yml)
[](https://www.nuget.org/packages/SbeSourceGenerator/)
[](https://www.nuget.org/packages/SbeSourceGenerator/)
A Roslyn-based source generator that converts FIX Simple Binary Encoding (SBE) XML schemas into efficient, type-safe C# code.
## Features
- All SBE primitive types (int8–int64, uint8–uint64, float, double, char)
- Message encoding/decoding with proper field layout
- Optional fields with null value semantics
- Composite types (nested composites, `` elements)
- Enumerations and bit sets (flag enums) with `Has(flag)` / `Is{Flag}()` extension helpers
- Repeating groups with nested groups (unlimited depth, ancestor context callbacks with `in`)
- `foreach`-style zero-allocation enumerator on simple top-level groups (v1.5.0)
- Variable-length data (varData) with configurable length prefix (uint8/uint16/uint32)
- Constant fields in messages, composites, and groups
- Derived numeric constants (`MinValue`/`MaxValue` on type wrappers; `Decimals`/`Multiplier`/`MultiplierDecimal`/`Divisor` on decimal composites)
- Allocation-free formatting via `ISpanFormattable` on char types and decimal composites; `AsTrimmedSpan()` on InlineArray char types
- Static header parsing helpers (`TryReadTemplateId` / `TryReadHeader`) on the message header composite
- Automatic and explicit field offset calculation
- Byte order handling (little-endian native, big-endian with `readonly` property-based conversion)
- Schema versioning (`sinceVersion`, `deprecated` on fields, enums, sets, data) with per-message `{Msg}VersionMap` for `blockLength → version` lookup
- Schema evolution forward/backward compatibility via `TryReadBlock`
- Cross-schema coexistence (multiple schemas with isolated namespaces)
- Custom `headerType` support
- `characterEncoding` attribute (UTF-8, Latin1)
- Explicit `blockLength` on messages
- Validation constraints (min/max ranges)
- Zero-cost `SbeDispatcher` + `ISbeMessageHandler` for devirtualized message routing
- Comprehensive build-time diagnostics (SBE001–SBE014)
## What's New in v1.5.0
```csharp
// #156: foreach-style enumerator for top-level simple groups — zero alloc, no closure capture
if (OrderBookData.TryParse(buffer, out var reader))
{
foreach (ref readonly var bid in reader.Bids) Process(in bid);
foreach (ref readonly var ask in reader.Asks) Process(in ask);
}
```
Independent per-group views — out-of-order access, early `break`, and repeated iteration are all safe (no shared cursor). Emitted only when all top-level groups are simple (no nested groups, no group-level varData); otherwise `ReadGroups` remains the only API.
## What's New in v1.4.0
```csharp
// #155: AsTrimmedSpan — like AsSpan but strips trailing space/null padding (FIX convention) without allocating.
ReadOnlySpan sym = msg.Symbol.AsTrimmedSpan();
if (sym.SequenceEqual("PETR4"u8)) { /* ... */ }
// #153: ISpanFormattable on char types and decimal composites — allocation-free formatting
Span dest = stackalloc char[32];
msg.Price.TryFormat(dest, out int written, default, CultureInfo.InvariantCulture);
// #156 (helpers): single source of truth for header layout
if (MessageHeader.TryReadHeader(buffer, out var blockLength, out var templateId, out var schemaId, out var version))
{
// dispatch without hardcoding offsets
}
```
## What's New in v1.2.0
Four additive consumer-ergonomics features — all emit-time only, zero impact on the encode/decode hot path:
```csharp
// #144: inlinable bit-test helpers on [Flags] sets
if (order.Flags.IsOddLot() && order.Flags.Has(TradingFlags.PreMarket | TradingFlags.AfterHours)) { ... }
// #145: derived constants on decimal composites
decimal price = mantissa * Price.MultiplierDecimal; // single source of truth for scale
string formatted = price.ToString($"F{Price.Decimals}");
// #146: blockLength → version lookup for evolved messages
if (EvolvingOrderVersionMap.TryGetVersion(header.BlockLength, out int version)) { ... }
// #147: zero-cost devirtualized dispatch (struct handler is JIT-specialized)
struct MyHandler : ISbeMessageHandler {
public void OnTrade(in TradeDataReader r, int blockLength, int version) { /* hot path */ }
/* ... OnXxx for each message ... */
public void OnUnknownMessage(int templateId, int blockLength, int version, ReadOnlySpan payload) { }
}
var handler = new MyHandler();
SbeDispatcher.Dispatch(buffer, ref handler);
```
See the [v1.2.0 entry in CHANGELOG.md](./CHANGELOG.md) for the full list.
## What's New in v1.0.0
**Zero-copy `MessageDataReader`** — `TryParse` returns a lightweight ref struct that holds a reference directly into the buffer. Access fields via `reader.Data` (zero-copy `ref readonly`), iterate top-level groups via `foreach` (since v1.5.0, no closure alloc) or `reader.ReadGroups(...)`, and recover the raw wire bytes via `reader.Buffer` / `reader.Block` (since v1.3.0) for replay/forwarding scenarios.
```csharp
if (CarData.TryParse(buffer, out var car))
{
Console.WriteLine(car.Data.SerialNumber); // zero-copy field access
// v1.5.0: foreach-style enumerators on simple top-level groups (zero alloc, no closures).
foreach (ref readonly var fuel in car.FuelFigures) { /* ... */ }
// For groups with nested groups or group-level varData, ReadGroups remains:
car.ReadGroups(
(in FuelFiguresData fuel) => { /* ... */ },
(in PerformanceFiguresData perf, AccelerationData.AccelerationHandler onAccel) => { /* ... */ });
Console.WriteLine($"Total bytes: {car.BytesConsumed}");
}
```
> **Migrating from v0.9.x?** See the [migration guide in CHANGELOG.md](./CHANGELOG.md#100---2026-04-10).
## Quick Start
### 1. Install
Install the NuGet package:
```bash
dotnet add package SbeSourceGenerator
```
Or add it directly to your `.csproj`:
```xml
```
> **For local development**, use a project reference instead:
> ```xml
>
> OutputItemType="Analyzer"
> ReferenceOutputAssembly="false" />
>
> ```
### 2. Add Your Schema
Add your SBE XML schema as an additional file:
```xml
```
### 3. Build
Build your project. The generator will automatically create C# types from your schema.
### 4. Use Generated Code
**Simple Messages:**
```csharp
// Create and encode a simple message
var trade = new TradeData
{
TradeId = 123456,
Price = 9950,
Quantity = 100,
Side = Side.Buy
};
// Encode to binary format
byte[] buffer = new byte[TradeData.MESSAGE_SIZE];
if (trade.TryEncode(buffer, out int bytesWritten))
{
// Send via network
await socket.SendAsync(buffer.AsMemory(0, bytesWritten));
}
// Decode from binary format
if (TradeData.TryParse(receivedBuffer, out var reader))
{
Console.WriteLine($"Trade: {reader.Data.TradeId}, Price: {reader.Data.Price}");
}
```
**Messages with Repeating Groups (Span-Based API):**
```csharp
// Create message with groups
var orderBook = new OrderBookData { InstrumentId = 42 };
var bids = new[] {
new BidsData { Price = 1000, Quantity = 100 },
new BidsData { Price = 1010, Quantity = 101 }
};
var asks = new[] {
new AsksData { Price = 2000, Quantity = 200 }
};
// Encode with comprehensive TryEncode - enforces correct schema order
Span buffer = stackalloc byte[1024];
bool success = OrderBookData.TryEncode(
orderBook,
buffer,
bids, // Groups/varData in schema-defined order
asks, // Compiler ensures correct parameter order
out int bytesWritten
);
// Decode with groups — zero-copy via MessageDataReader
if (OrderBookData.TryParse(buffer, out var reader))
{
Console.WriteLine($"Instrument: {reader.Data.InstrumentId}");
// v1.5.0: foreach for simple top-level groups — no closure alloc, supports break.
foreach (ref readonly var bid in reader.Bids) Console.WriteLine($"Bid: {bid.Price}");
foreach (ref readonly var ask in reader.Asks) Console.WriteLine($"Ask: {ask.Price}");
// ReadGroups still works (and is required for nested groups / group-level varData):
// reader.ReadGroups(
// (in BidsData bid) => Console.WriteLine($"Bid: {bid.Price}"),
// (in AsksData ask) => Console.WriteLine($"Ask: {ask.Price}")
// );
}
```
**Messages with Repeating Groups (Zero-Allocation Callback API):**
```csharp
// For high-performance scenarios: use callbacks to avoid array allocations
var orderBook = new OrderBookData { InstrumentId = 42 };
Span buffer = stackalloc byte[1024];
bool success = OrderBookData.TryEncode(
orderBook,
buffer,
bidCount: 3,
bidsEncoder: (int index, ref BidsData item) => {
// Populate item from your data source without allocations
item.Price = GetBidPrice(index);
item.Quantity = GetBidQuantity(index);
},
askCount: 2,
asksEncoder: (int index, ref AsksData item) => {
item.Price = GetAskPrice(index);
item.Quantity = GetAskQuantity(index);
},
out int bytesWritten
);
```
**Messages with Variable-Length Data (Span-Based API):**
```csharp
// Encode message with varData
var order = new NewOrderData { OrderId = 123, Price = 9950 };
var symbolBytes = Encoding.UTF8.GetBytes("AAPL");
Span buffer = stackalloc byte[512];
bool success = NewOrderData.TryEncode(
order,
buffer,
symbolBytes, // VarData in schema-defined order
out int bytesWritten
);
// Decode varData — zero-copy via MessageDataReader
if (NewOrderData.TryParse(buffer, out var reader))
{
Console.WriteLine($"Order: {reader.Data.OrderId}");
reader.ReadGroups(
symbol => {
var text = Encoding.UTF8.GetString(symbol.VarData.Slice(0, symbol.Length));
Console.WriteLine($"Symbol: {text}");
}
);
}
```
## Example Schema
```xml
0
1
```
## Generated Code
The generator creates:
- **Enums**: C# enums for SBE enum types
- **Sets**: C# flag enums for SBE set types
- **Composites**: C# structs for composite types
- **Types**: Type aliases and wrappers
- **Messages**: C# structs for SBE messages
- **Groups**: Nested structs for repeating groups
All generated types use `[StructLayout(LayoutKind.Explicit)]` with `[FieldOffset]` attributes for efficient binary serialization.
## Architecture
```
SBESourceGenerator (Orchestrator)
│
├── TypesCodeGenerator
│ ├── Enums
│ ├── Sets
│ ├── Composites
│ └── Types
│
├── MessagesCodeGenerator
│ └── Messages with Groups, parsing helpers, and {Msg}VersionMap
│
├── DispatcherGenerator
│ ├── ISbeMessageHandler (per-schema interface)
│ └── SbeDispatcher (struct-generic zero-cost dispatch)
│
└── UtilitiesCodeGenerator
├── SpanReader
└── SpanWriter
```
See [ARCHITECTURE_DIAGRAMS.md](./docs/ARCHITECTURE_DIAGRAMS.md) for detailed architecture diagrams.
## Diagnostics
The generator provides comprehensive diagnostics:
| Code | Severity | Description |
|------|----------|-------------|
| SBE001 | Error | Invalid integer attribute value |
| SBE002 | Error | Missing required attribute |
| SBE003 | Error | Invalid enum flag value |
| SBE004 | Error | Malformed schema |
| SBE005 | Warning | Unsupported construct |
| SBE006 | Error | Invalid type length |
| SBE007 | Info | Big-endian schema with conditional byte swap |
| SBE008 | Error | Unresolved type reference |
| SBE009 | Warning | Invalid numeric constraint |
| SBE010 | Warning | Unknown primitive type fallback |
| SBE011 | Error | Set choice bit position exceeds encoding width |
| SBE012 | Warning | Invalid SbeAssumeHostEndianness value |
| SBE013 | Warning | Duplicate type name |
| SBE014 | Warning | sinceVersion exceeds schema version |
See [Diagnostics README](./src/SbeCodeGenerator/Diagnostics/README.md) for details.
## Testing
### Run Unit Tests
```bash
dotnet test tests/SbeCodeGenerator.Tests/
```
### Run Integration Tests
```bash
dotnet test tests/SbeCodeGenerator.IntegrationTests/
```
### All Tests
```bash
dotnet test
```
## Documentation
- **[Changelog](./CHANGELOG.md)** - Version history and release notes
- **[Contributing](./CONTRIBUTING.md)** - Development setup and guidelines
- **[Architecture Diagrams](./docs/ARCHITECTURE_DIAGRAMS.md)** - System architecture
- **[Testing Guide](./docs/TESTING_GUIDE.md)** - How to test the generator
- **[CI/CD Pipeline](./docs/CICD_PIPELINE.md)** - CI/CD configuration and NuGet publishing
## Project Structure
```
SbeSourceGenerator/
├── src/
│ └── SbeCodeGenerator/ # Source generator implementation
│ ├── Diagnostics/ # Diagnostic descriptors
│ ├── Generators/ # Code generators
│ │ ├── Fields/ # Field generators
│ │ └── Types/ # Type generators
│ ├── Helpers/ # Helper utilities
│ └── Schema/ # DTOs and parsing
├── tests/
│ ├── SbeCodeGenerator.Tests/ # Unit tests
│ └── SbeCodeGenerator.IntegrationTests/ # Integration tests
├── examples/ # Example applications
│ ├── SbeBinanceConsole/ # Binance market data processing
│ └── HighPerformanceMarketData/ # High-performance SBE patterns
├── benchmarks/ # BenchmarkDotNet performance benchmarks
├── profiling/ # Profiling tools and scripts
└── docs/ # Documentation files
```
## Examples
The repository includes several example projects in the `examples/` folder:
1. **SbeBinanceConsole** - Binance market data processing with live dashboard
2. **HighPerformanceMarketData** - High-performance SBE message processing patterns
## Performance
The generated code is designed for high performance:
- Zero-copy deserialization via `MessageDataReader` ref struct (`reader.Data` is `ref readonly` into buffer)
- Zero-copy `ReadBlockRef` for advanced SpanReader usage
- `ReadGroups` with `in` delegate callbacks (no struct copies)
- `readonly` property getters to prevent defensive copies
- Struct-based value types (no heap allocations)
- Explicit memory layout for cache efficiency
- Blittable types for P/Invoke scenarios
**Benchmark Infrastructure**: Benchmarks using BenchmarkDotNet are available in the `benchmarks/` directory.
## Compliance
The generator implements the FIX SBE 1.0 specification, including all core encoding types, repeating groups (with nesting), variable-length data, schema versioning, and field presence modes. See [Changelog](./CHANGELOG.md) for version history.
## Contributing
Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
### Development Setup
1. Clone the repository
2. Open `SbeCodeGenerator.sln` in Visual Studio 2022+ or VS Code
3. Build the solution: `dotnet build`
4. Run tests: `dotnet test`
## Requirements
- .NET SDK 9.0+ (for building examples and tests)
- The generator itself targets **netstandard2.0** and works with any compatible runtime
- Roslyn Source Generators support (Visual Studio 2022+, .NET SDK 6.0+)
## References
- [FIX Simple Binary Encoding (SBE) Standard](https://www.fixtrading.org/standards/sbe/)
- [SBE GitHub Repository](https://github.com/FIXTradingCommunity/fix-simple-binary-encoding)
- [Real Logic SBE Implementation](https://github.com/real-logic/simple-binary-encoding)
## License
See [LICENSE.txt](./LICENSE.txt) for license information.
## Support
For questions, issues, or feature requests:
- Open an issue on GitHub
- Check existing documentation
- Review example projects
---
**Version**: 1.0.2 (see [Changelog](./CHANGELOG.md))