Skip to content

Commit c8f6012

Browse files
authored
Remove span pinning associated with string.Create (#78914)
* Remove span pinning associated with string.Create This is now possible with C# 11. * Address PR feedback
1 parent eb8d5f7 commit c8f6012

File tree

9 files changed

+170
-191
lines changed

9 files changed

+170
-191
lines changed

src/libraries/Common/src/System/HexConverter.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,10 @@ public static unsafe string ToString(ReadOnlySpan<byte> bytes, Casing casing = C
208208
}
209209
return result.ToString();
210210
#else
211-
fixed (byte* bytesPtr = bytes)
212-
{
213-
return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length, casing), static (chars, args) =>
214-
{
215-
var ros = new ReadOnlySpan<byte>((byte*)args.Ptr, args.Length);
216-
EncodeToUtf16(ros, chars, args.casing);
217-
});
218-
}
211+
#pragma warning disable CS8500 // takes address of managed type
212+
return string.Create(bytes.Length * 2, (RosPtr: (IntPtr)(&bytes), casing), static (chars, args) =>
213+
EncodeToUtf16(*(ReadOnlySpan<byte>*)args.RosPtr, chars, args.casing));
214+
#pragma warning restore CS8500
219215
#endif
220216
}
221217

src/libraries/Common/src/System/IO/Archiving.Utils.Windows.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,22 @@ public static unsafe string EntryFromPath(ReadOnlySpan<char> path, bool appendPa
6161
string.Empty;
6262
}
6363

64-
fixed (char* pathPtr = &MemoryMarshal.GetReference(path))
64+
#pragma warning disable CS8500 // takes address of managed type
65+
ReadOnlySpan<char> tmpPath = path; // avoid address exposing the span and impacting the other code in the method that uses it
66+
return string.Create(appendPathSeparator ? tmpPath.Length + 1 : tmpPath.Length, (appendPathSeparator, RosPtr: (IntPtr)(&tmpPath)), static (dest, state) =>
6567
{
66-
return string.Create(appendPathSeparator ? path.Length + 1 : path.Length, (appendPathSeparator, (IntPtr)pathPtr, path.Length), static (dest, state) =>
68+
var path = *(ReadOnlySpan<char>*)state.RosPtr;
69+
path.CopyTo(dest);
70+
if (state.appendPathSeparator)
6771
{
68-
ReadOnlySpan<char> path = new ReadOnlySpan<char>((char*)state.Item2, state.Length);
69-
path.CopyTo(dest);
70-
if (state.appendPathSeparator)
71-
{
72-
dest[^1] = '/';
73-
}
72+
dest[^1] = '/';
73+
}
7474

75-
// To ensure tar files remain compatible with Unix, and per the ZIP File Format Specification 4.4.17.1,
76-
// all slashes should be forward slashes.
77-
dest.Replace('\\', '/');
78-
});
79-
}
75+
// To ensure tar files remain compatible with Unix, and per the ZIP File Format Specification 4.4.17.1,
76+
// all slashes should be forward slashes.
77+
dest.Replace('\\', '/');
78+
});
79+
#pragma warning restore CS8500
8080
}
8181
}
8282
}

src/libraries/System.Private.CoreLib/src/System/IO/Path.cs

Lines changed: 64 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -686,132 +686,91 @@ private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan
686686
{
687687
Debug.Assert(first.Length > 0 && second.Length > 0, "should have dealt with empty paths");
688688

689-
bool hasSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
690-
|| PathInternal.IsDirectorySeparator(second[0]);
689+
bool hasSeparator = PathInternal.IsDirectorySeparator(first[^1]) || PathInternal.IsDirectorySeparator(second[0]);
691690

692691
return hasSeparator ?
693692
string.Concat(first, second) :
694693
string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second);
695694
}
696695

697-
private readonly unsafe struct Join3Payload
698-
{
699-
public Join3Payload(char* first, int firstLength, char* second, int secondLength, char* third, int thirdLength, byte separators)
700-
{
701-
First = first;
702-
FirstLength = firstLength;
703-
Second = second;
704-
SecondLength = secondLength;
705-
Third = third;
706-
ThirdLength = thirdLength;
707-
Separators = separators;
708-
}
709-
710-
public readonly char* First;
711-
public readonly int FirstLength;
712-
public readonly char* Second;
713-
public readonly int SecondLength;
714-
public readonly char* Third;
715-
public readonly int ThirdLength;
716-
public readonly byte Separators;
717-
}
718-
719696
private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third)
720697
{
721698
Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0, "should have dealt with empty paths");
722699

