From 4484b897ed87456e728c381edd91e868a99a104b Mon Sep 17 00:00:00 2001 From: Jonathan Chambers Date: Thu, 29 Sep 2022 11:23:30 -0400 Subject: [PATCH 1/2] Add support for Unity NativeArray vector type. --- src/FlatSharp.Compiler/CompilerOptions.cs | 3 + src/FlatSharp.Compiler/FlatSharpCompiler.cs | 28 ++- .../SchemaModel/PropertyFieldModel.cs | 1 + src/FlatSharp.Compiler/VectorType.cs | 1 + .../IO/InputBufferExtensions.cs | 18 ++ .../IO/SpanWriterExtensions.cs | 23 +++ .../FlatSharp.UnityPolyfills.csproj | 25 +++ src/FlatSharp.UnityPolyfills/NativeArray.cs | 166 ++++++++++++++++++ src/FlatSharp.sln | 6 + src/FlatSharp/FlatBufferSerializer.cs | 3 + .../TypeModel/TypeModelContainerExtensions.cs | 27 +++ .../TypeModel/UnityTypeModelProvider.cs | 38 ++++ .../UnityNativeArrayVectorTypeModel.cs | 101 +++++++++++ .../CompilerTestHelpers.cs | 4 +- .../CopyConstructorTests.cs | 15 +- .../FlatSharpCompilerTests.csproj | 1 + src/Tests/FlatSharpCompilerTests/FullTests.cs | 5 +- .../InvalidAttributeTests.cs | 2 +- .../FlatSharpPoolableEndToEndTests.csproj | 1 + .../FlatSharpTests/ClassLib/TypeModelTests.cs | 43 +++++ .../FlatSharpTests/FlatSharpTests.csproj | 1 + .../VectorDeserializationTests.cs | 136 +++++++++++++- .../VectorSerializationTests.cs | 19 +- 23 files changed, 650 insertions(+), 17 deletions(-) create mode 100644 src/FlatSharp.UnityPolyfills/FlatSharp.UnityPolyfills.csproj create mode 100644 src/FlatSharp.UnityPolyfills/NativeArray.cs create mode 100644 src/FlatSharp/TypeModel/TypeModelContainerExtensions.cs create mode 100644 src/FlatSharp/TypeModel/UnityTypeModelProvider.cs create mode 100644 src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs diff --git a/src/FlatSharp.Compiler/CompilerOptions.cs b/src/FlatSharp.Compiler/CompilerOptions.cs index 82ddbe0d..afe0c186 100644 --- a/src/FlatSharp.Compiler/CompilerOptions.cs +++ b/src/FlatSharp.Compiler/CompilerOptions.cs @@ -43,4 +43,7 @@ public record CompilerOptions [Option("debug", Hidden = true, Default = false)] public bool Debug { get; set; } + + [Option("unity-assembly-path", HelpText = "Path to assembly (e.g. UnityEngine.dll) which enables Unity support.")] + public string? UnityAssemblyPath { get; set; } } diff --git a/src/FlatSharp.Compiler/FlatSharpCompiler.cs b/src/FlatSharp.Compiler/FlatSharpCompiler.cs index 92aca1be..5f2e8f9f 100644 --- a/src/FlatSharp.Compiler/FlatSharpCompiler.cs +++ b/src/FlatSharp.Compiler/FlatSharpCompiler.cs @@ -75,6 +75,12 @@ private static int RunCompiler(CompilerOptions options) Console.Error.WriteLine("FlatSharp compiler: No output directory specified."); return -1; } + + if (!string.IsNullOrEmpty(options.UnityAssemblyPath) && !File.Exists(options.UnityAssemblyPath)) + { + Console.Error.WriteLine("FlatSharp compiler: Unity assembly path specified does not exist."); + return -1; + } // Read existing file to see if we even need to do any work. const string outputFileName = "FlatSharp.generated.cs"; @@ -315,7 +321,7 @@ private static (Assembly, string) CompileAndLoadAssemblyWithCode( { try { - Assembly[] additionalRefs = additionalReferences?.ToArray() ?? Array.Empty(); + Assembly[] additionalRefs = BuildAdditionalReferences(options, additionalReferences); options.InputFiles = fbsFiles.Select(x => x.FullName); @@ -527,6 +533,8 @@ private static void CreateCSharp( } ErrorContext.Current.ThrowIfHasErrors(); + + Assembly[] additionalRefs = BuildAdditionalReferences(options); foreach (var step in steps) { @@ -539,7 +547,7 @@ private static void CreateCSharp( if (step > CodeWritingPass.Initialization) { csharp = writer.ToString(); - (assembly, _, _) = RoslynSerializerGenerator.CompileAssembly(csharp, true); + (assembly, _, _) = RoslynSerializerGenerator.CompileAssembly(csharp, true, additionalRefs); } writer = new CodeWriter(); @@ -552,7 +560,7 @@ private static void CreateCSharp( Options = localOptions, InputHash = inputHash, PreviousAssembly = assembly, - TypeModelContainer = TypeModelContainer.CreateDefault(), + TypeModelContainer = TypeModelContainer.CreateDefault().WithUnitySupport(localOptions.UnityAssemblyPath is not null), }); ErrorContext.Current.ThrowIfHasErrors(); @@ -580,7 +588,8 @@ private static Schema.Schema ParseSchema( List> postProcessTransforms, params ISchemaMutator[] mutators) { - ISerializer mutableSerializer = FlatBufferSerializer.Default + ISerializer mutableSerializer = new FlatBufferSerializer( + new FlatBufferSerializerOptions(), TypeModelContainer.CreateDefault().WithUnitySupport(options.UnityAssemblyPath is not null)) .Compile() .WithSettings(s => s.UseGreedyMutableDeserialization()); @@ -599,4 +608,15 @@ private static Schema.Schema ParseSchema( // Immutable. return mutableSerializer.WithSettings(s => s.UseGreedyDeserialization()).Parse(temp); } + + private static Assembly[] BuildAdditionalReferences(CompilerOptions options, IEnumerable? additionalReferences = null) + { + var references = new List(); + if (additionalReferences is not null) + references.AddRange(additionalReferences); + if (options.UnityAssemblyPath is not null) + references.Add(Assembly.LoadFrom(options.UnityAssemblyPath)); + + return references.ToArray(); + } } diff --git a/src/FlatSharp.Compiler/SchemaModel/PropertyFieldModel.cs b/src/FlatSharp.Compiler/SchemaModel/PropertyFieldModel.cs index 8975a4cd..6574cae4 100644 --- a/src/FlatSharp.Compiler/SchemaModel/PropertyFieldModel.cs +++ b/src/FlatSharp.Compiler/SchemaModel/PropertyFieldModel.cs @@ -320,6 +320,7 @@ private string GetVectorTypeName(string innerType, string? keyType) VectorType.Memory => $"Memory<{innerType}>", VectorType.ReadOnlyMemory => $"ReadOnlyMemory<{innerType}>", VectorType.IIndexedVector => $"IIndexedVector<{keyType ?? "string"}, {innerType}>", + VectorType.UnityNativeArray => $"Unity.Collections.NativeArray<{innerType}>", // Unqualified ubyte vectors default to Memory. null => this.Field.Type.ElementType == BaseType.UByte diff --git a/src/FlatSharp.Compiler/VectorType.cs b/src/FlatSharp.Compiler/VectorType.cs index 52e28fd1..cee3bb26 100644 --- a/src/FlatSharp.Compiler/VectorType.cs +++ b/src/FlatSharp.Compiler/VectorType.cs @@ -26,4 +26,5 @@ public enum VectorType Memory, ReadOnlyMemory, IIndexedVector, + UnityNativeArray } diff --git a/src/FlatSharp.Runtime/IO/InputBufferExtensions.cs b/src/FlatSharp.Runtime/IO/InputBufferExtensions.cs index cd6bdfa3..bac4bd3d 100644 --- a/src/FlatSharp.Runtime/IO/InputBufferExtensions.cs +++ b/src/FlatSharp.Runtime/IO/InputBufferExtensions.cs @@ -15,6 +15,7 @@ */ using System.IO; +using System.Runtime.InteropServices; namespace FlatSharp.Internal; @@ -136,6 +137,23 @@ public static ReadOnlyMemory ReadByteReadOnlyMemoryBlock(this TBu return buffer.GetReadOnlyMemory().Slice(uoffset + sizeof(uint), (int)buffer.ReadUInt(uoffset)); } } + + public static Span UnsafeReadSpan(this TBuffer buffer, int uoffset) where TBuffer : IInputBuffer where TElement : struct + { + checked + { + // The local value stores a uoffset_t, so follow that now. + uoffset = uoffset + buffer.ReadUOffset(uoffset); + + // We need to construct a Span from byte buffer that: + // 1. starts at correct offset for vector data + // 2. has a length based on *TElement* count not *byte* count + var byteSpanAtDataOffset = buffer.GetSpan().Slice(uoffset + sizeof(uint)); + var sourceSpan = MemoryMarshal.Cast(byteSpanAtDataOffset).Slice(0, (int)buffer.ReadUInt(uoffset)); + + return sourceSpan; + } + } [ExcludeFromCodeCoverage] // Not currently used. [Conditional("DEBUG")] diff --git a/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs b/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs index 91eb343d..c40a00e0 100644 --- a/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs +++ b/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs @@ -14,6 +14,8 @@ * limitations under the License. */ +using System.Runtime.InteropServices; + namespace FlatSharp.Internal; /// @@ -36,6 +38,27 @@ public static void WriteReadOnlyByteMemoryBlock( memory.Span.CopyTo(span.Slice(vectorStartOffset + sizeof(uint))); } + + public static void UnsafeWriteSpan( + this TSpanWriter spanWriter, + Span span, + Span buffer, + int offset, + SerializationContext ctx) where TSpanWriter : ISpanWriter where TElement : struct + { + int numberOfItems = buffer.Length; + int vectorStartOffset = ctx.AllocateVector( + itemAlignment: Unsafe.SizeOf(), + numberOfItems, + sizePerItem: Unsafe.SizeOf()); + + spanWriter.WriteUOffset(span, offset, vectorStartOffset); + spanWriter.WriteInt(span, numberOfItems, vectorStartOffset); + + + var start = span.Slice(vectorStartOffset + sizeof(uint)); + MemoryMarshal.Cast(buffer).CopyTo(start); + } /// /// Writes the given string. diff --git a/src/FlatSharp.UnityPolyfills/FlatSharp.UnityPolyfills.csproj b/src/FlatSharp.UnityPolyfills/FlatSharp.UnityPolyfills.csproj new file mode 100644 index 00000000..43824745 --- /dev/null +++ b/src/FlatSharp.UnityPolyfills/FlatSharp.UnityPolyfills.csproj @@ -0,0 +1,25 @@ + + + + + netstandard2.0 + FlatSharp.UnityPolyfills + FlatSharp.UnityPolyfills is a utility assembly to expose the Unity API needed for FlatSharp support of UnityEngine.Collections.NativeArray. + annotations + $(DefineContants);FLATSHARP_UNITY_POLYFILLS + FlatSharp.UnityPolyfills + true + + + + enable + + + + + + + + + + diff --git a/src/FlatSharp.UnityPolyfills/NativeArray.cs b/src/FlatSharp.UnityPolyfills/NativeArray.cs new file mode 100644 index 00000000..716fc78d --- /dev/null +++ b/src/FlatSharp.UnityPolyfills/NativeArray.cs @@ -0,0 +1,166 @@ +/* + * Copyright 2022 Unity Technologies + * + * 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; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Unity.Collections +{ + public enum NativeArrayOptions + { + UninitializedMemory = 0, + ClearMemory = 1 + } + + public enum Allocator + { + Invalid = 0, + None = 1, + Temp = 2, + TempJob = 3, + Persistent = 4, + } + + // for testing, we remove the 'where T : struct' constraint and enforce this in UnityNativeArrayVectorTypeModel + public unsafe struct NativeArray : IEnumerable, IEquatable> /* where T : struct */ + { + internal Memory m_Buffer; + + internal void* m_Data; + internal int m_Length; + + public NativeArray(int length, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory) + { + Allocate(length, allocator, out this); + } + + public NativeArray(T[] array, Allocator allocator) + { + Allocate(array.Length, allocator, out this); + array.AsSpan().CopyTo(m_Buffer.Span); + } + + public NativeArray(NativeArray array, Allocator allocator) + { + Allocate(array.Length, allocator, out this); + array.AsSpan().CopyTo(this.AsSpan()); + } + + static void Allocate(int length, Allocator allocator, out NativeArray nativeArray) + { + var backing = new T[length]; + nativeArray = new NativeArray() + { + m_Buffer = new Memory(backing) + }; + } + + struct Enumerator : IEnumerator + { + private readonly NativeArray m_Array; + private int m_Index; + + public Enumerator(NativeArray nativeArray) + { + m_Array = nativeArray; + m_Index = -1; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + m_Index++; + return m_Index < m_Array.Length; + } + + public void Reset() + { + m_Index = -1; + } + + // Let NativeArray indexer check for out of range. + public T Current => m_Array[m_Index]; + + object IEnumerator.Current => Current; + } + + public IEnumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool Equals(NativeArray other) + { + throw new NotImplementedException(); + } + + public int Length => m_Data != null ? m_Length : m_Buffer.Length; + + public T this[int index] + { + get => AsSpan()[index]; + + set => AsSpan()[index] = value; + } + + public readonly Span AsSpan() + { + return m_Data != null ? new Span(m_Data, m_Length) : m_Buffer.Span; + } + } +} + +namespace Unity.Collections.LowLevel.Unsafe +{ + public static unsafe class NativeArrayUnsafeUtility + { + public static NativeArray ConvertExistingDataToNativeArray(void* dataPointer, int length, Allocator allocator) where T : struct + { + var newArray = new NativeArray + { + m_Data = dataPointer, + m_Length = length, + }; + + return newArray; + } + } + + + public static unsafe class NativeArrayUnsafeUtilityEx + { + public static NativeArray ConvertExistingDataToNativeArray(Span span, Allocator allocator) where T : struct + { + var newArray = new NativeArray + { + m_Data = System.Runtime.CompilerServices.Unsafe.AsPointer(ref span.GetPinnableReference()), + m_Length = span.Length, + }; + + return newArray; + } + } +} diff --git a/src/FlatSharp.sln b/src/FlatSharp.sln index c7cd807a..b44ac9cf 100644 --- a/src/FlatSharp.sln +++ b/src/FlatSharp.sln @@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microbench.6.3.3", "Benchma EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlatSharpPoolableEndToEndTests", "Tests\FlatSharpPoolableEndToEndTests\FlatSharpPoolableEndToEndTests.csproj", "{7E55A248-4BBD-48AD-B27D-A7E0E705DC89}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlatSharp.UnityPolyfills", "FlatSharp.UnityPolyfills\FlatSharp.UnityPolyfills.csproj", "{E439E4AF-B948-4E6C-8791-1830619EB35B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -93,6 +95,10 @@ Global {7E55A248-4BBD-48AD-B27D-A7E0E705DC89}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E55A248-4BBD-48AD-B27D-A7E0E705DC89}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E55A248-4BBD-48AD-B27D-A7E0E705DC89}.Release|Any CPU.Build.0 = Release|Any CPU + {E439E4AF-B948-4E6C-8791-1830619EB35B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E439E4AF-B948-4E6C-8791-1830619EB35B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E439E4AF-B948-4E6C-8791-1830619EB35B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E439E4AF-B948-4E6C-8791-1830619EB35B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/FlatSharp/FlatBufferSerializer.cs b/src/FlatSharp/FlatBufferSerializer.cs index 28203f15..6a4da297 100644 --- a/src/FlatSharp/FlatBufferSerializer.cs +++ b/src/FlatSharp/FlatBufferSerializer.cs @@ -26,6 +26,9 @@ namespace FlatSharp; public sealed class FlatBufferSerializer { public static FlatBufferSerializer Default { get; } = new FlatBufferSerializer(new FlatBufferSerializerOptions()); + + // Test hook + internal static FlatBufferSerializer DefaultWithUnitySupport { get; } = new FlatBufferSerializer(new FlatBufferSerializerOptions(), TypeModelContainer.CreateDefault().WithUnitySupport(true)); private readonly Dictionary serializerCache = new(); diff --git a/src/FlatSharp/TypeModel/TypeModelContainerExtensions.cs b/src/FlatSharp/TypeModel/TypeModelContainerExtensions.cs new file mode 100644 index 00000000..d4b72bad --- /dev/null +++ b/src/FlatSharp/TypeModel/TypeModelContainerExtensions.cs @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Unity Technologies + * + * 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. + */ + +namespace FlatSharp.TypeModel; + +internal static class TypeModelContainerExtensions +{ + public static TypeModelContainer WithUnitySupport(this TypeModelContainer container, bool enableUnitySupport) + { + if (enableUnitySupport) + container.RegisterProvider(new UnityTypeModelProvider()); + return container; + } +} \ No newline at end of file diff --git a/src/FlatSharp/TypeModel/UnityTypeModelProvider.cs b/src/FlatSharp/TypeModel/UnityTypeModelProvider.cs new file mode 100644 index 00000000..03b26b87 --- /dev/null +++ b/src/FlatSharp/TypeModel/UnityTypeModelProvider.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Unity Technologies + * + * 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 FlatSharp.TypeModel.Vectors; + +namespace FlatSharp.TypeModel; + +public class UnityTypeModelProvider : ITypeModelProvider +{ + public bool TryCreateTypeModel(TypeModelContainer container, Type type, [NotNullWhen(true)] out ITypeModel? typeModel) + { + if (type.IsGenericType) + { + var genericDef = type.GetGenericTypeDefinition(); + if (genericDef.Namespace == "Unity.Collections" && genericDef.Name == "NativeArray`1") + { + typeModel = new UnityNativeArrayVectorTypeModel(type, container); + return true; + } + } + + typeModel = null; + return false; + } +} diff --git a/src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs b/src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs new file mode 100644 index 00000000..20051bb2 --- /dev/null +++ b/src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs @@ -0,0 +1,101 @@ +namespace FlatSharp.TypeModel.Vectors; + +/// +/// Defines a vector type model for a Unity NativeArray collection. +/// +public class UnityNativeArrayVectorTypeModel : BaseVectorTypeModel +{ + internal UnityNativeArrayVectorTypeModel(Type vectorType, TypeModelContainer provider) : base(vectorType, provider) + { + } + + public override string LengthPropertyName => nameof(Memory.Length); + + public override Type OnInitialize() + { + return this.ClrType.GetGenericArguments()[0]; + } + + public override void Validate() + { + if (!this.ClrType.IsGenericType || this.ClrType.GetGenericTypeDefinition().FullName != "Unity.Collections.NativeArray`1") + throw new InvalidFlatBufferDefinitionException( + $"UnityNativeArray vectors must be of type Unity.Collections.NativeArray. Type = {this.GetCompilableTypeName()}."); + + var genericArgumentType = this.ClrType.GetGenericArguments()[0]; + if (ItemTypeModel.SchemaType != FlatBufferSchemaType.Scalar && ItemTypeModel.SchemaType != FlatBufferSchemaType.Struct) + throw new InvalidFlatBufferDefinitionException( + $"UnityNativeArray vectors only support scalar or struct generic arguments. Type = {this.GetCompilableTypeName()}."); + + if (!genericArgumentType.IsValueType) + throw new InvalidFlatBufferDefinitionException( + $"UnityNativeArray vectors only support value type generic arguments. Type = {this.GetCompilableTypeName()}."); + + if (ItemTypeModel.PhysicalLayout.Length != 1) + throw new InvalidFlatBufferDefinitionException( + $"UnityNativeArray vectors do not support the specified generic argument type. Type = {this.GetCompilableTypeName()}."); + + + base.Validate(); + } + + protected override string CreateLoop(FlatBufferSerializerOptions options, string vectorVariableName, string numberOfItemsVariableName, string expectedVariableName, string body) + { + FlatSharpInternal.Assert(false, "Not expecting to do loop get max size for memory vector"); + throw new Exception(); + } + + public override CodeGeneratedMethod CreateParseMethodBody(ParserCodeGenContext context) + { + ValidateWriteThrough( + writeThroughSupported: false, + this, + context.AllFieldContexts, + context.Options); + + if (context.Options.GreedyDeserialize) + { + string body = $@" + var bufferSpan = {context.InputBufferVariableName}.UnsafeReadSpan<{context.InputBufferTypeName}, {this.ItemTypeModel.GetGlobalCompilableTypeName()}>({context.OffsetVariableName}); + var nativeArray = new NativeArray<{this.ItemTypeModel.GetGlobalCompilableTypeName()}>(bufferSpan.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + bufferSpan.CopyTo(nativeArray.AsSpan()); + return nativeArray; + "; + + return new CodeGeneratedMethod(body); + } + else + { + string body = $@" + if (!buffer.IsPinned) + throw new NotSupportedException(""Non-greedy parsing of a NativeArray requires a pinned buffer.""); + var bufferSpan = {context.InputBufferVariableName}.UnsafeReadSpan<{context.InputBufferTypeName}, {this.ItemTypeModel.GetGlobalCompilableTypeName()}>({context.OffsetVariableName}); + var nativeArray = Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtilityEx.ConvertExistingDataToNativeArray<{this.ItemTypeModel.GetGlobalCompilableTypeName()}>(bufferSpan, Allocator.None); + #if ENABLE_UNITY_COLLECTIONS_CHECKS + Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeArray, Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.GetTempUnsafePtrSliceHandle()); + #endif + return nativeArray; + "; + + return new CodeGeneratedMethod(body); + } + } + + public override CodeGeneratedMethod CreateSerializeMethodBody(SerializationCodeGenContext context) + { + var writeNativeArray = $"{context.SpanWriterVariableName}.UnsafeWriteSpan({context.SpanVariableName}, {context.ValueVariableName}.AsSpan(), {context.OffsetVariableName}, " + + $"{context.SerializationContextVariableName});"; + + return new CodeGeneratedMethod(writeNativeArray); + } + + public override CodeGeneratedMethod CreateCloneMethodBody(CloneCodeGenContext context) + { + string parameters = $"{context.ItemVariableName}, Unity.Collections.Allocator.Persistent"; + string body = $"return new Unity.Collections.NativeArray<{this.ItemTypeModel.GetCompilableTypeName()}>({parameters});"; + return new CodeGeneratedMethod(body) + { + IsMethodInline = true, + }; + } +} diff --git a/src/Tests/FlatSharpCompilerTests/CompilerTestHelpers.cs b/src/Tests/FlatSharpCompilerTests/CompilerTestHelpers.cs index f14b20d2..d29c2744 100644 --- a/src/Tests/FlatSharpCompilerTests/CompilerTestHelpers.cs +++ b/src/Tests/FlatSharpCompilerTests/CompilerTestHelpers.cs @@ -14,10 +14,12 @@ * limitations under the License. */ +using FlatSharp.TypeModel; + namespace FlatSharpTests.Compiler; public static class CompilerTestHelpers { public static readonly FlatBufferSerializer CompilerTestSerializer = new FlatBufferSerializer( - new FlatBufferSerializerOptions { EnableAppDomainInterceptOnAssemblyLoad = true }); + new FlatBufferSerializerOptions { EnableAppDomainInterceptOnAssemblyLoad = true }, TypeModelContainer.CreateDefault().WithUnitySupport(true)); } diff --git a/src/Tests/FlatSharpCompilerTests/CopyConstructorTests.cs b/src/Tests/FlatSharpCompilerTests/CopyConstructorTests.cs index 1ad03ad6..b7acea4c 100644 --- a/src/Tests/FlatSharpCompilerTests/CopyConstructorTests.cs +++ b/src/Tests/FlatSharpCompilerTests/CopyConstructorTests.cs @@ -15,6 +15,8 @@ */ using FlatSharp.Internal; +using System.Linq; +using Unity.Collections; namespace FlatSharpTests.Compiler; @@ -46,6 +48,7 @@ table OuterTable ({MetadataKeys.SerializerKind}: ""Greedy"") {{ IntVector_List:[int] ({MetadataKeys.VectorKind}:""IList"", id: 9); IntVector_RoList:[int] ({MetadataKeys.VectorKind}:""IReadOnlyList"", id: 10); IntVector_Array:[int] ({MetadataKeys.VectorKind}:""IList"", id: 11); + IntVector_NativeArray:[int] ({MetadataKeys.VectorKind}:""UnityNativeArray"", id: 26); TableVector_List:[InnerTable] ({MetadataKeys.VectorKind}:""IList"", id: 12); TableVector_RoList:[InnerTable] ({MetadataKeys.VectorKind}:""IReadOnlyList"", id: 13); @@ -94,6 +97,7 @@ table InnerTable {{ IntVector_Array = new[] { 7, 8, 9, }, IntVector_List = new[] { 10, 11, 12, }.ToList(), IntVector_RoList = new[] { 13, 14, 15 }.ToList(), + IntVector_NativeArray = new NativeArray(new[] { 16, 17, 18, }, Allocator.Persistent), TableVector_Array = CreateInner("Rocket", "Molly", "Clementine"), TableVector_Indexed = new IndexedVector(CreateInner("Pudge", "Sunshine", "Gypsy"), false), @@ -124,10 +128,10 @@ table InnerTable {{ } }; - byte[] data = new byte[FlatBufferSerializer.Default.GetMaxSize(original)]; - int bytesWritten = FlatBufferSerializer.Default.Serialize(original, data); + byte[] data = new byte[FlatBufferSerializer.DefaultWithUnitySupport.GetMaxSize(original)]; + int bytesWritten = FlatBufferSerializer.DefaultWithUnitySupport.Serialize(original, data); - Assembly asm = FlatSharpCompiler.CompileAndLoadAssembly(schema, new() { NormalizeFieldNames = false }); + Assembly asm = FlatSharpCompiler.CompileAndLoadAssembly(schema, new() { NormalizeFieldNames = false, UnityAssemblyPath = typeof(NativeArray<>).Assembly.Location}); Type outerTableType = asm.GetType("CopyConstructorTest.OuterTable"); dynamic serializer = outerTableType.GetProperty("Serializer", BindingFlags.Public | BindingFlags.Static).GetValue(null); @@ -152,6 +156,8 @@ table InnerTable {{ DeepCompareIntVector(original.IntVector_Array, parsed.IntVector_Array, copied.IntVector_Array); DeepCompareIntVector(original.IntVector_List, parsed.IntVector_List, copied.IntVector_List); DeepCompareIntVector(original.IntVector_RoList, parsed.IntVector_RoList, copied.IntVector_RoList); + DeepCompareIntVector(original.IntVector_NativeArray, parsed.IntVector_NativeArray, copied.IntVector_NativeArray); + Assert.Equal((byte)3, original.UnionVal.Value.Discriminator); Assert.Equal((byte)3, parsed.UnionVal.Discriminator); @@ -369,6 +375,9 @@ public class OuterTable [FlatBufferItem(11)] public virtual IList? IntVector_Array { get; set; } + [FlatBufferItem(26)] + public virtual NativeArray? IntVector_NativeArray { get; set; } + [FlatBufferItem(12)] public virtual IList? TableVector_List { get; set; } diff --git a/src/Tests/FlatSharpCompilerTests/FlatSharpCompilerTests.csproj b/src/Tests/FlatSharpCompilerTests/FlatSharpCompilerTests.csproj index ff6ed9d6..c6d52f09 100644 --- a/src/Tests/FlatSharpCompilerTests/FlatSharpCompilerTests.csproj +++ b/src/Tests/FlatSharpCompilerTests/FlatSharpCompilerTests.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Tests/FlatSharpCompilerTests/FullTests.cs b/src/Tests/FlatSharpCompilerTests/FullTests.cs index 4858075f..5db44b48 100644 --- a/src/Tests/FlatSharpCompilerTests/FullTests.cs +++ b/src/Tests/FlatSharpCompilerTests/FullTests.cs @@ -14,6 +14,8 @@ * limitations under the License. */ +using Unity.Collections; + namespace FlatSharpTests.Compiler; public class FullTests @@ -76,6 +78,7 @@ table Table ({MetadataKeys.SerializerKind}:""{option}"") ScalarVector : [uint] ({MetadataKeys.VectorKind}:""IList""); ScalarArray : [uint] ({MetadataKeys.VectorKind}:""IReadOnlyList""); + ScalarUnityNativeArray : [uint] ({MetadataKeys.VectorKind}:""UnityNativeArray""); UnionVector : [Any] ({MetadataKeys.VectorKind}:""IList""); UnionArray : [Any] ({MetadataKeys.VectorKind}:""IReadOnlyList""); @@ -98,7 +101,7 @@ struct OuterStruct {{ }} "; - FlatSharpCompiler.CompileAndLoadAssembly(schema, new()); + FlatSharpCompiler.CompileAndLoadAssembly(schema, new() { UnityAssemblyPath = typeof(NativeArray<>).Assembly.Location}); } #endif } diff --git a/src/Tests/FlatSharpCompilerTests/InvalidAttributeTests.cs b/src/Tests/FlatSharpCompilerTests/InvalidAttributeTests.cs index 04a0c995..31d861f4 100644 --- a/src/Tests/FlatSharpCompilerTests/InvalidAttributeTests.cs +++ b/src/Tests/FlatSharpCompilerTests/InvalidAttributeTests.cs @@ -126,6 +126,6 @@ namespace ns; "; var ex = Assert.Throws(() => FlatSharpCompiler.CompileAndLoadAssembly(schema, new())); - Assert.Contains("Unable to parse 'fs_vector' value 'banana'. Valid values are: IList, IReadOnlyList, Memory, ReadOnlyMemory, IIndexedVector.", ex.Message); + Assert.Contains("Unable to parse 'fs_vector' value 'banana'. Valid values are: IList, IReadOnlyList, Memory, ReadOnlyMemory, IIndexedVector, UnityNativeArray.", ex.Message); } } diff --git a/src/Tests/FlatSharpPoolableEndToEndTests/FlatSharpPoolableEndToEndTests.csproj b/src/Tests/FlatSharpPoolableEndToEndTests/FlatSharpPoolableEndToEndTests.csproj index b97f35aa..e2b7adff 100644 --- a/src/Tests/FlatSharpPoolableEndToEndTests/FlatSharpPoolableEndToEndTests.csproj +++ b/src/Tests/FlatSharpPoolableEndToEndTests/FlatSharpPoolableEndToEndTests.csproj @@ -40,6 +40,7 @@ + diff --git a/src/Tests/FlatSharpTests/ClassLib/TypeModelTests.cs b/src/Tests/FlatSharpTests/ClassLib/TypeModelTests.cs index 2b380aad..1228cb2c 100644 --- a/src/Tests/FlatSharpTests/ClassLib/TypeModelTests.cs +++ b/src/Tests/FlatSharpTests/ClassLib/TypeModelTests.cs @@ -17,6 +17,7 @@ using System.Linq; using FlatSharp.CodeGen; using FlatSharp.TypeModel; +using Unity.Collections; namespace FlatSharpTests; @@ -1016,6 +1017,48 @@ public void TypeModel_StructsWithPadding() Assert.Equal(33, structModel.PhysicalLayout.Single().InlineSize); Assert.Equal(8, structModel.PhysicalLayout.Single().Alignment); } + + [Fact] + public void TypeModel_Vector_NativeArrayOfUnionIsNotAllowed() + { + var ex = Assert.Throws( + () => TypeModelContainer.CreateDefault().WithUnitySupport(true).CreateTypeModel(typeof(NativeArray>))); + Assert.Equal("UnityNativeArray vectors only support scalar or struct generic arguments. Type = Unity.Collections.NativeArray>.", ex.Message); + } + + [Fact] + public void TypeModel_Vector_NativeArrayOfClassIsNotAllowed() + { + var ex = Assert.Throws( + () => TypeModelContainer.CreateDefault().WithUnitySupport(true).CreateTypeModel(typeof(NativeArray))); + Assert.Equal("UnityNativeArray vectors only support scalar or struct generic arguments. Type = Unity.Collections.NativeArray.", ex.Message); + } + + [Fact] + public void Parse_NativeArrayThrowsExceptionForNonPinnedBuffer() + { + byte[] data = + { + 12, 0, 0, 0, // offset to table start + 6, 0, // vtable length + 8, 0, // table length + 4, 0, // offset of index 0 field + 0, 0, // padding to 4-byte alignment + 8, 0, 0, 0, // soffset to vtable + 4, 0, 0, 0, // uoffset_t to vector + 0, 0, 0, 0, // vector length + }; + + Assert.Throws(() => + { + var table = new FlatBufferSerializer( + new FlatBufferSerializerOptions() { DeserializationOption = FlatBufferDeserializationOption.Lazy }, + TypeModelContainer.CreateDefault().WithUnitySupport(true)).Parse>>(data); + + // the lazy access here should cause an exception + table.Value.GetHashCode(); + }); + } public enum UntaggedEnum { diff --git a/src/Tests/FlatSharpTests/FlatSharpTests.csproj b/src/Tests/FlatSharpTests/FlatSharpTests.csproj index 7ff3566b..f1ca7505 100644 --- a/src/Tests/FlatSharpTests/FlatSharpTests.csproj +++ b/src/Tests/FlatSharpTests/FlatSharpTests.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Tests/FlatSharpTests/SerializationTests/VectorDeserializationTests.cs b/src/Tests/FlatSharpTests/SerializationTests/VectorDeserializationTests.cs index 57f52b58..9d8412d6 100644 --- a/src/Tests/FlatSharpTests/SerializationTests/VectorDeserializationTests.cs +++ b/src/Tests/FlatSharpTests/SerializationTests/VectorDeserializationTests.cs @@ -18,6 +18,8 @@ using System.Drawing; using System.Linq; using System.Runtime.InteropServices; +using FlatSharp.TypeModel; +using Unity.Collections; namespace FlatSharpTests; @@ -129,6 +131,73 @@ public void SimpleMemoryVector() Assert.Equal((byte)2, item.Vector.Span[1]); Assert.Equal((byte)3, item.Vector.Span[2]); } + + [Fact] + public void EmptyNativeArrayVector() + { + byte[] data = + { + 12, 0, 0, 0, // offset to table start + 6, 0, // vtable length + 8, 0, // table length + 4, 0, // offset of index 0 field + 0, 0, // padding to 4-byte alignment + 8, 0, 0, 0, // soffset to vtable + 4, 0, 0, 0, // uoffset_t to vector + 0, 0, 0, 0, // vector length + }; + + var item = FlatBufferSerializer.DefaultWithUnitySupport.Parse>>(data); + Assert.True(item.Vector.Length == 0); + } + + [Fact] + public void SimpleNativeArrayVector() + { + byte[] data = + { + 12, 0, 0, 0, // offset to table start + 6, 0, // vtable length + 8, 0, // table length + 4, 0, // offset of index 0 field + 0, 0, // padding to 4-byte alignment + 8, 0, 0, 0, // soffset to vtable + 4, 0, 0, 0, // uoffset_t to vector + 3, 0, 0, 0, // vector length + 1, 2, 3, // True false true + }; + + var item = FlatBufferSerializer.DefaultWithUnitySupport.Parse>>(data); + Assert.Equal(3, item.Vector.Length); + Assert.Equal((byte)1, item.Vector[0]); + Assert.Equal((byte)2, item.Vector[1]); + Assert.Equal((byte)3, item.Vector[2]); + } + + [Fact] + public void SimpleNativeArrayVectorOfInt() + { + byte[] data = + { + 12, 0, 0, 0, // offset to table start + 6, 0, // vtable length + 8, 0, // table length + 4, 0, // offset of index 0 field + 0, 0, // padding to 4-byte alignment + 8, 0, 0, 0, // soffset to vtable + 4, 0, 0, 0, // uoffset_t to vector + 3, 0, 0, 0, // vector length + 1, 0, 0, 0, // vector data + 2, 0, 0, 0, + 3, 0, 0, 0, + }; + + var item = FlatBufferSerializer.DefaultWithUnitySupport.Parse>>(data); + Assert.Equal(3, item.Vector.Length); + Assert.Equal(1, item.Vector[0]); + Assert.Equal(2, item.Vector[1]); + Assert.Equal(3, item.Vector[2]); + } [Fact] public void NullString() @@ -322,7 +391,7 @@ public void MemoryVector_GreedyDeserialize() Assert.Equal((byte)1, parsed1.Vector.Span[0]); Assert.Equal((byte)1, parsed2.Vector.Span[0]); - // Asser that this change affects only the 'parsed1' object. + // Assert that this change affects only the 'parsed1' object. parsed1.Vector.Span[0] = 2; Assert.Equal((byte)2, parsed1.Vector.Span[0]); @@ -390,6 +459,71 @@ public void MemoryVector_ProgressiveDeserialize() Assert.Equal((byte)2, parsed1.Vector.Span[0]); Assert.Equal((byte)2, parsed2.Vector.Span[0]); } + + [Fact] + public void NativeArrayVector_GreedyDeserialize() + { + RootTable> root = new RootTable>() + { + Vector = new NativeArray(new byte[100], Allocator.Temp) + }; + + root.Vector.AsSpan().Fill(1); + + byte[] buffer = new byte[1024]; + var options = new FlatBufferSerializerOptions(FlatBufferDeserializationOption.Greedy); + var serializer = new FlatBufferSerializer(options, TypeModelContainer.CreateDefault().WithUnitySupport(true)); + + serializer.Serialize(root, buffer.AsSpan()); + + var parsed1 = serializer.Parse>>(buffer); + var parsed2 = serializer.Parse>>(buffer); + + Assert.Equal((byte)1, parsed1.Vector[0]); + Assert.Equal((byte)1, parsed2.Vector[0]); + + // Assert that this change affects only the 'parsed1' object. + parsed1.Vector.AsSpan()[0] = 2; + + + Assert.Equal((byte)2, parsed1.Vector[0]); + Assert.Equal((byte)1, parsed2.Vector[0]); + } + + [Fact] + public void NativeArrayVector_LazyDeserialize() + { + RootTable> root = new RootTable>() + { + Vector = new NativeArray(new byte[100], Allocator.Temp) + }; + + root.Vector.AsSpan().Fill(1); + + byte[] buffer = new byte[1024]; + var options = new FlatBufferSerializerOptions(FlatBufferDeserializationOption.Lazy); + var serializer = new FlatBufferSerializer(options, TypeModelContainer.CreateDefault().WithUnitySupport(true)); + + serializer.Serialize(root, buffer.AsSpan()); + + var memory = buffer.AsMemory(); + using (var handle = memory.Pin()) + { + var parsed1 = + serializer.Parse>, MemoryInputBuffer>(new MemoryInputBuffer(memory, true)); + var parsed2 = + serializer.Parse>, MemoryInputBuffer>(new MemoryInputBuffer(memory, true)); + + Assert.Equal((byte)1, parsed1.Vector[0]); + Assert.Equal((byte)1, parsed2.Vector[0]); + + // Asser that this change affects both objects. + parsed1.Vector.AsSpan()[0] = 2; + + Assert.Equal((byte)2, parsed1.Vector[0]); + Assert.Equal((byte)2, parsed2.Vector[0]); + } + } [Fact] public void UnalignedStruct_Value5Byte() diff --git a/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs b/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs index 3ba014d9..f9047ff5 100644 --- a/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs +++ b/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs @@ -18,6 +18,8 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; +using FlatSharp.TypeModel; +using Unity.Collections; namespace FlatSharpTests; @@ -111,8 +113,8 @@ static void Test(Func factory) }; Span target = new byte[1024]; - int offset = FlatBufferSerializer.Default.Serialize(root, target); - string csharp = FlatBufferSerializer.Default.Compile(root).GetCSharp(); + int offset = FlatBufferSerializer.DefaultWithUnitySupport.Serialize(root, target); + string csharp = FlatBufferSerializer.DefaultWithUnitySupport.Compile(root).GetCSharp(); target = target.Slice(0, offset); @@ -123,8 +125,10 @@ static void Test(Func factory) Test>(a => a.ToList()); Test>(a => a.AsMemory()); Test>(a => a.AsMemory()); + Test>(a => new NativeArray(a, Allocator.Temp)); Test?>(a => a.AsMemory()); Test?>(a => a.AsMemory()); + Test?>(a => new NativeArray(a, Allocator.Temp)); } [Fact] @@ -150,8 +154,8 @@ static void Test(T instance) }; Span target = new byte[1024]; - int offset = FlatBufferSerializer.Default.Serialize(root, target); - string csharp = FlatBufferSerializer.Default.Compile(root).GetCSharp(); + int offset = FlatBufferSerializer.DefaultWithUnitySupport.Serialize(root, target); + string csharp = FlatBufferSerializer.DefaultWithUnitySupport.Compile(root).GetCSharp(); target = target.Slice(0, offset); @@ -162,8 +166,10 @@ static void Test(T instance) Test>(new List()); Test>(new Memory(new byte[0])); Test>(new ReadOnlyMemory(new byte[0])); + Test>(new NativeArray(new byte[0], Allocator.Temp)); Test?>(new Memory(new byte[0])); Test?>(new ReadOnlyMemory(new byte[0])); + Test?>(new NativeArray(new byte[0], Allocator.Temp)); Test>>(new IndexedVector>()); } @@ -186,8 +192,8 @@ static void Test() }; Span target = new byte[1024]; - int offset = FlatBufferSerializer.Default.Serialize(root, target); - string csharp = FlatBufferSerializer.Default.Compile(root).GetCSharp(); + int offset = FlatBufferSerializer.DefaultWithUnitySupport.Serialize(root, target); + string csharp = FlatBufferSerializer.DefaultWithUnitySupport.Compile(root).GetCSharp(); target = target.Slice(0, offset); @@ -198,6 +204,7 @@ static void Test() Test>(); Test?>(); Test?>(); + Test?>(); Test>>(); Test>>(); From c2e8ca579a38b994f7cf95ce47c8b7448e41f213 Mon Sep 17 00:00:00 2001 From: Jonathan Chambers Date: Thu, 1 Dec 2022 14:45:35 -0500 Subject: [PATCH 2/2] Ensure NativeArray serialization occurs only on aligned types. --- .../IO/SpanWriterExtensions.cs | 12 +++- .../UnityNativeArrayVectorTypeModel.cs | 2 +- .../VectorSerializationTests.cs | 69 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs b/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs index c40a00e0..52815de4 100644 --- a/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs +++ b/src/FlatSharp.Runtime/IO/SpanWriterExtensions.cs @@ -44,13 +44,19 @@ public static void UnsafeWriteSpan( Span span, Span buffer, int offset, - SerializationContext ctx) where TSpanWriter : ISpanWriter where TElement : struct + int alignment, + SerializationContext ctx) where TSpanWriter : ISpanWriter where TElement : unmanaged { + var size = Unsafe.SizeOf(); + + if (size % alignment != 0) + throw new InvalidOperationException($"Unsafe Span serialization does not support types with size {size} that is not a multiple of alignment {alignment}."); + int numberOfItems = buffer.Length; int vectorStartOffset = ctx.AllocateVector( - itemAlignment: Unsafe.SizeOf(), + itemAlignment: alignment, numberOfItems, - sizePerItem: Unsafe.SizeOf()); + sizePerItem: size); spanWriter.WriteUOffset(span, offset, vectorStartOffset); spanWriter.WriteInt(span, numberOfItems, vectorStartOffset); diff --git a/src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs b/src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs index 20051bb2..1c99faef 100644 --- a/src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs +++ b/src/FlatSharp/TypeModel/Vectors/UnityNativeArrayVectorTypeModel.cs @@ -84,7 +84,7 @@ public override CodeGeneratedMethod CreateParseMethodBody(ParserCodeGenContext c public override CodeGeneratedMethod CreateSerializeMethodBody(SerializationCodeGenContext context) { var writeNativeArray = $"{context.SpanWriterVariableName}.UnsafeWriteSpan({context.SpanVariableName}, {context.ValueVariableName}.AsSpan(), {context.OffsetVariableName}, " + - $"{context.SerializationContextVariableName});"; + $"{ItemTypeModel.PhysicalLayout[0].Alignment}, {context.SerializationContextVariableName});"; return new CodeGeneratedMethod(writeNativeArray); } diff --git a/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs b/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs index f9047ff5..e564c8d3 100644 --- a/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs +++ b/src/Tests/FlatSharpTests/SerializationTests/VectorSerializationTests.cs @@ -295,6 +295,67 @@ public void UnalignedStruct_Value5Byte() Assert.True(expectedResult.AsSpan().SequenceEqual(target)); } + [Fact] + public void UnalignedStruct_Value5Byte_NativeArray() + { + var root = new RootTable> + { + Vector = new NativeArray(new[] + { + new ValueFiveByteStruct { Byte = 1, Int = 1 }, + new ValueFiveByteStruct { Byte = 2, Int = 2 }, + new ValueFiveByteStruct { Byte = 3, Int = 3 }, + }, Allocator.Temp) + }; + + Assert.Throws(() => + { + Span target = new byte[10240]; + FlatBufferSerializer.DefaultWithUnitySupport.Serialize(root, target); + }); + } + + [Fact] + public void AlignedStruct_Value5Byte_NativeArray() + { + var root = new RootTable> + { + Vector = new NativeArray(new[] + { + new ValueFiveByteStructWithPadding { Byte = 1, Int = 1 }, + new ValueFiveByteStructWithPadding { Byte = 2, Int = 2 }, + new ValueFiveByteStructWithPadding { Byte = 3, Int = 3 }, + }, Allocator.Temp) + }; + + Span target = new byte[10240]; + int offset = FlatBufferSerializer.DefaultWithUnitySupport.Serialize(root, target); + target = target.Slice(0, offset); + + byte[] expectedResult = + { + 4, 0, 0, 0, // offset to table start + 248, 255, 255, 255, // soffset to vtable (-8) + 12, 0, 0, 0, // uoffset_t to vector + 6, 0, // vtable length + 8, 0, // table length + 4, 0, // offset of index 0 field + 0, 0, // padding to 4-byte alignment + 3, 0, 0, 0, // vector length + 1, 0, 0, 0, // index 0.Int + 1, // index 0.Byte + 0, 0, 0, // padding + 2, 0, 0, 0, // index 1.Int + 2, // index 1.Byte + 0, 0, 0, // padding + 3, 0, 0, 0, // index2.Int + 3, // Index2.byte + 0, 0, 0, // padding + }; + + Assert.True(expectedResult.AsSpan().SequenceEqual(target)); + } + [Fact] public void UnalignedStruct_9Byte() { @@ -840,4 +901,12 @@ public struct ValueFiveByteStruct [FieldOffset(4)] public byte Byte; } + + [FlatBufferStruct, StructLayout(LayoutKind.Explicit)] + public struct ValueFiveByteStructWithPadding + { + [FieldOffset(0)] public int Int; + + [FieldOffset(4)] public byte Byte; + } } \ No newline at end of file