using System; using System.Diagnostics; using System.Runtime.InteropServices; using Unity.Burst; using Unity.Jobs; using Unity.Jobs.LowLevel.Unsafe; using UnityEngine.Assertions; namespace Unity.Collections.LowLevel.Unsafe { internal static class UnsafeTextExtensions { public static ref UnsafeList AsUnsafeListOfBytes( this ref UnsafeText text ) { return ref UnsafeUtility.As>(ref text.m_UntypedListData); } } /// /// An unmanaged, mutable, resizable UTF-8 string. /// /// /// The string is always null-terminated, meaning a zero byte always immediately follows the last character. /// [BurstCompatible] [DebuggerDisplay("Length = {Length}, Capacity = {Capacity}, IsCreated = {IsCreated}, IsEmpty = {IsEmpty}")] [StructLayout(LayoutKind.Sequential)] public unsafe struct UnsafeText : INativeDisposable, IUTF8Bytes, INativeList { // NOTE! This Length is always > 0, because we have a null terminating byte. // We hide this byte from UnsafeText users. internal UntypedUnsafeList m_UntypedListData; /// /// Initializes and returns an instance of UnsafeText. /// /// The initial capacity, in bytes. /// The allocator to use. public UnsafeText(int capacity, AllocatorManager.AllocatorHandle allocator) { m_UntypedListData = default; this.AsUnsafeListOfBytes() = new UnsafeList(capacity + 1, allocator); Length = 0; } /// /// Whether this string's character buffer has been allocated (and not yet deallocated). /// /// Whether this string's character buffer has been allocated (and not yet deallocated). public bool IsCreated => this.AsUnsafeListOfBytes().IsCreated; /// /// Releases all resources (memory). /// public void Dispose() { this.AsUnsafeListOfBytes().Dispose(); } /// /// Creates and schedules a job that will dispose this string. /// /// The handle of a job which the new job will depend upon. /// The handle of a new job that will dispose this string. The new job depends upon inputDeps. [NotBurstCompatible /* This is not burst compatible because of IJob's use of a static IntPtr. Should switch to IJobBurstSchedulable in the future */] public JobHandle Dispose(JobHandle inputDeps) { return this.AsUnsafeListOfBytes().Dispose(inputDeps); } /// /// Reports whether container is empty. /// /// True if the string is empty or the string has not been constructed. public bool IsEmpty => !IsCreated || Length == 0; /// /// The byte at an index. /// /// A zero-based byte index. /// The byte at the index. /// Thrown if the index is out of bounds. public byte this[int index] { get { CheckIndexInRange(index); return UnsafeUtility.ReadArrayElement(m_UntypedListData.Ptr, index); } set { CheckIndexInRange(index); UnsafeUtility.WriteArrayElement(m_UntypedListData.Ptr, index, value); } } /// /// Returns a reference to the byte (not character) at an index. /// /// /// Deallocating or reallocating this string's character buffer makes the reference invalid. /// /// A byte index. /// A reference to the byte at the index. /// Thrown if the index is out of bounds. public ref byte ElementAt(int index) { CheckIndexInRange(index); return ref UnsafeUtility.ArrayElementAsRef(m_UntypedListData.Ptr, index); } /// /// Sets the length to 0. /// public void Clear() { Length = 0; } /// /// Returns a pointer to this string's character buffer. /// /// /// The pointer is made invalid by operations that reallocate the character buffer, such as setting . /// /// A pointer to this string's character buffer. public byte* GetUnsafePtr() { return (byte*)m_UntypedListData.Ptr; } /// /// Attempt to set the length in bytes of this string. /// /// The new length in bytes of the string. /// Whether any bytes added should be zeroed out. /// Always true. public bool TryResize(int newLength, NativeArrayOptions clearOptions = NativeArrayOptions.ClearMemory) { // this can't ever fail, because if we can't resize malloc will abort this.AsUnsafeListOfBytes().Resize(newLength + 1, clearOptions); this.AsUnsafeListOfBytes()[newLength] = 0; return true; } /// /// The current capacity in bytes of this string. /// /// /// The null-terminator byte is not included in the capacity, so the string's character buffer is `Capacity + 1` in size. /// /// The current capacity in bytes of the string. public int Capacity { get => this.AsUnsafeListOfBytes().Capacity - 1; set { CheckCapacityInRange(value + 1, this.AsUnsafeListOfBytes().Length); this.AsUnsafeListOfBytes().SetCapacity(value + 1); } } /// /// The current length in bytes of this string. /// /// /// The length does not include the null terminator byte. /// /// The current length in bytes of the UTF-8 encoded string. public int Length { get => this.AsUnsafeListOfBytes().Length - 1; set { this.AsUnsafeListOfBytes().Resize(value + 1); this.AsUnsafeListOfBytes()[value] = 0; } } /// /// Returns a managed string copy of this string. /// /// A managed string copy of this string. [NotBurstCompatible] public override string ToString() { if (!IsCreated) return ""; return this.ConvertToString(); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] void CheckIndexInRange(int index) { if (index < 0) throw new IndexOutOfRangeException($"Index {index} must be positive."); if (index >= Length) throw new IndexOutOfRangeException($"Index {index} is out of range in UnsafeText of {Length} length."); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] void ThrowCopyError(CopyError error, string source) { throw new ArgumentException($"UnsafeText: {error} while copying \"{source}\""); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] static void CheckCapacityInRange(int value, int length) { if (value < 0) throw new ArgumentOutOfRangeException($"Value {value} must be positive."); if ((uint)value < (uint)length) throw new ArgumentOutOfRangeException($"Value {value} is out of range in NativeList of '{length}' Length."); } } }