Skip to content

Commit 27160d2

Browse files
committed
fix: support signed-byte arrays across compiler hosts
1 parent ea7a1b0 commit 27160d2

8 files changed

Lines changed: 121 additions & 2 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ jobs:
8888
node tools/quality/generate-parity-layouts.mjs --check
8989
dotnet test ./tests/CStructSharp.Generated.Parity/CStructSharp.Generated.Parity.csproj -c Release -f net8.0 --no-build --logger trx --results-directory ./artifacts/test-results/parity
9090
91+
- name: Test newer compiler compatibility
92+
run: dotnet test tests/CStructSharp.Generators.Modern.Tests/CStructSharp.Generators.Modern.Tests.csproj -c Release --no-build --logger trx --results-directory artifacts/test-results/compiler-compatibility
93+
9194
- name: Generate README quality badges
9295
run: >-
9396
node tools/quality/readme-badges.mjs

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Related changes are consolidated; routine formatting and benchmark bookkeeping a
2626

2727
### Fixed
2828

29+
- Generated signed-byte array readers compile under C# 14 by explicitly selecting a writable span, while
30+
retaining Roslyn 4.8 compatibility. Shared regression tests cover Roslyn 4.8/C# 12 and Roslyn 5.9/C# 12 and 14
31+
in CI and release verification.
2932
- Allow an array element type with size zero when the array's own declared size is also zero, instead of
3033
rejecting every zero-size element outright. Kernel BTF genuinely declares empty marker structs (for example
3134
Linux's `lock_class_key`, used only for its address, never its contents) and arrays of them; the size-consistency

‎CStructSharp.NonWeb.sln‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CStructSharp.Generators.Tes
2222
EndProject
2323
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CStructSharp.Generated.Parity", "tests\CStructSharp.Generated.Parity\CStructSharp.Generated.Parity.csproj", "{B7BCCE6C-7136-4363-BB05-DBEF34548FD4}"
2424
EndProject
25+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CStructSharp.Generators.Modern.Tests", "tests\CStructSharp.Generators.Modern.Tests\CStructSharp.Generators.Modern.Tests.csproj", "{C398CF15-81BC-4FDD-82B2-E340CC503708}"
26+
EndProject
2527
Global
2628
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2729
Debug|Any CPU = Debug|Any CPU
@@ -32,6 +34,18 @@ Global
3234
Release|x86 = Release|x86
3335
EndGlobalSection
3436
GlobalSection(ProjectConfigurationPlatforms) = postSolution
37+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|Any CPU.Build.0 = Debug|Any CPU
39+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x64.ActiveCfg = Debug|Any CPU
40+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x64.Build.0 = Debug|Any CPU
41+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x86.ActiveCfg = Debug|Any CPU
42+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x86.Build.0 = Debug|Any CPU
43+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|Any CPU.ActiveCfg = Release|Any CPU
44+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|Any CPU.Build.0 = Release|Any CPU
45+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x64.ActiveCfg = Release|Any CPU
46+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x64.Build.0 = Release|Any CPU
47+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x86.ActiveCfg = Release|Any CPU
48+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x86.Build.0 = Release|Any CPU
3549
{EF1FDDFF-5608-4063-BA6D-830ADEB16FD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3650
{EF1FDDFF-5608-4063-BA6D-830ADEB16FD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
3751
{EF1FDDFF-5608-4063-BA6D-830ADEB16FD1}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -145,6 +159,7 @@ Global
145159
HideSolutionNode = FALSE
146160
EndGlobalSection
147161
GlobalSection(NestedProjects) = preSolution
162+
{C398CF15-81BC-4FDD-82B2-E340CC503708} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
148163
{B4B2F2C7-32F7-4E66-AE42-B0D66D576CD7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
149164
{884D1B8A-9107-4357-9203-259CF7013F3F} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
150165
{B7BCCE6C-7136-4363-BB05-DBEF34548FD4} = {0AB3BF05-4346-4AA6-1389-037BE0695223}

‎CStructSharp.sln‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CStructSharp.Generators.Tes
2424
EndProject
2525
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CStructSharp.Generated.Parity", "tests\CStructSharp.Generated.Parity\CStructSharp.Generated.Parity.csproj", "{1D145FEA-0561-413F-A315-69EAD06EC855}"
2626
EndProject
27+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CStructSharp.Generators.Modern.Tests", "tests\CStructSharp.Generators.Modern.Tests\CStructSharp.Generators.Modern.Tests.csproj", "{C398CF15-81BC-4FDD-82B2-E340CC503708}"
28+
EndProject
2729
Global
2830
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2931
Debug|Any CPU = Debug|Any CPU
@@ -34,6 +36,18 @@ Global
3436
Release|x86 = Release|x86
3537
EndGlobalSection
3638
GlobalSection(ProjectConfigurationPlatforms) = postSolution
39+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|Any CPU.Build.0 = Debug|Any CPU
41+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x64.ActiveCfg = Debug|Any CPU
42+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x64.Build.0 = Debug|Any CPU
43+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x86.ActiveCfg = Debug|Any CPU
44+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Debug|x86.Build.0 = Debug|Any CPU
45+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|Any CPU.ActiveCfg = Release|Any CPU
46+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|Any CPU.Build.0 = Release|Any CPU
47+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x64.ActiveCfg = Release|Any CPU
48+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x64.Build.0 = Release|Any CPU
49+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x86.ActiveCfg = Release|Any CPU
50+
{C398CF15-81BC-4FDD-82B2-E340CC503708}.Release|x86.Build.0 = Release|Any CPU
3751
{EF1FDDFF-5608-4063-BA6D-830ADEB16FD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3852
{EF1FDDFF-5608-4063-BA6D-830ADEB16FD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
3953
{EF1FDDFF-5608-4063-BA6D-830ADEB16FD1}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -159,6 +173,7 @@ Global
159173
HideSolutionNode = FALSE
160174
EndGlobalSection
161175
GlobalSection(NestedProjects) = preSolution
176+
{C398CF15-81BC-4FDD-82B2-E340CC503708} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
162177
{F7AC2394-1939-4DC0-9A82-581C07069ACC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
163178
{248C237D-69DA-47B7-A4FD-0F1573571CA5} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
164179
{1D145FEA-0561-413F-A315-69EAD06EC855} = {0AB3BF05-4346-4AA6-1389-037BE0695223}

‎docs/project/testing.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ compatibility tests on both frameworks.
3232

3333
## Generator snapshots and parity
3434

35-
The source generator has two test projects of its own:
35+
The source generator has three test projects of its own:
3636

3737
```sh
3838
dotnet test tests/CStructSharp.Generators.Tests/CStructSharp.Generators.Tests.csproj -c Release
3939
dotnet test tests/CStructSharp.Generated.Parity/CStructSharp.Generated.Parity.csproj -c Release
40+
dotnet test tests/CStructSharp.Generators.Modern.Tests/CStructSharp.Generators.Modern.Tests.csproj -c Release
4041
```
4142

4243
The first runs the generator in memory over small sources: the generated file for each fixture is compared byte
@@ -51,6 +52,13 @@ UPDATE_SNAPSHOTS=1 dotnet test tests/CStructSharp.Generators.Tests/CStructSharp.
5152

5253
A snapshot is never rewritten to make a failing test pass; the diff is the review.
5354

55+
The first project hosts the generator under Roslyn 4.8 with C# 12 consumers. The third runs shared compiler
56+
compatibility tests under Roslyn 5.9 with both C# 12 and C# 14 consumers. These tests compile generated readers
57+
and writers, check signed-byte values, and verify exact round trips for fixed and counted arrays, including an
58+
empty counted array. The generator itself still references Roslyn 4.8: a newer test host does not raise its
59+
minimum compiler requirement. Both host checks run in CI and release verification. Compiler-host coverage is
60+
separate from testing the runtime library on .NET 8 and .NET 10.
61+
5462
The second project generates every layout fixture the runtime is tested with into one assembly. Its
5563
`Layouts.g.cs` and `layouts.json` come from the fixture sources; regenerate and check them with:
5664

‎src/CStructSharp.Generators/LayoutEmitter.Arrays.cs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,19 @@ private static string ElementType(string arrayType, int dimensions)
156156
return element;
157157
}
158158

159+
/// <summary>Builds a bulk array decoder using a writable destination on both older and newer compiler hosts.</summary>
160+
/// <param name="codec">The element codec, including its byte order.</param>
161+
/// <param name="elementType">The generated C# element type.</param>
162+
/// <returns>A statement that decodes bytes into the existing elements array.</returns>
159163
private static string BulkDecode(PrimitiveCodec codec, string elementType)
160164
{
161165
string le = Bool(codec.LittleEndian);
162166
return codec.Kind switch
163167
{
164168
PrimitiveCodecKind.UInt8 => "bytes.CopyTo(elements);",
165-
PrimitiveCodecKind.Int8 => "bytes.CopyTo(global::System.Runtime.InteropServices.MemoryMarshal.AsBytes<sbyte>(elements));",
169+
170+
// An explicit writable span avoids C# 14 preferring the ReadOnlySpan overload for an array.
171+
PrimitiveCodecKind.Int8 => "bytes.CopyTo(global::System.Runtime.InteropServices.MemoryMarshal.AsBytes<sbyte>(new global::System.Span<sbyte>(elements)));",
166172
PrimitiveCodecKind.Bool => CodecClass + ".DecodeBooleans(bytes, elements);",
167173
PrimitiveCodecKind.Int24 => CodecClass + ".DecodeInt24(bytes, elements, " + le + ");",
168174
PrimitiveCodecKind.UInt24 => CodecClass + ".DecodeUInt24(bytes, elements, " + le + ");",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<LangVersion>latest</LangVersion>
5+
<IsPackable>false</IsPackable>
6+
<RootNamespace>CStructSharp.Generators.Tests</RootNamespace>
7+
<DefineConstants>$(DefineConstants);MODERN_ROSLYN</DefineConstants>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
11+
<PackageReference Include="MSTest" Version="4.4.1" />
12+
<!-- Only the test host uses newer Roslyn; the shipped generator retains its 4.8 API floor. -->
13+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
14+
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
15+
<ProjectReference Include="../../src/CStructSharp.Generators/CStructSharp.Generators.csproj" Aliases="Generators" />
16+
<ProjectReference Include="../../src/CStructSharp/CStructSharp.csproj" />
17+
<Compile Include="../CStructSharp.Generators.Tests/GeneratorRunner.cs" Link="GeneratorRunner.cs" />
18+
<Compile Include="../CStructSharp.Generators.Tests/GeneratorResult.cs" Link="GeneratorResult.cs" />
19+
<Compile Include="../CStructSharp.Generators.Tests/CompilerCompatibilityTests.cs" Link="CompilerCompatibilityTests.cs" />
20+
</ItemGroup>
21+
</Project>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
namespace CStructSharp.Generators.Tests;
2+
3+
using System;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
6+
/// <summary>Checks that signed-byte arrays compile and retain their values across supported compiler hosts.</summary>
7+
[TestClass]
8+
public class CompilerCompatibilityTests
9+
{
10+
/// <summary>Compiles fixed and counted arrays, then checks signed values and exact serialized bytes.</summary>
11+
/// <param name="languageVersion">The consumer language version, independent of the generator's build language.</param>
12+
[TestMethod]
13+
[DataRow(LanguageVersion.CSharp12)]
14+
#if MODERN_ROSLYN
15+
[DataRow(LanguageVersion.CSharp14)]
16+
#endif
17+
public void SignedByteArrays_CompileAndRoundTrip(LanguageVersion languageVersion)
18+
{
19+
const string Source = """
20+
using CStructSharp;
21+
namespace Compatibility;
22+
[CStructLayout("struct root { int8 fixedValues[4]; uint8 count; int8 countedValues[count]; };", Root = "root")]
23+
public static partial class Packet
24+
{
25+
/// <summary>Checks decoding and returns the encoded bytes for an exact round trip.</summary>
26+
public static byte[] Probe(byte[] bytes)
27+
{
28+
var value = Parse(bytes);
29+
if (value.FixedValues[0] != -128 || value.FixedValues[1] != -1 ||
30+
value.FixedValues[2] != 0 || value.FixedValues[3] != 127 ||
31+
value.CountedValues.Length != value.Count)
32+
throw new System.InvalidOperationException("Incorrect fixed array or count.");
33+
for (int i = 0; i < value.CountedValues.Length; i++)
34+
if (value.CountedValues[i] != value.FixedValues[i])
35+
throw new System.InvalidOperationException("Incorrect counted array.");
36+
return Serialize(value);
37+
}
38+
}
39+
""";
40+
Type packet = GeneratorRunner.Run(Source, languageVersion: languageVersion).AssertClean().Load().GetType("Compatibility.Packet")!;
41+
byte[][] inputs = [[128, 255, 0, 127, 4, 128, 255, 0, 127], [128, 255, 0, 127, 0]];
42+
foreach (byte[] input in inputs)
43+
{
44+
byte[] output = (byte[])packet.GetMethod("Probe")!.Invoke(null, [input])!;
45+
CollectionAssert.AreEqual(input, output);
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)