From 7c25c87ace67e1a620c829974227d311723f8f0e Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 28 Jul 2026 13:23:31 +0800 Subject: [PATCH 1/4] feat: add netstandard2.0 target framework support - Add netstandard2.0 to TargetFrameworks - Condition IsAotCompatible/IsTrimmable for net8.0+ - Add LangVersion latest - Condition System.Memory package for netstandard2.0 - Add IsExternalInit polyfill for netstandard2.0 - Add RequiresUnreferencedCodeAttribute polyfill for netstandard2.0 - Guard DateOnly/TimeOnly converters with #if NET8_0_OR_GREATER - Fix SerializeToStreamAsync: extract SerializeCoreAsync, guard 3-param override --- src/Irihi.Text.Toon/Http/ToonContent.cs | 9 ++++- .../Internal/BuiltInConverters.cs | 2 + .../Internal/IsExternalInit.cs | 17 ++++++++ .../RequiresUnreferencedCodeAttribute.cs | 40 +++++++++++++++++++ .../Internal/ToonDefaultConverters.cs | 4 ++ src/Irihi.Text.Toon/Irihi.Text.Toon.csproj | 12 ++++-- 6 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 src/Irihi.Text.Toon/Internal/IsExternalInit.cs create mode 100644 src/Irihi.Text.Toon/Internal/RequiresUnreferencedCodeAttribute.cs diff --git a/src/Irihi.Text.Toon/Http/ToonContent.cs b/src/Irihi.Text.Toon/Http/ToonContent.cs index dfcc966..22770fa 100644 --- a/src/Irihi.Text.Toon/Http/ToonContent.cs +++ b/src/Irihi.Text.Toon/Http/ToonContent.cs @@ -31,10 +31,15 @@ public static ToonContent Create(T value, ToonSerializerOptions? options = nu /// protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) - => SerializeToStreamAsync(stream, context, CancellationToken.None); + => SerializeCoreAsync(stream, CancellationToken.None); +#if NET8_0_OR_GREATER /// - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + => SerializeCoreAsync(stream, cancellationToken); +#endif + + private async Task SerializeCoreAsync(Stream stream, CancellationToken cancellationToken) { if (_value is null) { diff --git a/src/Irihi.Text.Toon/Internal/BuiltInConverters.cs b/src/Irihi.Text.Toon/Internal/BuiltInConverters.cs index 993c909..7fe01cd 100644 --- a/src/Irihi.Text.Toon/Internal/BuiltInConverters.cs +++ b/src/Irihi.Text.Toon/Internal/BuiltInConverters.cs @@ -139,6 +139,7 @@ public override void Write(ref Utf8ToonWriter writer, DateTimeOffset value, Toon => writer.WriteStringValue(value.ToString("O")); } +#if NET8_0_OR_GREATER internal sealed class DateOnlyToonConverter : ToonConverter { public override DateOnly Read(ref Utf8ToonReader reader, Type typeToConvert, ToonSerializerOptions options) @@ -154,6 +155,7 @@ public override TimeOnly Read(ref Utf8ToonReader reader, Type typeToConvert, Too public override void Write(ref Utf8ToonWriter writer, TimeOnly value, ToonSerializerOptions options) => writer.WriteStringValue(value.ToString("O")); } +#endif internal sealed class TimeSpanToonConverter : ToonConverter { diff --git a/src/Irihi.Text.Toon/Internal/IsExternalInit.cs b/src/Irihi.Text.Toon/Internal/IsExternalInit.cs new file mode 100644 index 0000000..73d4b98 --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/IsExternalInit.cs @@ -0,0 +1,17 @@ +#if NETSTANDARD2_0 + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + internal static class IsExternalInit + { + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Internal/RequiresUnreferencedCodeAttribute.cs b/src/Irihi.Text.Toon/Internal/RequiresUnreferencedCodeAttribute.cs new file mode 100644 index 0000000..3fc7147 --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/RequiresUnreferencedCodeAttribute.cs @@ -0,0 +1,40 @@ +#if NETSTANDARD2_0 + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Indicates that the specified method requires dynamic access to code that is not + /// statically referenced, for example through . + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)] + internal sealed class RequiresUnreferencedCodeAttribute : Attribute + { + /// + /// Initializes a new instance of the class + /// with the specified message. + /// + /// + /// A message that contains information about the usage of unreferenced code. + /// + public RequiresUnreferencedCodeAttribute(string message) + { + Message = message; + } + + /// + /// Gets a message that contains information about the usage of unreferenced code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the member, + /// why it requires unreferenced code, and what options a consumer has to deal with it. + /// + public string? Url { get; set; } + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Internal/ToonDefaultConverters.cs b/src/Irihi.Text.Toon/Internal/ToonDefaultConverters.cs index 89522e2..05fea2b 100644 --- a/src/Irihi.Text.Toon/Internal/ToonDefaultConverters.cs +++ b/src/Irihi.Text.Toon/Internal/ToonDefaultConverters.cs @@ -21,8 +21,10 @@ internal static class ToonDefaultConverters internal static readonly ToonConverter GuidConverter = new GuidToonConverter(); internal static readonly ToonConverter DateTimeConverter = new DateTimeToonConverter(); internal static readonly ToonConverter DateTimeOffsetConverter = new DateTimeOffsetToonConverter(); +#if NET8_0_OR_GREATER internal static readonly ToonConverter DateOnlyConverter = new DateOnlyToonConverter(); internal static readonly ToonConverter TimeOnlyConverter = new TimeOnlyToonConverter(); +#endif internal static readonly ToonConverter TimeSpanConverter = new TimeSpanToonConverter(); internal static readonly ToonConverter UriConverter = new UriToonConverter(); internal static readonly ToonConverter VersionConverter = new VersionToonConverter(); @@ -47,8 +49,10 @@ static ToonDefaultConverters() [typeof(Guid)] = new GuidToonConverter(), [typeof(DateTime)] = new DateTimeToonConverter(), [typeof(DateTimeOffset)] = new DateTimeOffsetToonConverter(), +#if NET8_0_OR_GREATER [typeof(DateOnly)] = new DateOnlyToonConverter(), [typeof(TimeOnly)] = new TimeOnlyToonConverter(), +#endif [typeof(TimeSpan)] = new TimeSpanToonConverter(), [typeof(Uri)] = new UriToonConverter(), [typeof(Version)] = new VersionToonConverter(), diff --git a/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj b/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj index e2119f9..4781d0e 100644 --- a/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj +++ b/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj @@ -1,11 +1,12 @@ - net8.0;net10.0 + netstandard2.0;net8.0;net10.0 + latest enable enable - true - true + true + true true @@ -31,6 +32,11 @@ Visible="false" /> + + + + + From e3f1df18194c044eccc8e9ce464c93883b252893 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 28 Jul 2026 13:31:37 +0800 Subject: [PATCH 2/4] feat: add more netstandard2.0 polyfills and fix API incompatibilities - Add System.Index/Range polyfill for ^ and .. syntax - Add CallerArgumentExpression polyfill - Add NotNullAttribute polyfill - Add ThrowHelper to replace ArgumentNullException.ThrowIfNull - Add System.ValueTuple package for netstandard2.0 - Fix Array.Fill with for-loop on netstandard2.0 --- .../CallerArgumentExpressionAttribute.cs | 22 ++++ src/Irihi.Text.Toon/Internal/IndexRange.cs | 110 ++++++++++++++++++ .../Internal/NotNullAttribute.cs | 12 ++ src/Irihi.Text.Toon/Internal/ThrowHelper.cs | 21 ++++ src/Irihi.Text.Toon/Irihi.Text.Toon.csproj | 1 + src/Irihi.Text.Toon/Nodes/ToonNode.cs | 2 +- .../Nodes/ToonNodeExtensions.cs | 2 +- .../Metadata/ToonTypeInfoBuilder.cs | 4 +- .../Serialization/ToonSerializer.cs | 16 +-- src/Irihi.Text.Toon/ToonDocument.cs | 4 +- src/Irihi.Text.Toon/ToonElement.cs | 2 +- src/Irihi.Text.Toon/Utf8ToonWriter.cs | 5 + 12 files changed, 186 insertions(+), 15 deletions(-) create mode 100644 src/Irihi.Text.Toon/Internal/CallerArgumentExpressionAttribute.cs create mode 100644 src/Irihi.Text.Toon/Internal/IndexRange.cs create mode 100644 src/Irihi.Text.Toon/Internal/NotNullAttribute.cs create mode 100644 src/Irihi.Text.Toon/Internal/ThrowHelper.cs diff --git a/src/Irihi.Text.Toon/Internal/CallerArgumentExpressionAttribute.cs b/src/Irihi.Text.Toon/Internal/CallerArgumentExpressionAttribute.cs new file mode 100644 index 0000000..558fb6e --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,22 @@ +#if NETSTANDARD2_0 + +namespace System.Runtime.CompilerServices +{ + /// + /// Indicates that a parameter captures the expression passed for another parameter as a string. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : Attribute + { + /// Initializes a new instance with the target parameter name. + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + /// Gets the name of the parameter whose expression is captured. + public string ParameterName { get; } + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Internal/IndexRange.cs b/src/Irihi.Text.Toon/Internal/IndexRange.cs new file mode 100644 index 0000000..1a7e01f --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/IndexRange.cs @@ -0,0 +1,110 @@ +#if NETSTANDARD2_0 + +namespace System +{ + /// Represents a type that can index a collection from the start or the end. + public readonly struct Index : IEquatable + { + private readonly int _value; + + /// Creates an index with the given value and direction. + public Index(int value, bool fromEnd = false) + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value)); + _value = fromEnd ? ~value : value; + } + + /// Gets the raw value (non-negative when from start, complemented when from end). + public int Value => _value < 0 ? ~_value : _value; + + /// Whether this index counts from the end. + public bool IsFromEnd => _value < 0; + + /// Returns the absolute offset from the start for a collection of the given length. + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + offset += length + 1; + return offset; + } + + /// + public bool Equals(Index other) => _value == other._value; + + /// + public override bool Equals(object? obj) => obj is Index other && Equals(other); + + /// + public override int GetHashCode() => _value; + + /// + public override string ToString() + { + if (IsFromEnd) + return "^" + Value; + return Value.ToString(); + } + + public static implicit operator Index(int value) => new Index(value); + + public static bool operator ==(Index left, Index right) => left.Equals(right); + public static bool operator !=(Index left, Index right) => !left.Equals(right); + } + + /// Represents a range with a start and end index. + public readonly struct Range : IEquatable + { + /// Gets the start index (inclusive). + public Index Start { get; } + + /// Gets the end index (exclusive). + public Index End { get; } + + /// Creates a range from start (inclusive) to end (exclusive). + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Creates a range from to the end of the collection. + public static Range StartAt(Index start) => new Range(start, new Index(0, fromEnd: true)); + + /// Creates a range from the start of the collection to . + public static Range EndAt(Index end) => new Range(new Index(0), end); + + /// A range that covers the entire collection. + public static Range All => new Range(new Index(0), new Index(0, fromEnd: true)); + + /// Calculates the start offset and length for a collection of . + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + throw new ArgumentOutOfRangeException(nameof(length)); + + return (start, end - start); + } + + /// + public bool Equals(Range other) => Start.Equals(other.Start) && End.Equals(other.End); + + /// + public override bool Equals(object? obj) => obj is Range other && Equals(other); + + /// + public override int GetHashCode() => unchecked(Start.GetHashCode() * 31 + End.GetHashCode()); + + /// + public override string ToString() => Start + ".." + End; + + public static bool operator ==(Range left, Range right) => left.Equals(right); + public static bool operator !=(Range left, Range right) => !left.Equals(right); + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Internal/NotNullAttribute.cs b/src/Irihi.Text.Toon/Internal/NotNullAttribute.cs new file mode 100644 index 0000000..a50532b --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/NotNullAttribute.cs @@ -0,0 +1,12 @@ +#if NETSTANDARD2_0 + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that an output is not null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Internal/ThrowHelper.cs b/src/Irihi.Text.Toon/Internal/ThrowHelper.cs new file mode 100644 index 0000000..637690c --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/ThrowHelper.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Irihi.Text.Toon.Internal; + +/// Unified null-argument guard for netstandard2.0 polyfill. +internal static class ThrowHelper +{ + /// Throws if is null. + public static void ThrowIfNull( + [NotNull] object? argument, + [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { +#if NETSTANDARD2_0 + if (argument is null) + throw new ArgumentNullException(paramName); +#else + ArgumentNullException.ThrowIfNull(argument, paramName); +#endif + } +} diff --git a/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj b/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj index 4781d0e..8227f70 100644 --- a/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj +++ b/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Irihi.Text.Toon/Nodes/ToonNode.cs b/src/Irihi.Text.Toon/Nodes/ToonNode.cs index dbddda2..8be544f 100644 --- a/src/Irihi.Text.Toon/Nodes/ToonNode.cs +++ b/src/Irihi.Text.Toon/Nodes/ToonNode.cs @@ -47,7 +47,7 @@ public string ToToonString(ToonWriterOptions? options = null) /// public static ToonNode Parse(string toon) { - ArgumentNullException.ThrowIfNull(toon); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(toon); int maxByteCount = Encoding.UTF8.GetMaxByteCount(toon.Length); byte[] rented = ArrayPool.Shared.Rent(maxByteCount); try diff --git a/src/Irihi.Text.Toon/Nodes/ToonNodeExtensions.cs b/src/Irihi.Text.Toon/Nodes/ToonNodeExtensions.cs index 1d25212..16a3951 100644 --- a/src/Irihi.Text.Toon/Nodes/ToonNodeExtensions.cs +++ b/src/Irihi.Text.Toon/Nodes/ToonNodeExtensions.cs @@ -34,7 +34,7 @@ public static IEnumerable DescendantNodesAndSelf(this ToonNode node) /// public static ToonNode? SelectPath(this ToonNode node, string path) { - ArgumentNullException.ThrowIfNull(path); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(path); ToonNode? current = node; foreach (string segment in path.Split('.')) diff --git a/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoBuilder.cs b/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoBuilder.cs index ca9491e..95ff6a9 100644 --- a/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoBuilder.cs +++ b/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoBuilder.cs @@ -20,8 +20,8 @@ internal static class ToonTypeInfoBuilder /// internal static ToonTypeInfo Build(Type type, ToonSerializerOptions options) { - ArgumentNullException.ThrowIfNull(type); - ArgumentNullException.ThrowIfNull(options); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(type); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(options); // Collection / dictionary detection bool isDictionary = TryGetDictionaryInfo(type, out var dictKeyType, out var dictValueType); diff --git a/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs b/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs index c23306b..8748f10 100644 --- a/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs +++ b/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs @@ -56,7 +56,7 @@ public static byte[] SerializeToUtf8Bytes(T value, ToonSerializerOptions? opt public static void Serialize(Stream utf8Stream, T value, ToonSerializerOptions? options = null) { - ArgumentNullException.ThrowIfNull(utf8Stream); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(utf8Stream); var opt = FreezeOptions(options); var wOpts = new ToonWriterOptions { MinifyOutput = opt.MinifyOutput, IndentSize = opt.IndentSize }; var writer = new Utf8ToonWriter(utf8Stream, wOpts); @@ -70,7 +70,7 @@ public static async Task SerializeAsync( ToonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(utf8Stream); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(utf8Stream); cancellationToken.ThrowIfCancellationRequested(); @@ -85,7 +85,7 @@ public static async Task SerializeAsync( public static T? Deserialize(string toon, ToonSerializerOptions? options = null) { - ArgumentNullException.ThrowIfNull(toon); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(toon); var opt = FreezeOptions(options); int maxByteCount = Encoding.UTF8.GetMaxByteCount(toon.Length); byte[] rented = ArrayPool.Shared.Rent(maxByteCount); @@ -105,7 +105,7 @@ public static async Task SerializeAsync( /// public static object? Deserialize(string toon, Type type, ToonSerializerOptions? options = null) { - ArgumentNullException.ThrowIfNull(toon); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(toon); var opt = FreezeOptions(options); var bytes = Encoding.UTF8.GetBytes(toon); var reader = new Utf8ToonReader(bytes, new ToonReaderOptions { Mode = ToonReaderMode.Loose }); @@ -126,7 +126,7 @@ public static async Task SerializeAsync( public static T? Deserialize(Stream utf8Stream, ToonSerializerOptions? options = null) { - ArgumentNullException.ThrowIfNull(utf8Stream); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(utf8Stream); var opt = FreezeOptions(options); byte[] buffer = ArrayPool.Shared.Rent(4096); @@ -160,7 +160,7 @@ public static async Task SerializeAsync( ToonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(utf8Stream); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(utf8Stream); byte[] buffer = ArrayPool.Shared.Rent(4096); int total = 0; @@ -195,7 +195,7 @@ public static async Task SerializeAsync( public static string Serialize(T value, ToonTypeInfo typeInfo) { - ArgumentNullException.ThrowIfNull(typeInfo); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(typeInfo); var opt = ToonSerializerOptions.Default; var bw = new ArrayBufferWriter(); var w = new Utf8ToonWriter(bw, new ToonWriterOptions { IndentSize = 2 }); @@ -206,7 +206,7 @@ public static string Serialize(T value, ToonTypeInfo typeInfo) public static byte[] SerializeToUtf8Bytes(T value, ToonTypeInfo typeInfo) { - ArgumentNullException.ThrowIfNull(typeInfo); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(typeInfo); var opt = ToonSerializerOptions.Default; var bw = new ArrayBufferWriter(); var w = new Utf8ToonWriter(bw, new ToonWriterOptions { IndentSize = 2 }); diff --git a/src/Irihi.Text.Toon/ToonDocument.cs b/src/Irihi.Text.Toon/ToonDocument.cs index 2d40f12..137ea31 100644 --- a/src/Irihi.Text.Toon/ToonDocument.cs +++ b/src/Irihi.Text.Toon/ToonDocument.cs @@ -37,7 +37,7 @@ public ToonElement RootElement /// Parses a TOON string into a document. public static ToonDocument Parse(string toon, ToonDocumentOptions options = default) { - ArgumentNullException.ThrowIfNull(toon); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(toon); int maxByteCount = Encoding.UTF8.GetMaxByteCount(toon.Length); byte[] rented = ArrayPool.Shared.Rent(maxByteCount); int written; @@ -74,7 +74,7 @@ public static ToonDocument Parse(ReadOnlySpan utf8Toon, ToonDocumentOption /// public static ToonDocument Parse(Stream stream, ToonDocumentOptions options = default) { - ArgumentNullException.ThrowIfNull(stream); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(stream); int initialSize = 4096; if (stream.CanSeek) diff --git a/src/Irihi.Text.Toon/ToonElement.cs b/src/Irihi.Text.Toon/ToonElement.cs index ac6c8de..1e73de6 100644 --- a/src/Irihi.Text.Toon/ToonElement.cs +++ b/src/Irihi.Text.Toon/ToonElement.cs @@ -48,7 +48,7 @@ public ToonElement this[string propertyName] /// Tries to get a child property by name. public bool TryGetProperty(string propertyName, out ToonElement value) { - ArgumentNullException.ThrowIfNull(propertyName); + Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(propertyName); int byteCount = Encoding.UTF8.GetByteCount(propertyName); if (byteCount <= 256) diff --git a/src/Irihi.Text.Toon/Utf8ToonWriter.cs b/src/Irihi.Text.Toon/Utf8ToonWriter.cs index f215be7..2fb01bd 100644 --- a/src/Irihi.Text.Toon/Utf8ToonWriter.cs +++ b/src/Irihi.Text.Toon/Utf8ToonWriter.cs @@ -28,7 +28,12 @@ public ref partial struct Utf8ToonWriter private static byte[] BuildIndentCache() { var cache = new byte[512]; +#if NETSTANDARD2_0 + for (int i = 0; i < cache.Length; i++) + cache[i] = ToonUtf8Constants.Space; +#else Array.Fill(cache, ToonUtf8Constants.Space); +#endif return cache; } From ddd58d213f2d1acfe6dc727f36a43df936990a36 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 28 Jul 2026 13:55:35 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20complete=20netstandard2.0=20build?= =?UTF-8?q?=20support=20=E2=80=94=20zero=20errors/warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add EncodingPolyfill (GetUtf8String/GetUtf8Bytes for Span APIs) - Add PolyfillArrayBufferWriter (replaces ArrayBufferWriter) - Add MemoryMarshalShim (replaces MemoryMarshal.CreateSpan) - Add StreamPolyfill (WriteAsync/ReadAsync Memory overloads) - Add KvpExtensions (Deconstruct for KeyValuePair) - Fix Enum.Parse -> (T)Enum.Parse(typeof(T),...) - Fix Type.IsAssignableTo -> IsAssignableFrom - Fix HashCode.Combine -> manual hash - Fix ConcurrentDictionary.GetOrAdd 3-arg -> closure - Fix decimal.TryFormat -> ToString+GetBytes - Fix ReadOnlySpan.Contains -> IndexOf - Fix ReadOnlySequence.FirstSpan -> First.Span - Fix pattern-matching unassigned enumerable - Add System.Buffers package reference - Add CultureInfo using --- .../Http/ToonHttpClientExtensions.cs | 4 ++ .../Internal/ConverterFactories.cs | 4 ++ .../Internal/EncodingPolyfill.cs | 39 ++++++++++++ src/Irihi.Text.Toon/Internal/KvpExtensions.cs | 17 ++++++ .../Internal/MemoryMarshalShim.cs | 17 ++++++ .../Internal/ObjectToonConverter.cs | 10 +++- .../Internal/PolyfillArrayBufferWriter.cs | 60 +++++++++++++++++++ .../Internal/StreamPolyfill.cs | 21 +++++++ src/Irihi.Text.Toon/Irihi.Text.Toon.csproj | 1 + src/Irihi.Text.Toon/Nodes/ToonNode.cs | 17 ++++-- src/Irihi.Text.Toon/Nodes/ToonObject.cs | 1 + .../Metadata/ToonTypeInfoCache.cs | 8 +++ .../Serialization/ToonSerializer.cs | 44 +++++++++++--- src/Irihi.Text.Toon/ToonDocument.cs | 3 +- src/Irihi.Text.Toon/ToonElement.cs | 18 +++--- .../Utf8ToonReader.Sequence.cs | 2 +- src/Irihi.Text.Toon/Utf8ToonReader.cs | 8 +-- src/Irihi.Text.Toon/Utf8ToonWriter.cs | 11 +++- 18 files changed, 251 insertions(+), 34 deletions(-) create mode 100644 src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs create mode 100644 src/Irihi.Text.Toon/Internal/KvpExtensions.cs create mode 100644 src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs create mode 100644 src/Irihi.Text.Toon/Internal/PolyfillArrayBufferWriter.cs create mode 100644 src/Irihi.Text.Toon/Internal/StreamPolyfill.cs diff --git a/src/Irihi.Text.Toon/Http/ToonHttpClientExtensions.cs b/src/Irihi.Text.Toon/Http/ToonHttpClientExtensions.cs index 0f2564d..b4046c3 100644 --- a/src/Irihi.Text.Toon/Http/ToonHttpClientExtensions.cs +++ b/src/Irihi.Text.Toon/Http/ToonHttpClientExtensions.cs @@ -70,7 +70,11 @@ public static Task PutAsToonAsync(this HttpClient client public static async Task ReadFromToonAsync(this HttpContent content, ToonSerializerOptions? options, CancellationToken cancellationToken = default) { +#if NETSTANDARD2_0 + var toon = await content.ReadAsStringAsync().ConfigureAwait(false); +#else var toon = await content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); +#endif if (string.IsNullOrWhiteSpace(toon) || toon == "null") return default; return ToonSerializer.Deserialize(toon, options); } diff --git a/src/Irihi.Text.Toon/Internal/ConverterFactories.cs b/src/Irihi.Text.Toon/Internal/ConverterFactories.cs index 5e15332..e9374a5 100644 --- a/src/Irihi.Text.Toon/Internal/ConverterFactories.cs +++ b/src/Irihi.Text.Toon/Internal/ConverterFactories.cs @@ -65,7 +65,11 @@ internal sealed class EnumToonConverter : ToonConverter where T : struct, public override T Read(ref Utf8ToonReader reader, Type typeToConvert, ToonSerializerOptions options) { if (options.EnumFormat == ToonEnumFormat.String || reader.TokenType == ToonTokenType.String) +#if NETSTANDARD2_0 + return (T)Enum.Parse(typeof(T), reader.GetString(), ignoreCase: true); +#else return Enum.Parse(reader.GetString(), ignoreCase: true); +#endif if (reader.TokenType == ToonTokenType.Number) return (T)Enum.ToObject(typeof(T), reader.GetInt64()); throw new ToonException($"Cannot deserialize {reader.TokenType} as '{typeof(T).Name}'."); diff --git a/src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs b/src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs new file mode 100644 index 0000000..0b7ea91 --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace Irihi.Text.Toon.Internal; + +/// Polyfill for Encoding.UTF8 span-based APIs on netstandard2.0. +internal static class EncodingPolyfill +{ + public static string GetUtf8String(ReadOnlySpan bytes) + { +#if NETSTANDARD2_0 + if (bytes.IsEmpty) return string.Empty; + return Encoding.UTF8.GetString(bytes.ToArray()); +#else + return Encoding.UTF8.GetString(bytes); +#endif + } + + public static int GetUtf8Bytes(string value, Span destination) + { +#if NETSTANDARD2_0 + byte[] bytes = Encoding.UTF8.GetBytes(value); + bytes.CopyTo(destination); + return bytes.Length; +#else + return Encoding.UTF8.GetBytes(value, destination); +#endif + } + + public static int GetUtf8Bytes(ReadOnlySpan value, Span destination) + { +#if NETSTANDARD2_0 + byte[] bytes = Encoding.UTF8.GetBytes(value.ToArray()); + bytes.CopyTo(destination); + return bytes.Length; +#else + return Encoding.UTF8.GetBytes(value, destination); +#endif + } +} diff --git a/src/Irihi.Text.Toon/Internal/KvpExtensions.cs b/src/Irihi.Text.Toon/Internal/KvpExtensions.cs new file mode 100644 index 0000000..b8845c3 --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/KvpExtensions.cs @@ -0,0 +1,17 @@ +#if NETSTANDARD2_0 + +using System.Collections.Generic; + +namespace Irihi.Text.Toon.Internal; + +/// Polyfill Deconstruct for on netstandard2.0. +internal static class KvpExtensions +{ + public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + { + key = kvp.Key; + value = kvp.Value; + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs b/src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs new file mode 100644 index 0000000..c3a769d --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Irihi.Text.Toon.Internal; + +/// Polyfill for on netstandard2.0. +internal static class MemoryMarshalShim +{ + public static unsafe Span CreateSpan(ref T reference, int length) + { +#if NETSTANDARD2_0 + return new Span(Unsafe.AsPointer(ref reference), length); +#else + return MemoryMarshal.CreateSpan(ref reference, length); +#endif + } +} diff --git a/src/Irihi.Text.Toon/Internal/ObjectToonConverter.cs b/src/Irihi.Text.Toon/Internal/ObjectToonConverter.cs index 04b4b1d..f2a0ed9 100644 --- a/src/Irihi.Text.Toon/Internal/ObjectToonConverter.cs +++ b/src/Irihi.Text.Toon/Internal/ObjectToonConverter.cs @@ -77,10 +77,16 @@ public override void Write(ref Utf8ToonWriter writer, T value, ToonSerializerOpt if (ShouldSkip(pv, prop)) continue; // Collection property: embed [N] header or use tabular - if (pv is IEnumerable enumerable + var enumerable = pv as IEnumerable; + if (enumerable is not null && prop.PropertyType != typeof(string) && pv is not IDictionary - && !prop.PropertyType.IsAssignableTo(typeof(IDictionary))) + && ! +#if NETSTANDARD2_0 + typeof(IDictionary).IsAssignableFrom(prop.PropertyType)) +#else + prop.PropertyType.IsAssignableTo(typeof(IDictionary))) +#endif { var items = new List(); foreach (object? item in enumerable) items.Add(item); diff --git a/src/Irihi.Text.Toon/Internal/PolyfillArrayBufferWriter.cs b/src/Irihi.Text.Toon/Internal/PolyfillArrayBufferWriter.cs new file mode 100644 index 0000000..0d8a18f --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/PolyfillArrayBufferWriter.cs @@ -0,0 +1,60 @@ +#if NETSTANDARD2_0 + +using System.Buffers; + +namespace Irihi.Text.Toon.Internal; + +/// Polyfill for on netstandard2.0. +internal sealed class PolyfillArrayBufferWriter : IBufferWriter +{ + private T[] _buffer; + private int _index; + + public PolyfillArrayBufferWriter(int initialCapacity = 256) + { + _buffer = ArrayPool.Shared.Rent(initialCapacity); + } + + public ReadOnlyMemory WrittenMemory => new ReadOnlyMemory(_buffer, 0, _index); + public ReadOnlySpan WrittenSpan => new ReadOnlySpan(_buffer, 0, _index); + public int WrittenCount => _index; + public int Capacity => _buffer.Length; + public int FreeCapacity => _buffer.Length - _index; + + public void Clear() => _index = 0; + + public void Advance(int count) + { + if (count < 0 || _index + count > _buffer.Length) + throw new InvalidOperationException("Cannot advance past capacity."); + _index += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + Ensure(sizeHint); + return new Memory(_buffer, _index, _buffer.Length - _index); + } + + public Span GetSpan(int sizeHint = 0) + { + Ensure(sizeHint); + return new Span(_buffer, _index, _buffer.Length - _index); + } + + private void Ensure(int sizeHint) + { + if (sizeHint < 0) sizeHint = 0; + if (sizeHint == 0 && FreeCapacity > 0) return; + + int growBy = Math.Max(sizeHint, _buffer.Length); + int newSize = _buffer.Length + growBy; + + var newBuf = ArrayPool.Shared.Rent(newSize); + Array.Copy(_buffer, 0, newBuf, 0, _index); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuf; + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Internal/StreamPolyfill.cs b/src/Irihi.Text.Toon/Internal/StreamPolyfill.cs new file mode 100644 index 0000000..4b1e6d1 --- /dev/null +++ b/src/Irihi.Text.Toon/Internal/StreamPolyfill.cs @@ -0,0 +1,21 @@ +#if NETSTANDARD2_0 + +namespace Irihi.Text.Toon.Internal; + +/// Polyfill extension methods for on netstandard2.0. +internal static class StreamPolyfill +{ + public static Task WriteAsync(this Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + var array = buffer.ToArray(); + return stream.WriteAsync(array, 0, array.Length, cancellationToken); + } + + public static Task ReadAsync(this Stream stream, Memory buffer, CancellationToken cancellationToken) + { + var array = buffer.ToArray(); // pre-allocate target + return stream.ReadAsync(array, 0, buffer.Length, cancellationToken); + } +} + +#endif diff --git a/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj b/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj index 8227f70..c11231d 100644 --- a/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj +++ b/src/Irihi.Text.Toon/Irihi.Text.Toon.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Irihi.Text.Toon/Nodes/ToonNode.cs b/src/Irihi.Text.Toon/Nodes/ToonNode.cs index 8be544f..87963d6 100644 --- a/src/Irihi.Text.Toon/Nodes/ToonNode.cs +++ b/src/Irihi.Text.Toon/Nodes/ToonNode.cs @@ -28,11 +28,16 @@ public abstract class ToonNode /// public string ToToonString(ToonWriterOptions? options = null) { - var bufferWriter = new ArrayBufferWriter(); + var bufferWriter = new +#if NETSTANDARD2_0 + PolyfillArrayBufferWriter(); +#else + ArrayBufferWriter(); +#endif var writer = new Utf8ToonWriter(bufferWriter, options); WriteTo(ref writer); writer.Dispose(); - return Encoding.UTF8.GetString(bufferWriter.WrittenSpan); + return EncodingPolyfill.GetUtf8String(bufferWriter.WrittenSpan); } /// @@ -52,7 +57,7 @@ public static ToonNode Parse(string toon) byte[] rented = ArrayPool.Shared.Rent(maxByteCount); try { - int written = Encoding.UTF8.GetBytes(toon, rented); + int written = EncodingPolyfill.GetUtf8Bytes(toon, rented); return Parse(rented.AsSpan(0, written)); } finally @@ -114,7 +119,7 @@ internal static ToonNode ParseValue(ref Utf8ToonReader reader, ReadOnlySpan span) // Quoted strings stay strings if (isQuoted) - return ToonValue.Create(System.Text.Encoding.UTF8.GetString(raw)); + return ToonValue.Create(EncodingPolyfill.GetUtf8String(raw)); // Detect booleans / null if (NumberConverter.IsTrue(raw)) @@ -160,7 +165,7 @@ private static ToonValue ClassifyAndCreateValue(ReadOnlySpan span) if (NumberConverter.TryParseDouble(raw, out double d)) return ToonValue.Create(d); - return ToonValue.Create(System.Text.Encoding.UTF8.GetString(raw)); + return ToonValue.Create(EncodingPolyfill.GetUtf8String(raw)); } // ── Implicit conversions ────────────────────── diff --git a/src/Irihi.Text.Toon/Nodes/ToonObject.cs b/src/Irihi.Text.Toon/Nodes/ToonObject.cs index a97a411..f32a253 100644 --- a/src/Irihi.Text.Toon/Nodes/ToonObject.cs +++ b/src/Irihi.Text.Toon/Nodes/ToonObject.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Text; +using Irihi.Text.Toon.Internal; namespace Irihi.Text.Toon.Nodes; diff --git a/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoCache.cs b/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoCache.cs index 8f5d23b..fa99e8f 100644 --- a/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoCache.cs +++ b/src/Irihi.Text.Toon/Serialization/Metadata/ToonTypeInfoCache.cs @@ -19,7 +19,11 @@ internal static class ToonTypeInfoCache internal static ToonTypeInfo GetOrAdd(Type type, ToonSerializerOptions options) { var key = new CacheKey(options, type); +#if NETSTANDARD2_0 + return _cache.GetOrAdd(key, k => ToonTypeInfoBuilder.Build(k.Type, options)); +#else return _cache.GetOrAdd(key, static (k, o) => ToonTypeInfoBuilder.Build(k.Type, o), options); +#endif } /// @@ -45,6 +49,10 @@ public override bool Equals(object? obj) => obj is CacheKey other && Equals(other); public override int GetHashCode() => +#if NETSTANDARD2_0 + unchecked(RuntimeHelpers.GetHashCode(_options) * 397 ^ _type.GetHashCode()); +#else HashCode.Combine(RuntimeHelpers.GetHashCode(_options), _type.GetHashCode()); +#endif } } diff --git a/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs b/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs index 8748f10..074b74f 100644 --- a/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs +++ b/src/Irihi.Text.Toon/Serialization/ToonSerializer.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Buffers; using System.Text; +using Irihi.Text.Toon.Internal; using Irihi.Text.Toon.Serialization; using Irihi.Text.Toon.Serialization.Metadata; @@ -17,12 +18,17 @@ public static class ToonSerializer public static string Serialize(T value, ToonSerializerOptions? options = null) { var opt = FreezeOptions(options); - var bufferWriter = new ArrayBufferWriter(); + var bufferWriter = new +#if NETSTANDARD2_0 + PolyfillArrayBufferWriter(); +#else + ArrayBufferWriter(); +#endif var wOpts = new ToonWriterOptions { MinifyOutput = opt.MinifyOutput, IndentSize = opt.IndentSize }; var writer = new Utf8ToonWriter(bufferWriter, wOpts); ToonSerializerEngine.Serialize(value, ref writer, opt); writer.Dispose(); - return Encoding.UTF8.GetString(bufferWriter.WrittenSpan); + return EncodingPolyfill.GetUtf8String(bufferWriter.WrittenSpan); } /// @@ -31,12 +37,17 @@ public static string Serialize(T value, ToonSerializerOptions? options = null public static string Serialize(object? value, Type type, ToonSerializerOptions? options = null) { var opt = FreezeOptions(options); - var bw = new ArrayBufferWriter(); + var bw = new +#if NETSTANDARD2_0 + PolyfillArrayBufferWriter(); +#else + ArrayBufferWriter(); +#endif var wOpts = new ToonWriterOptions { MinifyOutput = opt.MinifyOutput, IndentSize = opt.IndentSize }; var writer = new Utf8ToonWriter(bw, wOpts); ToonSerializerEngine.Serialize(value, type, ref writer, opt); writer.Dispose(); - return Encoding.UTF8.GetString(bw.WrittenSpan); + return EncodingPolyfill.GetUtf8String(bw.WrittenSpan); } // ── Serialize → UTF-8 bytes ──────────────────── @@ -44,7 +55,12 @@ public static string Serialize(object? value, Type type, ToonSerializerOptions? public static byte[] SerializeToUtf8Bytes(T value, ToonSerializerOptions? options = null) { var opt = FreezeOptions(options); - var bufferWriter = new ArrayBufferWriter(); + var bufferWriter = new +#if NETSTANDARD2_0 + PolyfillArrayBufferWriter(); +#else + ArrayBufferWriter(); +#endif var wOpts = new ToonWriterOptions { MinifyOutput = opt.MinifyOutput, IndentSize = opt.IndentSize }; var writer = new Utf8ToonWriter(bufferWriter, wOpts); ToonSerializerEngine.Serialize(value, ref writer, opt); @@ -91,7 +107,7 @@ public static async Task SerializeAsync( byte[] rented = ArrayPool.Shared.Rent(maxByteCount); try { - int written = Encoding.UTF8.GetBytes(toon, rented); + int written = EncodingPolyfill.GetUtf8Bytes(toon, rented); return Deserialize(rented.AsSpan(0, written), opt); } finally @@ -197,18 +213,28 @@ public static string Serialize(T value, ToonTypeInfo typeInfo) { Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(typeInfo); var opt = ToonSerializerOptions.Default; - var bw = new ArrayBufferWriter(); + var bw = new +#if NETSTANDARD2_0 + PolyfillArrayBufferWriter(); +#else + ArrayBufferWriter(); +#endif var w = new Utf8ToonWriter(bw, new ToonWriterOptions { IndentSize = 2 }); ToonSerializerEngine.Serialize(value, ref w, opt, typeInfo); w.Dispose(); - return Encoding.UTF8.GetString(bw.WrittenSpan); + return EncodingPolyfill.GetUtf8String(bw.WrittenSpan); } public static byte[] SerializeToUtf8Bytes(T value, ToonTypeInfo typeInfo) { Irihi.Text.Toon.Internal.ThrowHelper.ThrowIfNull(typeInfo); var opt = ToonSerializerOptions.Default; - var bw = new ArrayBufferWriter(); + var bw = new +#if NETSTANDARD2_0 + PolyfillArrayBufferWriter(); +#else + ArrayBufferWriter(); +#endif var w = new Utf8ToonWriter(bw, new ToonWriterOptions { IndentSize = 2 }); ToonSerializerEngine.Serialize(value, ref w, opt, typeInfo); w.Dispose(); diff --git a/src/Irihi.Text.Toon/ToonDocument.cs b/src/Irihi.Text.Toon/ToonDocument.cs index 137ea31..68c2092 100644 --- a/src/Irihi.Text.Toon/ToonDocument.cs +++ b/src/Irihi.Text.Toon/ToonDocument.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using Irihi.Text.Toon.Internal; namespace Irihi.Text.Toon; @@ -43,7 +44,7 @@ public static ToonDocument Parse(string toon, ToonDocumentOptions options = defa int written; try { - written = Encoding.UTF8.GetBytes(toon, rented); + written = EncodingPolyfill.GetUtf8Bytes(toon, rented); } catch { diff --git a/src/Irihi.Text.Toon/ToonElement.cs b/src/Irihi.Text.Toon/ToonElement.cs index 1e73de6..c991b7d 100644 --- a/src/Irihi.Text.Toon/ToonElement.cs +++ b/src/Irihi.Text.Toon/ToonElement.cs @@ -54,14 +54,14 @@ public bool TryGetProperty(string propertyName, out ToonElement value) if (byteCount <= 256) { Span buffer = stackalloc byte[byteCount]; - Encoding.UTF8.GetBytes(propertyName, buffer); + EncodingPolyfill.GetUtf8Bytes(propertyName, buffer); return TryGetProperty(buffer, out value); } byte[] rented = ArrayPool.Shared.Rent(byteCount); try { - int written = Encoding.UTF8.GetBytes(propertyName, rented); + int written = EncodingPolyfill.GetUtf8Bytes(propertyName, rented); return TryGetProperty(rented.AsSpan(0, written), out value); } finally @@ -179,7 +179,7 @@ public int GetInt32() RequireKind(node.Kind, ToonValueKind.Number); ReadOnlySpan span = _ownerDocument!.GetBuffer()[node.Start..node.End]; if (!NumberConverter.TryParseInt32(span, out int value)) - throw new InvalidOperationException($"'{Encoding.UTF8.GetString(span)}' cannot be converted to Int32."); + throw new InvalidOperationException($"'{EncodingPolyfill.GetUtf8String(span)}' cannot be converted to Int32."); return value; } @@ -190,7 +190,7 @@ public long GetInt64() RequireKind(node.Kind, ToonValueKind.Number); ReadOnlySpan span = _ownerDocument!.GetBuffer()[node.Start..node.End]; if (!NumberConverter.TryParseInt64(span, out long value)) - throw new InvalidOperationException($"'{Encoding.UTF8.GetString(span)}' cannot be converted to Int64."); + throw new InvalidOperationException($"'{EncodingPolyfill.GetUtf8String(span)}' cannot be converted to Int64."); return value; } @@ -201,7 +201,7 @@ public double GetDouble() RequireKind(node.Kind, ToonValueKind.Number); ReadOnlySpan span = _ownerDocument!.GetBuffer()[node.Start..node.End]; if (!NumberConverter.TryParseDouble(span, out double value)) - throw new InvalidOperationException($"'{Encoding.UTF8.GetString(span)}' cannot be converted to Double."); + throw new InvalidOperationException($"'{EncodingPolyfill.GetUtf8String(span)}' cannot be converted to Double."); return value; } @@ -239,7 +239,7 @@ public override string ToString() var node = Node; if (node.Start < 0 || node.End < 0 || node.End < node.Start) return string.Empty; ReadOnlySpan raw = _ownerDocument.GetBuffer()[node.Start..node.End]; - return Encoding.UTF8.GetString(raw); + return EncodingPolyfill.GetUtf8String(raw); } /// @@ -296,7 +296,7 @@ public void WriteTo(ref Utf8ToonWriter writer) ReadOnlySpan span = doc.GetBuffer()[node.Start..node.End]; if (NumberConverter.TryParseInt64(span, out long l)) writer.WriteNumberValue(l); else if (NumberConverter.TryParseDouble(span, out double d)) writer.WriteNumberValue(d); - else throw new InvalidOperationException($"'{Encoding.UTF8.GetString(span)}' is not a writable number."); + else throw new InvalidOperationException($"'{EncodingPolyfill.GetUtf8String(span)}' is not a writable number."); break; } @@ -325,7 +325,7 @@ private static bool NameEquals(ToonDocument doc, string? nameOverride, int nameS if (nameOverride != null) { Span nameBytes = stackalloc byte[Encoding.UTF8.GetByteCount(nameOverride)]; - Encoding.UTF8.GetBytes(nameOverride, nameBytes); + EncodingPolyfill.GetUtf8Bytes(nameOverride, nameBytes); return caseInsensitive ? AsciiEqualsIgnoreCase(nameBytes, utf8Name) : nameBytes.SequenceEqual(utf8Name); } @@ -369,6 +369,6 @@ private static string DecodeUtf8Value(ReadOnlySpan buffer, int start, int Span unescapeBuffer = stripped.Length <= 256 ? stackalloc byte[stripped.Length] : new byte[stripped.Length]; int written = ToonEscapeHelper.Unescape(stripped, unescapeBuffer); ReadOnlySpan decoded = written < 0 ? stripped : unescapeBuffer[..written]; - return Encoding.UTF8.GetString(decoded); + return EncodingPolyfill.GetUtf8String(decoded); } } diff --git a/src/Irihi.Text.Toon/Utf8ToonReader.Sequence.cs b/src/Irihi.Text.Toon/Utf8ToonReader.Sequence.cs index a6fb46b..462f533 100644 --- a/src/Irihi.Text.Toon/Utf8ToonReader.Sequence.cs +++ b/src/Irihi.Text.Toon/Utf8ToonReader.Sequence.cs @@ -11,7 +11,7 @@ public ref partial struct Utf8ToonReader /// responsible for returning the buffer. /// public Utf8ToonReader(ReadOnlySequence sequence, ToonReaderOptions? options = null) - : this(sequence.IsSingleSegment ? sequence.FirstSpan : sequence.ToArray(), options) + : this(sequence.IsSingleSegment ? sequence.First.Span : sequence.ToArray(), options) { } } diff --git a/src/Irihi.Text.Toon/Utf8ToonReader.cs b/src/Irihi.Text.Toon/Utf8ToonReader.cs index 64de01e..dbd1eff 100644 --- a/src/Irihi.Text.Toon/Utf8ToonReader.cs +++ b/src/Irihi.Text.Toon/Utf8ToonReader.cs @@ -66,7 +66,7 @@ public unsafe Utf8ToonReader(ReadOnlySpan data, ToonReaderOptions? options _line = new LineState { Start = 0, End = 0, Indent = 0, Consumed = true }; _arr = default; _stackBuffer = default; - _scopeStack = MemoryMarshal.CreateSpan(ref Unsafe.As(ref _stackBuffer.Data[0]), opt.MaxDepth); + _scopeStack = MemoryMarshalShim.CreateSpan(ref Unsafe.As(ref _stackBuffer.Data[0]), opt.MaxDepth); } public readonly ToonTokenType TokenType => _tokenType; @@ -420,7 +420,7 @@ private bool ParseArrayHeader(ReadOnlySpan keySpan, int bracketPos, ReadOn // Spec §6: delimiter declared in header MUST match field list if (_mode == ToonReaderMode.Strict && delim != (byte)',') { - if (fs.Contains((byte)',')) + if (fs.IndexOf((byte)',') >= 0) throw new ToonReaderException( $"Delimiter mismatch: header declares '{ToonUtf8Constants.DelimiterName(delim)}' but field list uses comma.", _lineNumber, _columnNumber); @@ -451,7 +451,7 @@ private static string[] ParseTabularFields(ReadOnlySpan fields, byte delim var list = new List(); int s = 0; for (int i = 0; i <= fields.Length; i++) - if (i == fields.Length || fields[i] == delim) { var f = Trim(fields[s..i]); list.Add(System.Text.Encoding.UTF8.GetString(f)); s = i + 1; } + if (i == fields.Length || fields[i] == delim) { var f = Trim(fields[s..i]); list.Add(EncodingPolyfill.GetUtf8String(f)); s = i + 1; } return list.ToArray(); } @@ -531,7 +531,7 @@ public void Skip() { if (_tokenType == ToonTokenType.StartObject || _tokenType == ToonTokenType.StartArray) { int t = _currentDepth - 1; while (Read()) if (_currentDepth <= t) return; } } public string GetString() - { var raw = StripQuotes(_tokenValueSpan); Span buf = stackalloc byte[raw.Length]; int w = ToonEscapeHelper.Unescape(raw, buf); if (w < 0) w = raw.Length; return System.Text.Encoding.UTF8.GetString(buf[..w]); } + { var raw = StripQuotes(_tokenValueSpan); Span buf = stackalloc byte[raw.Length]; int w = ToonEscapeHelper.Unescape(raw, buf); if (w < 0) w = raw.Length; return EncodingPolyfill.GetUtf8String(buf[..w]); } public int GetInt32() { if (!NumberConverter.TryParseInt32(_tokenValueSpan, out int v)) throw new ToonReaderException($"Cannot convert '{GetString()}' to Int32.", _lineNumber, _columnNumber); return v; } diff --git a/src/Irihi.Text.Toon/Utf8ToonWriter.cs b/src/Irihi.Text.Toon/Utf8ToonWriter.cs index 2fb01bd..d25583e 100644 --- a/src/Irihi.Text.Toon/Utf8ToonWriter.cs +++ b/src/Irihi.Text.Toon/Utf8ToonWriter.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Buffers; using System.Buffers.Text; +using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -52,7 +53,7 @@ public unsafe Utf8ToonWriter(IBufferWriter bufferWriter, ToonWriterOptions _inTableRow = false; _rowCellCount = 0; _isDisposed = false; - _scopeStack = MemoryMarshal.CreateSpan( + _scopeStack = MemoryMarshalShim.CreateSpan( ref Unsafe.As(ref _scopeStackBuf.Data[0]), 64); } @@ -217,7 +218,7 @@ public void WriteStringValue(ReadOnlySpan value) { AssertNotDisposed(); Span buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(value.Length)]; - int written = Encoding.UTF8.GetBytes(value, buffer); + int written = EncodingPolyfill.GetUtf8Bytes(value, buffer); BeforeArrayValue(); BeforeTableCell(); for (int i = 0; i < written; i++) @@ -322,6 +323,11 @@ public void WriteNumberValue(decimal value) BeforeArrayValue(); BeforeTableCell(); Span tmp = stackalloc byte[32]; +#if NETSTANDARD2_0 + var str = value.ToString(CultureInfo.InvariantCulture); + int written = Encoding.UTF8.GetBytes(str, 0, str.Length, tmp.ToArray(), 0); + for (int i = 0; i < written; i++) WriteByte(tmp[i]); +#else if (value.TryFormat(tmp, out int written)) { for (int i = 0; i < written; i++) WriteByte(tmp[i]); @@ -330,6 +336,7 @@ public void WriteNumberValue(decimal value) { WriteBytes(ToonUtf8Constants.NullLiteral); } +#endif if (_inTableRow) _rowCellCount++; } From 877d2317d5ca63948e3701c11eff4df55b4daa53 Mon Sep 17 00:00:00 2001 From: Dong Bin Date: Tue, 28 Jul 2026 14:12:14 +0800 Subject: [PATCH 4/4] perf: add AggressiveInlining to polyfill forwarding methods EncodingPolyfill, MemoryMarshalShim, KvpExtensions, ThrowHelper --- src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs | 4 ++++ src/Irihi.Text.Toon/Internal/KvpExtensions.cs | 2 ++ src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs | 1 + src/Irihi.Text.Toon/Internal/ThrowHelper.cs | 1 + 4 files changed, 8 insertions(+) diff --git a/src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs b/src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs index 0b7ea91..c1eed3c 100644 --- a/src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs +++ b/src/Irihi.Text.Toon/Internal/EncodingPolyfill.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text; namespace Irihi.Text.Toon.Internal; @@ -5,6 +6,7 @@ namespace Irihi.Text.Toon.Internal; /// Polyfill for Encoding.UTF8 span-based APIs on netstandard2.0. internal static class EncodingPolyfill { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetUtf8String(ReadOnlySpan bytes) { #if NETSTANDARD2_0 @@ -15,6 +17,7 @@ public static string GetUtf8String(ReadOnlySpan bytes) #endif } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetUtf8Bytes(string value, Span destination) { #if NETSTANDARD2_0 @@ -26,6 +29,7 @@ public static int GetUtf8Bytes(string value, Span destination) #endif } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetUtf8Bytes(ReadOnlySpan value, Span destination) { #if NETSTANDARD2_0 diff --git a/src/Irihi.Text.Toon/Internal/KvpExtensions.cs b/src/Irihi.Text.Toon/Internal/KvpExtensions.cs index b8845c3..79d1931 100644 --- a/src/Irihi.Text.Toon/Internal/KvpExtensions.cs +++ b/src/Irihi.Text.Toon/Internal/KvpExtensions.cs @@ -1,12 +1,14 @@ #if NETSTANDARD2_0 using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Irihi.Text.Toon.Internal; /// Polyfill Deconstruct for on netstandard2.0. internal static class KvpExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; diff --git a/src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs b/src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs index c3a769d..420a63d 100644 --- a/src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs +++ b/src/Irihi.Text.Toon/Internal/MemoryMarshalShim.cs @@ -6,6 +6,7 @@ namespace Irihi.Text.Toon.Internal; /// Polyfill for on netstandard2.0. internal static class MemoryMarshalShim { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe Span CreateSpan(ref T reference, int length) { #if NETSTANDARD2_0 diff --git a/src/Irihi.Text.Toon/Internal/ThrowHelper.cs b/src/Irihi.Text.Toon/Internal/ThrowHelper.cs index 637690c..0bce18b 100644 --- a/src/Irihi.Text.Toon/Internal/ThrowHelper.cs +++ b/src/Irihi.Text.Toon/Internal/ThrowHelper.cs @@ -7,6 +7,7 @@ namespace Irihi.Text.Toon.Internal; internal static class ThrowHelper { /// Throws if is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowIfNull( [NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)