From 2c1f6e10168b743f0619fb2bfefdd64a4f0803f8 Mon Sep 17 00:00:00 2001 From: Zach Bray Date: Wed, 27 Sep 2023 13:48:36 +0100 Subject: [PATCH] [C#] Generate DTOs from SBE IR for non-perf-sensitive usecases. In some applications performance is not cricital. Some users would like to use SBE across their whole "estate", but don't want the "sharp edges" associated with using flyweight codecs, e.g., accidental escape. In this commit, I've added a first cut of DTO generation for C# and a simple test based on the Car Example. The DTOs support encoding and decoding via the generated codecs using `EncodeInto(CodecT codec)` and `DecodeFrom(CodecT codec)` methods. Currently there is no support for equality/comparison or read-only views over the data; although, these have been requested. Here are some points that we may or may not wish to change in the future: 1. Non-present (due to the encoded version) string/array data and repeating groups are represented as `null` rather than empty. 2. Non-present primitive values are represented as their associated null value rather than using nullable types. 3. Non-present bitsets are represented as `0`. 4. DTOs are generated via a separate `CodeGenerator` rather than a flag to the existing C# `CodeGenerator`. --- build.gradle | 16 +- csharp/sbe-dll/DirectBuffer.cs | 7 +- csharp/sbe-tests/DtoTests.cs | 110 ++ .../generation/csharp/CSharpDtoGenerator.java | 1076 +++++++++++++++++ .../sbe/generation/csharp/CSharpDtos.java | 35 + .../generation/csharp/CSharpGenerator.java | 109 +- .../sbe/generation/csharp/CSharpUtil.java | 116 +- 7 files changed, 1353 insertions(+), 116 deletions(-) create mode 100644 csharp/sbe-tests/DtoTests.cs create mode 100644 sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtoGenerator.java create mode 100644 sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtos.java diff --git a/build.gradle b/build.gradle index 2761b537e4..df43bef287 100644 --- a/build.gradle +++ b/build.gradle @@ -723,7 +723,7 @@ tasks.register('generateCSharpCodecsWithXIncludes', JavaExec) { 'sbe-samples/src/main/resources/example-extension-schema.xml'] } -tasks.register('generateCSharpCodecsTests', JavaExec) { +tasks.register('generateCSharpTestCodecs', JavaExec) { mainClass.set('uk.co.real_logic.sbe.SbeTool') classpath = project(':sbe-tool').sourceSets.main.runtimeClasspath systemProperties( @@ -740,9 +740,21 @@ tasks.register('generateCSharpCodecsTests', JavaExec) { 'sbe-benchmarks/src/main/resources/fix-message-samples.xml'] } +tasks.register('generateCSharpTestDtos', JavaExec) { + mainClass.set('uk.co.real_logic.sbe.SbeTool') + classpath = project(':sbe-tool').sourceSets.main.runtimeClasspath + systemProperties( + 'sbe.output.dir': 'csharp/sbe-generated', + 'sbe.target.language': 'uk.co.real_logic.sbe.generation.csharp.CSharpDtos', + 'sbe.xinclude.aware': 'true', + 'sbe.validation.xsd': validationXsdPath) + args = ['sbe-samples/src/main/resources/example-extension-schema.xml'] +} + tasks.register('generateCSharpCodecs') { description = 'Generate csharp codecs' - dependsOn 'generateCSharpCodecsTests', + dependsOn 'generateCSharpTestCodecs', + 'generateCSharpTestDtos', 'generateCSharpCodecsWithXIncludes' } diff --git a/csharp/sbe-dll/DirectBuffer.cs b/csharp/sbe-dll/DirectBuffer.cs index 09eb64ec7e..87f08b96b5 100644 --- a/csharp/sbe-dll/DirectBuffer.cs +++ b/csharp/sbe-dll/DirectBuffer.cs @@ -694,7 +694,6 @@ public int SetBytes(int index, ReadOnlySpan src) /// /// Writes a string into the underlying buffer, encoding using the provided . - /// If there is not enough room in the buffer for the bytes it will throw IndexOutOfRangeException. /// /// encoding to use to write the bytes from the string /// source string @@ -702,13 +701,9 @@ public int SetBytes(int index, ReadOnlySpan src) /// count of bytes written public unsafe int SetBytesFromString(Encoding encoding, string src, int index) { - int available = _capacity - index; int byteCount = encoding.GetByteCount(src); - if (byteCount > available) - { - ThrowHelper.ThrowIndexOutOfRangeException(_capacity); - } + CheckLimit(index + byteCount); fixed (char* ptr = src) { diff --git a/csharp/sbe-tests/DtoTests.cs b/csharp/sbe-tests/DtoTests.cs new file mode 100644 index 0000000000..99fbcadc74 --- /dev/null +++ b/csharp/sbe-tests/DtoTests.cs @@ -0,0 +1,110 @@ +using Extension; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.SbeTool.Sbe.Dll; + +namespace Org.SbeTool.Sbe.Tests +{ + [TestClass] + public class DtoTests + { + [TestMethod] + public void ShouldRoundTripCar() + { + var inputByteArray = new byte[1024]; + var inputBuffer = new DirectBuffer(inputByteArray); + EncodeCar(inputBuffer); + var decoder = new Car(); + decoder.WrapForDecode(inputBuffer, 0, Car.BlockLength, Car.SchemaVersion); + var decoderString = decoder.ToString(); + var dto = new CarDto(); + dto.DecodeFrom(decoder); + var outputByteArray = new byte[1024]; + var outputBuffer = new DirectBuffer(outputByteArray); + var encoder = new Car(); + encoder.WrapForEncode(outputBuffer, 0); + dto.EncodeInto(encoder); + var dtoString = dto.ToString(); + CollectionAssert.AreEqual(inputByteArray, outputByteArray); + Assert.AreEqual(decoderString, dtoString); + } + + private static void EncodeCar(DirectBuffer buffer) + { + var car = new Car(); + car.WrapForEncode(buffer, 0); + car.SerialNumber = 1234; + car.ModelYear = 2013; + car.Available = BooleanType.T; + car.Code = Model.A; + car.SetVehicleCode("ABCDEF"); + + for (int i = 0, size = Car.SomeNumbersLength; i < size; i++) + { + car.SetSomeNumbers(i, (uint)i); + } + + car.Extras = OptionalExtras.CruiseControl | OptionalExtras.SportsPack; + + car.CupHolderCount = 119; + + car.Engine.Capacity = 2000; + car.Engine.NumCylinders = 4; + car.Engine.SetManufacturerCode("ABC"); + car.Engine.Efficiency = 35; + car.Engine.BoosterEnabled = BooleanType.T; + car.Engine.Booster.BoostType = BoostType.NITROUS; + car.Engine.Booster.HorsePower = 200; + + var fuelFigures = car.FuelFiguresCount(3); + fuelFigures.Next(); + fuelFigures.Speed = 30; + fuelFigures.Mpg = 35.9f; + fuelFigures.SetUsageDescription("this is a description"); + + fuelFigures.Next(); + fuelFigures.Speed = 55; + fuelFigures.Mpg = 49.0f; + fuelFigures.SetUsageDescription("this is a description"); + + fuelFigures.Next(); + fuelFigures.Speed = 75; + fuelFigures.Mpg = 40.0f; + fuelFigures.SetUsageDescription("this is a description"); + + Car.PerformanceFiguresGroup perfFigures = car.PerformanceFiguresCount(2); + perfFigures.Next(); + perfFigures.OctaneRating = 95; + + Car.PerformanceFiguresGroup.AccelerationGroup acceleration = perfFigures.AccelerationCount(3).Next(); + acceleration.Mph = 30; + acceleration.Seconds = 4.0f; + + acceleration.Next(); + acceleration.Mph = 60; + acceleration.Seconds = 7.5f; + + acceleration.Next(); + acceleration.Mph = 100; + acceleration.Seconds = 12.2f; + + perfFigures.Next(); + perfFigures.OctaneRating = 99; + acceleration = perfFigures.AccelerationCount(3).Next(); + + acceleration.Mph = 30; + acceleration.Seconds = 3.8f; + + acceleration.Next(); + acceleration.Mph = 60; + acceleration.Seconds = 7.1f; + + acceleration.Next(); + acceleration.Mph = 100; + acceleration.Seconds = 11.8f; + + car.SetManufacturer("Ford"); + car.SetModel("Fiesta"); + car.SetActivationCode("1234"); + } + } +} \ No newline at end of file diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtoGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtoGenerator.java new file mode 100644 index 0000000000..a0f1e21d9d --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtoGenerator.java @@ -0,0 +1,1076 @@ +/* + * Copyright 2013-2023 Real Logic Limited. + * Copyright (C) 2017 MarketFactory, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.co.real_logic.sbe.generation.csharp; + +import uk.co.real_logic.sbe.PrimitiveType; +import uk.co.real_logic.sbe.generation.CodeGenerator; +import uk.co.real_logic.sbe.generation.Generators; +import uk.co.real_logic.sbe.ir.Ir; +import uk.co.real_logic.sbe.ir.Signal; +import uk.co.real_logic.sbe.ir.Token; +import org.agrona.Verify; +import org.agrona.generation.OutputManager; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import static uk.co.real_logic.sbe.generation.csharp.CSharpUtil.*; +import static uk.co.real_logic.sbe.ir.GenerationUtil.collectFields; +import static uk.co.real_logic.sbe.ir.GenerationUtil.collectGroups; +import static uk.co.real_logic.sbe.ir.GenerationUtil.collectVarData; + +/** + * DTO generator for the CSharp programming language. + */ +@SuppressWarnings("CodeBlock2Expr") +public class CSharpDtoGenerator implements CodeGenerator +{ + private static final String INDENT = " "; + private static final String BASE_INDENT = INDENT; + + private final Ir ir; + private final OutputManager outputManager; + + /** + * Create a new C# DTO {@link CodeGenerator}. + * + * @param ir for the messages and types. + * @param outputManager for generating the DTOs to. + */ + public CSharpDtoGenerator(final Ir ir, final OutputManager outputManager) + { + Verify.notNull(ir, "ir"); + Verify.notNull(outputManager, "outputManager"); + + this.ir = ir; + this.outputManager = outputManager; + } + + /** + * {@inheritDoc} + */ + public void generate() throws IOException + { + generateDtosForTypes(); + + for (final List tokens : ir.messages()) + { + final Token msgToken = tokens.get(0); + final String codecClassName = formatClassName(msgToken.name()); + final String className = formatDtoClassName(msgToken.name()); + + final List messageBody = tokens.subList(1, tokens.size() - 1); + int offset = 0; + + final StringBuilder sb = new StringBuilder(); + + final List fields = new ArrayList<>(); + offset = collectFields(messageBody, offset, fields); + generateFields(sb, fields, BASE_INDENT + INDENT); + + final List groups = new ArrayList<>(); + offset = collectGroups(messageBody, offset, groups); + generateGroups(sb, codecClassName, groups, BASE_INDENT + INDENT); + + final List varData = new ArrayList<>(); + collectVarData(messageBody, offset, varData); + generateVarData(sb, varData, BASE_INDENT + INDENT); + + generateDecodeFrom(sb, codecClassName, fields, groups, varData, BASE_INDENT + INDENT); + generateEncodeInto(sb, codecClassName, fields, groups, varData, BASE_INDENT + INDENT); + generateDisplay(sb, codecClassName, "WrapForEncode", null, BASE_INDENT + INDENT); + + try (Writer out = outputManager.createOutput(className)) + { + out.append(generateFileHeader(ir.applicableNamespace(), "using System.Collections.Generic;\n")); + out.append(generateDocumentation(BASE_INDENT, msgToken)); + + out.append(BASE_INDENT).append("public sealed partial class ").append(className).append("\n") + .append(BASE_INDENT).append("{") + .append(sb) + .append(BASE_INDENT).append("}\n") + .append("}\n"); + } + } + } + + private void generateGroups( + final StringBuilder sb, + final String parentMessageClassName, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token groupToken = tokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + final String groupName = groupToken.name(); + final String groupClassName = formatDtoClassName(groupName); + + sb.append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("public List<").append(groupClassName).append("> ") + .append(formatPropertyName(groupName)) + .append(" { get; set; } = new List<").append(groupClassName).append(">();\n"); + + sb.append("\n") + .append(generateDocumentation(indent, groupToken)) + .append(indent).append("public sealed partial class ").append(groupClassName).append("\n") + .append(indent).append("{"); + + i++; + i += tokens.get(i).componentTokenCount(); + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + generateFields(sb, fields, indent + INDENT); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + final String codecClassName = parentMessageClassName + "." + formatClassName(groupName) + "Group"; + generateGroups(sb, codecClassName, groups, indent + INDENT); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + generateVarData(sb, varData, indent + INDENT); + + generateDecodeFrom(sb, codecClassName, fields, groups, varData, indent + INDENT); + generateEncodeInto(sb, codecClassName, fields, groups, varData, indent + INDENT); + + sb.append(indent).append("}\n"); + } + } + + private void generateCompositeDecodeFrom( + final StringBuilder sb, + final String codecClassName, + final List tokens, + final String indent) + { + sb.append("\n") + .append(indent).append("public void DecodeFrom(").append(codecClassName).append(" codec)\n") + .append(indent).append("{\n"); + + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + + generateFieldDecodeFrom(sb, token, token, codecClassName, indent + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + sb.append(indent).append("}\n"); + } + + private void generateCompositeEncodeInto( + final StringBuilder sb, + final String codecClassName, + final List tokens, + final String indent) + { + sb.append("\n") + .append(indent).append("public void EncodeInto(").append(codecClassName).append(" codec)\n") + .append(indent).append("{\n"); + + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + + generateFieldEncodeInto(sb, token, token, indent + INDENT); + + i += tokens.get(i).componentTokenCount(); + } + + sb.append(indent).append("}\n"); + } + + private void generateDecodeFrom( + final StringBuilder sb, + final String codecClassName, + final List fields, + final List groups, + final List varData, + final String indent) + { + sb.append("\n") + .append(indent).append("public void DecodeFrom(").append(codecClassName).append(" codec)\n") + .append(indent).append("{\n"); + + generateFieldsDecodeFrom(sb, fields, codecClassName, indent + INDENT); + generateGroupsDecodeFrom(sb, groups, indent + INDENT); + generateVarDataDecodeFrom(sb, varData, indent + INDENT); + + sb.append(indent).append("}\n"); + } + + private void generateFieldsDecodeFrom( + final StringBuilder sb, + final List tokens, + final String codecClassName, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token signalToken = tokens.get(i); + if (signalToken.signal() == Signal.BEGIN_FIELD) + { + final Token encodingToken = tokens.get(i + 1); + + generateFieldDecodeFrom(sb, signalToken, encodingToken, codecClassName, indent); + } + } + } + + private void generateFieldDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String codecClassName, final String indent) + { + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveDecodeFrom(sb, fieldToken, typeToken, codecClassName, indent); + break; + + case BEGIN_SET: + generatePropertyDecodeFrom(sb, fieldToken, "0", indent); + break; + + case BEGIN_ENUM: + final String enumName = formatClassName(typeToken.applicableTypeName()); + final String nullValue = formatNamespace(ir.packageName()) + "." + enumName + ".NULL_VALUE"; + generatePropertyDecodeFrom(sb, fieldToken, nullValue, indent); + break; + + case BEGIN_COMPOSITE: + generateComplexDecodeFrom(sb, fieldToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String codecClassName, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + return; + } + + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + final String nullValue = codecClassName + "." + formatPropertyName(fieldToken.name()) + "NullValue"; + generatePropertyDecodeFrom(sb, fieldToken, nullValue, indent); + } + else if (arrayLength > 1) + { + generateArrayDecodeFrom(sb, fieldToken, typeToken, indent); + } + } + + private void generateArrayDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + wrapInActingVersionCheck( + sb, + fieldToken, + indent, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName) + .append(" = codec.Get").append(formattedPropertyName).append("();\n"); + }, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName).append(" = null;\n"); + } + ); + } + else + { + final String typeName = cSharpTypeName(typeToken.encoding().primitiveType()); + + wrapInActingVersionCheck( + sb, + fieldToken, + indent, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName) + .append(" = new ").append(typeName).append("[") + .append(typeToken.arrayLength()).append("];\n") + .append(blkIndent).append("codec.").append(formattedPropertyName) + .append(".CopyTo(new Span<").append(typeName).append(">(") + .append(formattedPropertyName).append("));\n"); + }, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName).append(" = null;\n"); + } + ); + } + } + + private void generatePropertyDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final String nullValue, + final String indent) + { + + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + wrapInActingVersionCheck( + sb, + fieldToken, + indent, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName).append(" = codec.") + .append(formattedPropertyName).append(";\n"); + }, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName).append(" = ").append(nullValue).append(";\n"); + } + ); + } + + private void generateComplexDecodeFrom( + final StringBuilder sb, + final Token fieldToken, + final String indent) + { + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + wrapInActingVersionCheck( + sb, + fieldToken, + indent, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName).append(" = new ") + .append(formatDtoClassName(propertyName)).append("();\n") + .append(blkIndent).append(formattedPropertyName).append(".DecodeFrom(codec.") + .append(formattedPropertyName).append(");\n"); + }, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName).append(" = null;\n"); + } + ); + } + + private void generateGroupsDecodeFrom( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token groupToken = tokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + final String groupName = groupToken.name(); + final String formattedPropertyName = formatPropertyName(groupName); + final String groupDtoClassName = formatDtoClassName(groupName); + final String groupCodecVarName = groupName + "Codec"; + + sb.append("\n") + .append(indent) + .append(formattedPropertyName).append(" = new List<").append(groupDtoClassName).append(">();\n"); + + wrapInActingVersionCheck( + sb, + groupToken, + indent, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append("var ").append(groupCodecVarName).append(" = codec.") + .append(formattedPropertyName).append(";\n") + .append(blkIndent).append("while (").append(groupCodecVarName).append(".HasNext)\n") + .append(blkIndent).append("{\n") + .append(blkIndent).append(INDENT) + .append("var element = new ").append(groupDtoClassName).append("();\n") + .append(blkIndent).append(INDENT) + .append("element.DecodeFrom(").append(groupCodecVarName).append(".Next());\n") + .append(blkIndent).append(INDENT) + .append(formattedPropertyName).append(".Add(element);\n") + .append(blkIndent).append("}\n"); + }, + null + ); + + i++; + i += tokens.get(i).componentTokenCount(); + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + } + } + + private void generateVarDataDecodeFrom( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0; i < tokens.size(); i++) + { + final Token token = tokens.get(i); + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final Token varDataToken = Generators.findFirst("varData", tokens, i); + final String characterEncoding = varDataToken.encoding().characterEncoding(); + + final String formattedPropertyName = formatPropertyName(propertyName); + final String accessor = characterEncoding == null ? + "Get" + formattedPropertyName + "Bytes" : + "Get" + formattedPropertyName; + + wrapInActingVersionCheck( + sb, + token, + indent, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName) + .append(" = codec.").append(accessor).append("();\n"); + }, + (blkSb, blkIndent) -> + { + blkSb.append(blkIndent).append(formattedPropertyName).append(" = null;\n"); + } + ); + } + } + } + + private void wrapInActingVersionCheck( + final StringBuilder sb, + final Token token, + final String indent, + final BiConsumer generatePresentBlock, + final BiConsumer generateNotPresentBlock) + { + final String propertyName = token.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (token.version() > 0) + { + sb.append("\n").append(indent).append("if (codec.").append(formattedPropertyName) + .append("InActingVersion())\n") + .append(indent).append("{\n"); + generatePresentBlock.accept(sb, indent + INDENT); + sb.append(indent).append("}\n"); + if (null != generateNotPresentBlock) + { + sb.append(indent).append("else\n") + .append(indent).append("{\n"); + generateNotPresentBlock.accept(sb, indent + INDENT); + sb.append(indent).append("}\n"); + } + } + else + { + generatePresentBlock.accept(sb, indent); + } + } + + private void generateEncodeInto( + final StringBuilder sb, + final String codecClassName, + final List fields, + final List groups, + final List varData, + final String indent) + { + sb.append("\n") + .append(indent).append("public void EncodeInto(").append(codecClassName).append(" codec)\n") + .append(indent).append("{\n"); + + generateFieldsEncodeInto(sb, fields, indent + INDENT); + generateGroupsEncodeInto(sb, groups, indent + INDENT); + generateVarDataEncodeInto(sb, varData, indent + INDENT); + + sb.append(indent).append("}\n"); + } + + private void generateFieldsEncodeInto( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token signalToken = tokens.get(i); + if (signalToken.signal() == Signal.BEGIN_FIELD) + { + final Token encodingToken = tokens.get(i + 1); + generateFieldEncodeInto(sb, signalToken, encodingToken, indent); + } + } + } + + private void generateFieldEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + + switch (typeToken.signal()) + { + case ENCODING: + generatePrimitiveEncodeInto(sb, fieldToken, typeToken, indent); + break; + + case BEGIN_SET: + case BEGIN_ENUM: + generatePropertyEncodeInto(sb, fieldToken, indent); + break; + + case BEGIN_COMPOSITE: + generateComplexEncodeInto(sb, fieldToken, indent); + break; + + default: + break; + } + } + + private void generatePrimitiveEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + return; + } + + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + generatePropertyEncodeInto(sb, fieldToken, indent); + } + else if (arrayLength > 1) + { + generateArrayEncodeInto(sb, fieldToken, typeToken, indent); + } + } + + private void generateArrayEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + sb.append(indent).append("codec.Set").append(formattedPropertyName).append("(") + .append(formattedPropertyName).append(");\n"); + } + else + { + final String typeName = cSharpTypeName(typeToken.encoding().primitiveType()); + + sb.append(indent).append("new Span<").append(typeName).append(">(").append(formattedPropertyName) + .append(").CopyTo(codec.").append(formattedPropertyName).append("AsSpan());\n"); + } + } + + private void generatePropertyEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final String indent) + { + if (fieldToken.isConstantEncoding()) + { + return; + } + + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + + sb.append(indent).append("codec.").append(formattedPropertyName).append(" = ") + .append(formattedPropertyName).append(";\n"); + } + + private void generateComplexEncodeInto( + final StringBuilder sb, + final Token fieldToken, + final String indent) + { + final String propertyName = fieldToken.name(); + final String formattedPropertyName = formatPropertyName(propertyName); + sb.append(indent).append(formattedPropertyName).append(".EncodeInto(codec.") + .append(formattedPropertyName).append(");\n"); + } + + private void generateGroupsEncodeInto( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token groupToken = tokens.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + final String groupName = groupToken.name(); + final String formattedPropertyName = formatPropertyName(groupName); + final String groupCodecVarName = groupName + "Codec"; + + sb.append("\n") + .append(indent).append("var ").append(groupCodecVarName) + .append(" = codec.").append(formattedPropertyName) + .append("Count(").append(formattedPropertyName).append(".Count);\n\n") + .append(indent).append("foreach (var group in ").append(formattedPropertyName).append(")\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("group.EncodeInto(").append(groupCodecVarName) + .append(".Next()").append(");\n") + .append(indent).append("}\n\n"); + + i++; + i += tokens.get(i).componentTokenCount(); + + final List fields = new ArrayList<>(); + i = collectFields(tokens, i, fields); + + final List groups = new ArrayList<>(); + i = collectGroups(tokens, i, groups); + + final List varData = new ArrayList<>(); + i = collectVarData(tokens, i, varData); + } + } + + private void generateVarDataEncodeInto( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (final Token token : tokens) + { + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + + sb.append(indent).append("codec.Set").append(formatPropertyName(propertyName)) + .append("(").append(formatPropertyName(propertyName)).append(");\n"); + } + } + } + + private void generateDisplay( + final StringBuilder sb, + final String codecClassName, + final String wrapMethod, + final String actingVersion, + final String indent) + { + sb.append("\n") + .append(indent).append("public override string ToString()\n") + .append(indent).append("{\n") + .append(indent).append(INDENT) + .append("var buffer = new DirectBuffer(new byte[128], (ignored, newSize) => new byte[newSize]);\n") + .append(indent).append(INDENT).append("var codec = new ").append(codecClassName).append("();\n") + .append(indent).append(INDENT).append("codec."); + sb.append(wrapMethod).append("(buffer, 0"); + if (null != actingVersion) + { + sb.append(", ").append(actingVersion); + } + sb.append(");\n"); + sb.append(indent).append(INDENT).append("EncodeInto(codec);\n") + .append(indent).append(INDENT).append("StringBuilder sb = new StringBuilder();\n") + .append(indent).append(INDENT).append("codec.BuildString(sb);\n") + .append(indent).append(INDENT).append("return sb.ToString();\n") + .append(indent).append("}\n"); + } + + private void generateFields( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token signalToken = tokens.get(i); + if (signalToken.signal() == Signal.BEGIN_FIELD) + { + final Token encodingToken = tokens.get(i + 1); + final String propertyName = signalToken.name(); + + switch (encodingToken.signal()) + { + case ENCODING: + generatePrimitiveProperty(sb, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_ENUM: + generateEnumProperty(sb, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_SET: + generateBitSetProperty(sb, propertyName, signalToken, encodingToken, indent); + break; + + case BEGIN_COMPOSITE: + generateCompositeProperty(sb, propertyName, signalToken, encodingToken, indent); + break; + + default: + break; + } + } + } + } + + private void generateCompositeProperty( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String bitSetName = formatDtoClassName(typeToken.applicableTypeName()); + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(bitSetName).append(" ").append(formatPropertyName(propertyName)) + .append(" { get; set; } = new ").append(bitSetName).append("();\n"); + } + + private void generateBitSetProperty( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String enumName = formatClassName(typeToken.applicableTypeName()); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(enumName).append(" ") + .append(formatPropertyName(propertyName)).append(" { get; set; }\n"); + } + + private void generateEnumProperty( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String enumName = formatClassName(typeToken.applicableTypeName()); + + if (fieldToken.isConstantEncoding()) + { + final String constValue = fieldToken.encoding().constValue().toString(); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public static ").append(enumName).append(" ") + .append(formatPropertyName(propertyName)).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("get { return ") + .append(formatNamespace(ir.packageName())).append(".").append(constValue) + .append("; }\n") + .append(indent).append("}\n"); + } + else + { + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(enumName).append(" ").append(formatPropertyName(propertyName)) + .append(" { get; set; }\n"); + } + } + + private void generatePrimitiveProperty( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.isConstantEncoding()) + { + generateConstPropertyMethods(sb, propertyName, fieldToken, typeToken, indent); + } + else + { + generatePrimitivePropertyMethods(sb, propertyName, fieldToken, typeToken, indent); + } + } + + private void generatePrimitivePropertyMethods( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final int arrayLength = typeToken.arrayLength(); + + if (arrayLength == 1) + { + generateSingleValueProperty(sb, propertyName, fieldToken, typeToken, indent); + } + else if (arrayLength > 1) + { + generateArrayProperty(sb, propertyName, fieldToken, typeToken, indent); + } + } + + private void generateArrayProperty( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public string ") + .append(formatPropertyName(propertyName)).append(" { get; set; }\n"); + } + else + { + final String typeName = cSharpTypeName(typeToken.encoding().primitiveType()); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(typeName).append("[] ") + .append(formatPropertyName(propertyName)).append(" { get; set; }\n"); + } + } + + private void generateSingleValueProperty( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + final String typeName = cSharpTypeName(typeToken.encoding().primitiveType()); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public ").append(typeName).append(" ") + .append(formatPropertyName(propertyName)).append(" { get; set; }\n"); + } + + private void generateConstPropertyMethods( + final StringBuilder sb, + final String propertyName, + final Token fieldToken, + final Token typeToken, + final String indent) + { + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public static string ").append(toUpperFirstChar(propertyName)).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT) + .append("get { return \"").append(typeToken.encoding().constValue().toString()).append("\"; }\n") + .append(indent).append("}\n"); + } + else + { + final String literalValue = + generateLiteral(typeToken.encoding().primitiveType(), typeToken.encoding().constValue().toString()); + + sb.append("\n") + .append(generateDocumentation(indent, fieldToken)) + .append(indent).append("public static ").append(cSharpTypeName(typeToken.encoding().primitiveType())) + .append(" ").append(formatPropertyName(propertyName)).append("\n") + .append(indent).append("{\n") + .append(indent).append(INDENT).append("get { return ").append(literalValue).append("; }\n") + .append(indent).append("}\n"); + } + } + + private void generateVarData( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0, size = tokens.size(); i < size; i++) + { + final Token token = tokens.get(i); + if (token.signal() == Signal.BEGIN_VAR_DATA) + { + final String propertyName = token.name(); + final Token varDataToken = Generators.findFirst("varData", tokens, i); + final String characterEncoding = varDataToken.encoding().characterEncoding(); + final String dtoType = characterEncoding == null ? "byte[]" : "string"; + + sb.append("\n") + .append(indent).append("public ").append(dtoType).append(" ") + .append(formatPropertyName(propertyName)).append(" { get; set; }\n"); + } + } + } + + private String formatDtoClassName(final String name) + { + return formatClassName(name + "Dto"); + } + + private void generateDtosForTypes() throws IOException + { + for (final List tokens : ir.types()) + { + switch (tokens.get(0).signal()) + { + case BEGIN_COMPOSITE: + generateComposite(tokens); + break; + + default: + break; + } + } + } + + private void generateComposite(final List tokens) throws IOException + { + final String name = tokens.get(0).applicableTypeName(); + final String className = formatDtoClassName(name); + final String codecClassName = formatClassName(name); + + try (Writer out = outputManager.createOutput(className)) + { + out.append(generateFileHeader(ir.applicableNamespace())); + out.append(generateDocumentation(BASE_INDENT, tokens.get(0))); + + final StringBuilder sb = new StringBuilder(); + + final List compositeTokens = tokens.subList(1, tokens.size() - 1); + generateCompositePropertyElements(sb, compositeTokens, BASE_INDENT + INDENT); + generateCompositeDecodeFrom(sb, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateCompositeEncodeInto(sb, codecClassName, compositeTokens, BASE_INDENT + INDENT); + generateDisplay(sb, codecClassName, "Wrap", codecClassName + ".SbeSchemaVersion", BASE_INDENT + INDENT); + + out.append(BASE_INDENT).append("public sealed partial class ").append(className).append("\n") + .append(BASE_INDENT).append("{") + .append(sb) + .append(BASE_INDENT).append("}\n") + .append("}\n"); + } + } + + private void generateCompositePropertyElements( + final StringBuilder sb, + final List tokens, + final String indent) + { + for (int i = 0; i < tokens.size(); ) + { + final Token token = tokens.get(i); + final String propertyName = formatPropertyName(token.name()); + + switch (token.signal()) + { + case ENCODING: + generatePrimitiveProperty(sb, propertyName, token, token, indent); + break; + + case BEGIN_ENUM: + generateEnumProperty(sb, propertyName, token, token, indent); + break; + + case BEGIN_SET: + generateBitSetProperty(sb, propertyName, token, token, indent); + break; + + case BEGIN_COMPOSITE: + generateCompositeProperty(sb, propertyName, token, token, indent); + break; + + default: + break; + } + + i += tokens.get(i).componentTokenCount(); + } + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtos.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtos.java new file mode 100644 index 0000000000..79859d6764 --- /dev/null +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpDtos.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2023 Real Logic Limited. + * Copyright 2017 MarketFactory Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package uk.co.real_logic.sbe.generation.csharp; + +import uk.co.real_logic.sbe.generation.CodeGenerator; +import uk.co.real_logic.sbe.generation.TargetCodeGenerator; +import uk.co.real_logic.sbe.ir.Ir; + +/** + * {@link CodeGenerator} factory for CSharp DTOs. + */ +public class CSharpDtos implements TargetCodeGenerator +{ + /** + * {@inheritDoc} + */ + public CodeGenerator newInstance(final Ir ir, final String outputDir) + { + return new CSharpDtoGenerator(ir, new CSharpNamespaceOutputManager(outputDir, ir.applicableNamespace())); + } +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java index 5a607a8414..867c65fd71 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java @@ -654,40 +654,6 @@ private CharSequence generateEnumValues(final List tokens, final Token en return sb; } - private CharSequence generateFileHeader(final String packageName) - { - String[] tokens = packageName.split("\\."); - final StringBuilder sb = new StringBuilder(); - for (final String t : tokens) - { - sb.append(toUpperFirstChar(t)).append("."); - } - if (sb.length() > 0) - { - sb.setLength(sb.length() - 1); - } - - tokens = sb.toString().split("-"); - sb.setLength(0); - - for (final String t : tokens) - { - sb.append(toUpperFirstChar(t)); - } - - return String.format( - "// \n" + - "// Generated SBE (Simple Binary Encoding) message codec\n" + - "// \n\n" + - "#pragma warning disable 1591 // disable warning on missing comments\n" + - "using System;\n" + - "using System.Text;\n" + - "using Org.SbeTool.Sbe.Dll;\n\n" + - "namespace %s\n" + - "{\n", - sb); - } - private CharSequence generateClassDeclaration(final String className) { return String.format( @@ -696,20 +662,6 @@ private CharSequence generateClassDeclaration(final String className) className); } - private static String generateDocumentation(final String indent, final Token token) - { - final String description = token.description(); - if (null == description || description.isEmpty()) - { - return ""; - } - - return - indent + "/// \n" + - indent + "/// " + description + "\n" + - indent + "/// \n"; - } - private void generateMetaAttributeEnum() throws IOException { try (Writer out = outputManager.createOutput(META_ATTRIBUTE_ENUM)) @@ -1076,7 +1028,7 @@ private CharSequence generateConstPropertyMethods( final StringBuilder sb = new StringBuilder(); - final String javaTypeName = cSharpTypeName(token.encoding().primitiveType()); + final String csharpTypeName = cSharpTypeName(token.encoding().primitiveType()); final byte[] constantValue = token.encoding().constValue().byteArrayValue(token.encoding().primitiveType()); final CharSequence values = generateByteLiteralList( token.encoding().constValue().byteArrayValue(token.encoding().primitiveType())); @@ -1098,7 +1050,7 @@ private CharSequence generateConstPropertyMethods( indent + INDENT + "{\n" + indent + INDENT + INDENT + "return _%3$sValue[index];\n" + indent + INDENT + "}\n\n", - javaTypeName, + csharpTypeName, toUpperFirstChar(propertyName), propertyName)); @@ -1509,63 +1461,6 @@ private String generateByteOrder(final ByteOrder byteOrder, final int primitiveT return "LittleEndian"; } - private String generateLiteral(final PrimitiveType type, final String value) - { - String literal = ""; - - final String castType = cSharpTypeName(type); - switch (type) - { - case CHAR: - case UINT8: - case INT8: - case INT16: - case UINT16: - literal = "(" + castType + ")" + value; - break; - - case INT32: - literal = value; - break; - - case UINT32: - literal = value + "U"; - break; - - case FLOAT: - if (value.endsWith("NaN")) - { - literal = "float.NaN"; - } - else - { - literal = value + "f"; - } - break; - - case UINT64: - literal = "0x" + Long.toHexString(Long.parseLong(value)) + "UL"; - break; - - case INT64: - literal = value + "L"; - break; - - case DOUBLE: - if (value.endsWith("NaN")) - { - literal = "double.NaN"; - } - else - { - literal = value + "d"; - } - break; - } - - return literal; - } - private void appendGroupInstanceDisplay( final StringBuilder sb, final List fields, diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java index 1140ae8b02..8934285685 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java @@ -17,6 +17,8 @@ package uk.co.real_logic.sbe.generation.csharp; import uk.co.real_logic.sbe.PrimitiveType; +import uk.co.real_logic.sbe.generation.Generators; +import uk.co.real_logic.sbe.ir.Token; import java.util.EnumMap; import java.util.Map; @@ -26,6 +28,118 @@ */ public class CSharpUtil { + static String generateLiteral(final PrimitiveType type, final String value) + { + String literal = ""; + + final String castType = cSharpTypeName(type); + switch (type) + { + case CHAR: + case UINT8: + case INT8: + case INT16: + case UINT16: + literal = "(" + castType + ")" + value; + break; + + case INT32: + literal = value; + break; + + case UINT32: + literal = value + "U"; + break; + + case FLOAT: + if (value.endsWith("NaN")) + { + literal = "float.NaN"; + } + else + { + literal = value + "f"; + } + break; + + case UINT64: + literal = "0x" + Long.toHexString(Long.parseLong(value)) + "UL"; + break; + + case INT64: + literal = value + "L"; + break; + + case DOUBLE: + if (value.endsWith("NaN")) + { + literal = "double.NaN"; + } + else + { + literal = value + "d"; + } + break; + } + + return literal; + } + + static CharSequence generateFileHeader(final String packageName, final String... imports) + { + return String.format( + "// \n" + + "// Generated SBE (Simple Binary Encoding) message codec\n" + + "// \n\n" + + "#pragma warning disable 1591 // disable warning on missing comments\n" + + "using System;\n" + + "using System.Text;\n" + + "%1$s" + + "using Org.SbeTool.Sbe.Dll;\n\n" + + "namespace %2$s\n" + + "{\n", + String.join("", imports), + formatNamespace(packageName)); + } + + static String formatNamespace(final String packageName) + { + String[] tokens = packageName.split("\\."); + final StringBuilder sb = new StringBuilder(); + for (final String t : tokens) + { + sb.append(Generators.toUpperFirstChar(t)).append("."); + } + if (sb.length() > 0) + { + sb.setLength(sb.length() - 1); + } + + tokens = sb.toString().split("-"); + sb.setLength(0); + + for (final String t : tokens) + { + sb.append(Generators.toUpperFirstChar(t)); + } + + return sb.toString(); + } + + static String generateDocumentation(final String indent, final Token token) + { + final String description = token.description(); + if (null == description || description.isEmpty()) + { + return ""; + } + + return + indent + "/// \n" + + indent + "/// " + description + "\n" + + indent + "/// \n"; + } + enum Separators { BEGIN_GROUP('['), @@ -89,7 +203,7 @@ public String toString() * @param primitiveType to map. * @return the name of the Java primitive that most closely maps. */ - public static String cSharpTypeName(final PrimitiveType primitiveType) + static String cSharpTypeName(final PrimitiveType primitiveType) { return PRIMITIVE_TYPE_STRING_ENUM_MAP.get(primitiveType); }