723-
byte firstNeedsSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
724-
|| PathInternal.IsDirectorySeparator(second[0]) ? (byte)0 : (byte)1;
725-
byte secondNeedsSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
726-
|| PathInternal.IsDirectorySeparator(third[0]) ? (byte)0 : (byte)1;
700+
bool firstHasSeparator = PathInternal.IsDirectorySeparator(first[^1]) || PathInternal.IsDirectorySeparator(second[0]);
701+
bool secondHasSeparator = PathInternal.IsDirectorySeparator(second[^1]) || PathInternal.IsDirectorySeparator(third[0]);
727702

728-
fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third))
703+
return (firstHasSeparator, secondHasSeparator) switch
729704
{
730-
var payload = new Join3Payload(
731-
f, first.Length, s, second.Length, t, third.Length,
732-
(byte)(firstNeedsSeparator | secondNeedsSeparator << 1));
733-
734-
return string.Create(
735-
first.Length + second.Length + third.Length + firstNeedsSeparator + secondNeedsSeparator,
736-
(IntPtr)(&payload),
737-
static (destination, statePtr) =>
738-
{
739-
ref Join3Payload state = ref *(Join3Payload*)statePtr;
740-
new Span<char>(state.First, state.FirstLength).CopyTo(destination);
741-
if ((state.Separators & 0b1) != 0)
742-
destination[state.FirstLength] = PathInternal.DirectorySeparatorChar;
743-
new Span<char>(state.Second, state.SecondLength).CopyTo(destination.Slice(state.FirstLength + (state.Separators & 0b1)));
744-
if ((state.Separators & 0b10) != 0)
745-
destination[destination.Length - state.ThirdLength - 1] = PathInternal.DirectorySeparatorChar;
746-
new Span<char>(state.Third, state.ThirdLength).CopyTo(destination.Slice(destination.Length - state.ThirdLength));
747-
});
748-
}
705+
(false, false) => string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second, PathInternal.DirectorySeparatorCharAsString, third),
706+
(false, true) => string.Concat(first, PathInternal.DirectorySeparatorCharAsString, second, third),
707+
(true, false) => string.Concat(first, second, PathInternal.DirectorySeparatorCharAsString, third),
708+
(true, true) => string.Concat(first, second, third),
709+
};
749710
}
750711

751-
private readonly unsafe struct Join4Payload
712+
private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
752713
{
753-
public Join4Payload(char* first, int firstLength, char* second, int secondLength, char* third, int thirdLength, char* fourth, int fourthLength, byte separators)
714+
Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths");
715+
716+
#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
717+
var state = new JoinInternalState
754718
{
755-
First = first;
756-
FirstLength = firstLength;
757-
Second = second;
758-
SecondLength = secondLength;
759-
Third = third;
760-
ThirdLength = thirdLength;
761-
Fourth = fourth;
762-
FourthLength = fourthLength;
763-
Separators = separators;
764-
}
719+
ReadOnlySpanPtr1 = (IntPtr)(&first),
720+
ReadOnlySpanPtr2 = (IntPtr)(&second),
721+
ReadOnlySpanPtr3 = (IntPtr)(&third),
722+
ReadOnlySpanPtr4 = (IntPtr)(&fourth),
723+
NeedSeparator1 = PathInternal.IsDirectorySeparator(first[^1]) || PathInternal.IsDirectorySeparator(second[0]) ? (byte)0 : (byte)1,
724+
NeedSeparator2 = PathInternal.IsDirectorySeparator(second[^1]) || PathInternal.IsDirectorySeparator(third[0]) ? (byte)0 : (byte)1,
725+
NeedSeparator3 = PathInternal.IsDirectorySeparator(third[^1]) || PathInternal.IsDirectorySeparator(fourth[0]) ? (byte)0 : (byte)1,
726+
};
765727

