From 844c16bb8ab4d2d4fd578fb0b01a53f904a3de20 Mon Sep 17 00:00:00 2001
From: glomdom <103685817+glomdom@users.noreply.github.com>
Date: Thu, 28 May 2026 09:35:30 +0300
Subject: [PATCH 1/2] feat: create `SerializeWithAttribute` stub
---
.../Embedded/SerializeWithAttribute.cs | 31 +++++++++++++++++++
1 file changed, 31 insertions(+)
create mode 100644 BinaryWizard/Embedded/SerializeWithAttribute.cs
diff --git a/BinaryWizard/Embedded/SerializeWithAttribute.cs b/BinaryWizard/Embedded/SerializeWithAttribute.cs
new file mode 100644
index 0000000..f884ed7
--- /dev/null
+++ b/BinaryWizard/Embedded/SerializeWithAttribute.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2026 glomdom
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+using System;
+
+namespace BinaryWizard;
+
+[AttributeUsage(AttributeTargets.Field)]
+public sealed class SerializeWithAttribute : Attribute {
+ ///
+ /// The type of the function to use when serializing the attached field.
+ ///
+ public Type Function { get; set; }
+
+ public SerializeWithAttribute(Type function) {
+ Function = function;
+ }
+}
\ No newline at end of file
From 6496af051212dbc3213da2c5ca80120650fe501b Mon Sep 17 00:00:00 2001
From: glomdom <103685817+glomdom@users.noreply.github.com>
Date: Thu, 28 May 2026 22:08:17 +0300
Subject: [PATCH 2/2] feat: serialize `string` by default with null terminated
reader (closes: #3)
---
BinaryWizard.Tests/BinaryWriterExtensions.cs | 19 ++++++++++++
BinaryWizard.Tests/Samples/Entity.cs | 2 +-
BinaryWizard.Tests/SerializerTests.cs | 5 +--
BinaryWizard/Analysis/SymbolExtensions.cs | 8 +++--
BinaryWizard/Emission/CodeBuilder.cs | 31 +++++++++++++++++--
BinaryWizard/Models/TypeModel.cs | 1 +
.../Segmenting/DynamicStringSegment.cs | 28 +++++++++++++++++
BinaryWizard/Segmenting/SegmentManager.cs | 16 +++++++---
8 files changed, 98 insertions(+), 12 deletions(-)
create mode 100644 BinaryWizard.Tests/BinaryWriterExtensions.cs
create mode 100644 BinaryWizard/Segmenting/DynamicStringSegment.cs
diff --git a/BinaryWizard.Tests/BinaryWriterExtensions.cs b/BinaryWizard.Tests/BinaryWriterExtensions.cs
new file mode 100644
index 0000000..f37bc32
--- /dev/null
+++ b/BinaryWizard.Tests/BinaryWriterExtensions.cs
@@ -0,0 +1,19 @@
+using System.Text;
+
+namespace BinaryWizard.Tests;
+
+public static class BinaryWriterExtensions {
+ public static void WriteNullTerminatedString(this BinaryWriter writer, string text, Encoding? encoding = null) {
+ encoding ??= Encoding.UTF8;
+
+ if (string.IsNullOrEmpty(text)) {
+ writer.Write("\0"u8);
+
+ return;
+ }
+
+ var textBytes = encoding.GetBytes(text);
+ writer.Write(textBytes);
+ writer.Write(encoding is UnicodeEncoding ? "\0\0"u8 : "\0"u8);
+ }
+}
\ No newline at end of file
diff --git a/BinaryWizard.Tests/Samples/Entity.cs b/BinaryWizard.Tests/Samples/Entity.cs
index ad2375b..e4c0766 100644
--- a/BinaryWizard.Tests/Samples/Entity.cs
+++ b/BinaryWizard.Tests/Samples/Entity.cs
@@ -19,6 +19,6 @@ namespace BinaryWizard.Tests.Samples;
[BinarySerializable]
public partial struct Entity {
public int Id;
- // public string Name;
+ public string Name;
public Vector3 Position;
}
\ No newline at end of file
diff --git a/BinaryWizard.Tests/SerializerTests.cs b/BinaryWizard.Tests/SerializerTests.cs
index b6c95ba..abacd53 100644
--- a/BinaryWizard.Tests/SerializerTests.cs
+++ b/BinaryWizard.Tests/SerializerTests.cs
@@ -74,7 +74,7 @@ public void Entity_CorrectlySerialized() {
using var stream = new MemoryStream();
using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) {
writer.Write(69); // Id
- // writer.Write("glomdom"); // Name
+ writer.WriteNullTerminatedString("glomdom"); // Name
// Position: Vector3
writer.Write(1); // X
@@ -88,7 +88,8 @@ public void Entity_CorrectlySerialized() {
var actual = Entity.FromBinary(reader);
var expected = new Entity {
- Id = 69, /* Name = "glomdom", */
+ Id = 69,
+ Name = "glomdom",
Position = new Vector3 {
X = 1, Y = 2, Z = 3,
},
diff --git a/BinaryWizard/Analysis/SymbolExtensions.cs b/BinaryWizard/Analysis/SymbolExtensions.cs
index b8c6006..83741ea 100644
--- a/BinaryWizard/Analysis/SymbolExtensions.cs
+++ b/BinaryWizard/Analysis/SymbolExtensions.cs
@@ -44,7 +44,7 @@ internal static string GetBinaryPrimitiveReader(this ITypeSymbol sym, Endianness
internal static int GetByteSize(this ITypeSymbol primitive) {
return primitive.SpecialType switch {
SpecialType.System_Boolean => 1,
- SpecialType.System_Char => 1,
+ SpecialType.System_Char => 2,
SpecialType.System_SByte => 1,
SpecialType.System_Byte => 1,
SpecialType.System_Int16 => 2,
@@ -53,10 +53,12 @@ internal static int GetByteSize(this ITypeSymbol primitive) {
SpecialType.System_UInt32 => 4,
SpecialType.System_Int64 => 8,
SpecialType.System_UInt64 => 8,
+ SpecialType.System_Single => 4,
+ SpecialType.System_Double => 8,
SpecialType.System_Decimal => 16,
- SpecialType.System_Double => 32,
+ SpecialType.System_String => -1,
- _ => throw new InvalidOperationException("Unexpected case encountered."),
+ _ => throw new InvalidOperationException($"Unexpected case encountered for SpecialType: {primitive.SpecialType}"),
};
}
diff --git a/BinaryWizard/Emission/CodeBuilder.cs b/BinaryWizard/Emission/CodeBuilder.cs
index b30263a..2ff0f87 100644
--- a/BinaryWizard/Emission/CodeBuilder.cs
+++ b/BinaryWizard/Emission/CodeBuilder.cs
@@ -51,7 +51,22 @@ internal static void Generate(SourceProductionContext spc, ClassSerializationMet
switch (seg) {
case FixedSegment fixedSeg: ProcessFixedSegment(++segmentCount, fixedSeg, bodyBuilder, bodyIndent, meta); break;
case DynamicSegment dynSeg: ProcessDynamicSegment(dynSeg, bodyBuilder, bodyIndent, meta); break;
- case NestedObjectSegment nestedSeg: bodyBuilder.AppendLine($"{bodyIndent}result.{nestedSeg.FieldName} = {nestedSeg.TypeName}.FromBinary(reader);"); break;
+ case NestedObjectSegment nestedSeg:
+ bodyBuilder.AppendLine($"{bodyIndent}result.{nestedSeg.FieldName} = {nestedSeg.TypeName}.FromBinary(reader);"); break;
+ case DynamicStringSegment stringSeg:
+ bodyBuilder.AppendLine($$"""
+ {{bodyIndent}}{
+ {{bodyIndent}} var stringBytes = new System.Collections.Generic.List();
+ {{bodyIndent}} while (true) {
+ {{bodyIndent}} byte b = reader.ReadByte();
+ {{bodyIndent}} if (b == 0) break;
+ {{bodyIndent}} stringBytes.Add(b);
+ {{bodyIndent}} }
+ {{bodyIndent}} result.{{stringSeg.Field.Name}} = System.Text.Encoding.UTF8.GetString(stringBytes.ToArray());
+ {{bodyIndent}}}
+ """);
+
+ break;
}
}
@@ -117,7 +132,19 @@ private static void ProcessFixedSegment(int segmentIndex, FixedSegment seg, Stri
continue;
}
- sb.AppendLine($"{indent}result.{field.Name} = {field.TypeModel.Type.GetBinaryPrimitiveReader(meta.Endianness)}({bufName}.Slice({localOffset}, {field.ByteSize}));");
+ if (field.TypeModel.IsString) {
+ sb.AppendLine($$"""
+ {{indent}}{
+ {{indent}} var textSpan = {{bufName}}.Slice({{localOffset}}, {{field.ByteSize}});
+ {{indent}} var nullIdx = textSpan.IndexOf((byte)0);
+ {{indent}} result.{{field.Name}} = System.Text.Encoding.UTF8.GetString(nullIdx >= 0 ? textSpan.Slice(0, nullIdx) : textSpan);
+ {{indent}}}
+ """);
+ } else {
+ sb.AppendLine(
+ $"{indent}result.{field.Name} = {field.TypeModel.Type.GetBinaryPrimitiveReader(meta.Endianness)}({bufName}.Slice({localOffset}, {field.ByteSize}));"
+ );
+ }
if (field.HasMagic) {
var magicStr = field.Magic;
diff --git a/BinaryWizard/Models/TypeModel.cs b/BinaryWizard/Models/TypeModel.cs
index cc4db7b..015f23f 100644
--- a/BinaryWizard/Models/TypeModel.cs
+++ b/BinaryWizard/Models/TypeModel.cs
@@ -26,6 +26,7 @@ internal sealed record TypeModel {
internal int? InnerTypeByteSize { get; set; }
internal bool IsFixedArray => FixedArraySize is not null && InnerType is not null;
internal bool IsDynamicArray => FixedArraySize is null && InnerType is not null;
+ internal bool IsString => Type.SpecialType == SpecialType.System_String;
internal TypeModel(ITypeSymbol type, int? fixedArraySize = null) {
Type = type;
diff --git a/BinaryWizard/Segmenting/DynamicStringSegment.cs b/BinaryWizard/Segmenting/DynamicStringSegment.cs
new file mode 100644
index 0000000..a1a2edf
--- /dev/null
+++ b/BinaryWizard/Segmenting/DynamicStringSegment.cs
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2026 glomdom
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+using System.Collections.Generic;
+using BinaryWizard.Models;
+
+namespace BinaryWizard.Segmenting;
+
+internal record DynamicStringSegment : Segment {
+ internal FieldDef Field { get; set; }
+
+ internal DynamicStringSegment(FieldDef field) {
+ Field = field;
+ }
+}
\ No newline at end of file
diff --git a/BinaryWizard/Segmenting/SegmentManager.cs b/BinaryWizard/Segmenting/SegmentManager.cs
index f209608..7507d48 100644
--- a/BinaryWizard/Segmenting/SegmentManager.cs
+++ b/BinaryWizard/Segmenting/SegmentManager.cs
@@ -29,13 +29,21 @@ internal sealed class SegmentManager {
internal void AddField(FieldDef field, string? lengthRef = null) {
if (field.IsDynamic) {
- if (string.IsNullOrEmpty(lengthRef)) throw new ArgumentNullException(nameof(lengthRef), "Length reference was not provided when field is dynamic");
-
CommitFixed();
- _segments.Add(new DynamicSegment([field], lengthRef!));
+ if (field.TypeModel.IsString) {
+ _segments.Add(new DynamicStringSegment(field));
+
+ Debug.WriteLine($"Added dynamic string segment for {field.Name}");
+ } else {
+ if (string.IsNullOrEmpty(lengthRef)) {
+ throw new ArgumentNullException(nameof(lengthRef), $"Length reference was not provided for dynamic array field '{field.Name}'");
+ }
+
+ _segments.Add(new DynamicSegment([field], lengthRef!));
- Debug.WriteLine($"Added dynamic segment for {field.Name} (dep. {lengthRef})");
+ Debug.WriteLine($"Added dynamic segment for {field.Name} (dep. {lengthRef})");
+ }
} else {
_currentFields.Add(field);