766-
public readonly char* First;
767-
public readonly int FirstLength;
768-
public readonly char* Second;
769-
public readonly int SecondLength;
770-
public readonly char* Third;
771-
public readonly int ThirdLength;
772-
public readonly char* Fourth;
773-
public readonly int FourthLength;
774-
public readonly byte Separators;
775-
}
728+
return string.Create(
729+
first.Length + second.Length + third.Length + fourth.Length + state.NeedSeparator1 + state.NeedSeparator2 + state.NeedSeparator3,
730+
state,
731+
static (destination, state) =>
732+
{
733+
ReadOnlySpan<char> first = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr1;
734+
first.CopyTo(destination);
735+
destination = destination.Slice(first.Length);
776736

777-
private static unsafe string JoinInternal(ReadOnlySpan<char> first, ReadOnlySpan<char> second, ReadOnlySpan<char> third, ReadOnlySpan<char> fourth)
778-
{
779-
Debug.Assert(first.Length > 0 && second.Length > 0 && third.Length > 0 && fourth.Length > 0, "should have dealt with empty paths");
737+
if (state.NeedSeparator1 != 0)
738+
{
739+
destination[0] = PathInternal.DirectorySeparatorChar;
740+
destination = destination.Slice(1);
741+
}
780742

781-
byte firstNeedsSeparator = PathInternal.IsDirectorySeparator(first[first.Length - 1])
782-
|| PathInternal.IsDirectorySeparator(second[0]) ? (byte)0 : (byte)1;
783-
byte secondNeedsSeparator = PathInternal.IsDirectorySeparator(second[second.Length - 1])
784-
|| PathInternal.IsDirectorySeparator(third[0]) ? (byte)0 : (byte)1;
785-
byte thirdNeedsSeparator = PathInternal.IsDirectorySeparator(third[third.Length - 1])
786-
|| PathInternal.IsDirectorySeparator(fourth[0]) ? (byte)0 : (byte)1;
743+
ReadOnlySpan<char> second = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr2;
744+
second.CopyTo(destination);
745+
destination = destination.Slice(second.Length);
787746

788-
fixed (char* f = &MemoryMarshal.GetReference(first), s = &MemoryMarshal.GetReference(second), t = &MemoryMarshal.GetReference(third), u = &MemoryMarshal.GetReference(fourth))
789-
{
790-
var payload = new Join4Payload(
791-
f, first.Length, s, second.Length, t, third.Length, u, fourth.Length,
792-
(byte)(firstNeedsSeparator | secondNeedsSeparator << 1 | thirdNeedsSeparator << 2));
793-
794-
return string.Create(
795-
first.Length + second.Length + third.Length + fourth.Length + firstNeedsSeparator + secondNeedsSeparator + thirdNeedsSeparator,
796-
(IntPtr)(&payload),
797-
static (destination, statePtr) =>
747+
if (state.NeedSeparator2 != 0)
798748
{
799-
ref Join4Payload state = ref *(Join4Payload*)statePtr;
800-
new Span<char>(state.First, state.FirstLength).CopyTo(destination);
801-
int insertionPoint = state.FirstLength;
802-
if ((state.Separators & 0b1) != 0)
803-
destination[insertionPoint++] = PathInternal.DirectorySeparatorChar;
804-
new Span<char>(state.Second, state.SecondLength).CopyTo(destination.Slice(insertionPoint));
805-
insertionPoint += state.SecondLength;
806-
if ((state.Separators & 0b10) != 0)
807-
destination[insertionPoint++] = PathInternal.DirectorySeparatorChar;
808-
new Span<char>(state.Third, state.ThirdLength).CopyTo(destination.Slice(insertionPoint));
809-
insertionPoint += state.ThirdLength;
810-
if ((state.Separators & 0b100) != 0)
811-
destination[insertionPoint++] = PathInternal.DirectorySeparatorChar;
812-
new Span<char>(state.Fourth, state.FourthLength).CopyTo(destination.Slice(insertionPoint));
813-
});
814-
}
749+
destination[0] = PathInternal.DirectorySeparatorChar;
750+
destination = destination.Slice(1);
751+
}
752+
753+
ReadOnlySpan<char> third = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr3;
754+
third.CopyTo(destination);
755+
destination = destination.Slice(third.Length);
756+
757+
if (state.NeedSeparator3 != 0)
758+
{
759+
destination[0] = PathInternal.DirectorySeparatorChar;
760+
destination = destination.Slice(1);
761+
}
762+
763+
ReadOnlySpan<char> fourth = *(ReadOnlySpan<char>*)state.ReadOnlySpanPtr4;
764+
Debug.Assert(fourth.Length == destination.Length);
765+
fourth.CopyTo(destination);
766+
});
767+
#pragma warning restore CS8500
768+
}
769+
770+
private struct JoinInternalState // used to avoid rooting ValueTuple`7
771+
{
772+
public IntPtr ReadOnlySpanPtr1, ReadOnlySpanPtr2, ReadOnlySpanPtr3, ReadOnlySpanPtr4;
773+
public byte NeedSeparator1, NeedSeparator2, NeedSeparator3;
815774
}
816775

817776
private static ReadOnlySpan<byte> Base32Char => "abcdefghijklmnopqrstuvwxyz012345"u8;

src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,34 @@ public static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, Re
379379
return result;
380380
}
381381

382+
internal static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, ReadOnlySpan<char> str2, ReadOnlySpan<char> str3, ReadOnlySpan<char> str4)
383+
{
384+
int length = checked(str0.Length + str1.Length + str2.Length + str3.Length + str4.Length);
385+
if (length == 0)
386+
{
387+
return Empty;
388+
}
389+
390+
string result = FastAllocateString(length);
391+
Span<char> resultSpan = new Span<char>(ref result._firstChar, result.Length);
392+
393+
str0.CopyTo(resultSpan);
394+
resultSpan = resultSpan.Slice(str0.Length);
395+
396+
str1.CopyTo(resultSpan);
397+
resultSpan = resultSpan.Slice(str1.Length);
398+
399+
str2.CopyTo(resultSpan);
400+
resultSpan = resultSpan.Slice(str2.Length);
401+
402+
str3.CopyTo(resultSpan);
403+
resultSpan = resultSpan.Slice(str3.Length);
404+
405+
str4.CopyTo(resultSpan);
406+
407+
return result;
408+
}
409+
382410
public static string Concat(params string?[] values)
383411
{
384412
ArgumentNullException.ThrowIfNull(values);

src/libraries/System.Private.Uri/src/System/Uri.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3840,15 +3840,14 @@ private static unsafe ParsingError CheckSchemeSyntax(ReadOnlySpan<char> span, re
38403840
}
38413841

38423842
// Then look up the syntax in a string-based table.
3843-
string str;
3844-
fixed (char* pSpan = span)
3843+
#pragma warning disable CS8500 // takes address of managed type
3844+
ReadOnlySpan<char> tmpSpan = span; // avoid address exposing the span and impacting the other code in the method that uses it
3845+
string str = string.Create(tmpSpan.Length, (IntPtr)(&tmpSpan), (buffer, spanPtr) =>
38453846
{
3846-
str = string.Create(span.Length, (ip: (IntPtr)pSpan, length: span.Length), (buffer, state) =>
3847-
{
3848-
int charsWritten = new ReadOnlySpan<char>((char*)state.ip, state.length).ToLowerInvariant(buffer);
3849-
Debug.Assert(charsWritten == buffer.Length);
3850-
});
3851-
}
3847+
int charsWritten = (*(ReadOnlySpan<char>*)spanPtr).ToLowerInvariant(buffer);
3848+
Debug.Assert(charsWritten == buffer.Length);
3849+
});
3850+
#pragma warning restore CS8500
38523851
syntax = UriParser.FindOrFetchAsUnknownV1Syntax(str);
38533852
return ParsingError.None;
38543853
}

src/libraries/System.Private.Uri/src/System/UriHelper.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -543,22 +543,21 @@ internal static unsafe string StripBidiControlCharacters(ReadOnlySpan<char> strT
543543
return backingString ?? new string(strToClean);
544544
}
545545

546-
fixed (char* pStrToClean = &MemoryMarshal.GetReference(strToClean))
546+
#pragma warning disable CS8500 // takes address of managed type
547+
ReadOnlySpan<char> tmpStrToClean = strToClean; // avoid address exposing the span and impacting the other code in the method that uses it
548+
return string.Create(tmpStrToClean.Length - charsToRemove, (IntPtr)(&tmpStrToClean), static (buffer, strToCleanPtr) =>
547549
{
548-
return string.Create(strToClean.Length - charsToRemove, (StrToClean: (IntPtr)pStrToClean, strToClean.Length), static (buffer, state) =>
550+
int destIndex = 0;
551+
foreach (char c in *(ReadOnlySpan<char>*)strToCleanPtr)
549552
{
550-
var strToClean = new ReadOnlySpan<char>((char*)state.StrToClean, state.Length);
551-
int destIndex = 0;
552-
foreach (char c in strToClean)
553+
if (!IsBidiControlCharacter(c))
553554
{
554-
if (!IsBidiControlCharacter(c))
555-
{
556-
buffer[destIndex++] = c;
557-
}
555+
buffer[destIndex++] = c;
558556
}
559-
Debug.Assert(buffer.Length == destIndex);
560-
});
561-
}
557+
}
558+
Debug.Assert(buffer.Length == destIndex);
559+
});
560+
#pragma warning restore CS8500
562561
}
563562
}
564563
}

0 commit comments

Comments
 (0)