Skip to content

Commit 6eb2b9b

Browse files
Copilotjkotas
andauthored
Add SpanOfCharAsUtf8StringMarshaller and remove ValueUtf8Converter (#121870)
Make sure that the thread-local P/Invoke last error is not getting corrupted by marshalling post-actions. Fixes #121868 --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: jkotas <[email protected]> Co-authored-by: Jan Kotas <[email protected]>
1 parent c582fa2 commit 6eb2b9b

File tree

13 files changed

+99
-115
lines changed

13 files changed

+99
-115
lines changed

src/libraries/Common/src/Interop/Unix/System.Native/Interop.MkDir.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,13 @@
33

44
using System;
55
using System.Runtime.InteropServices;
6-
using System.Text;
6+
using System.Runtime.InteropServices.Marshalling;
77

88
internal static partial class Interop
99
{
1010
internal static partial class Sys
1111
{
1212
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_MkDir", SetLastError = true)]
13-
private static partial int MkDir(ref byte path, int mode);
14-
15-
internal static int MkDir(ReadOnlySpan<char> path, int mode)
16-
{
17-
using ValueUtf8Converter converter = new(stackalloc byte[DefaultPathBufferSize]);
18-
int result = MkDir(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), mode);
19-
return result;
20-
}
13+
internal static partial int MkDir([MarshalUsing(typeof(SpanOfCharAsUtf8StringMarshaller))] ReadOnlySpan<char> path, int mode);
2114
}
2215
}

src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Buffers;
66
using System.Runtime.InteropServices;
7+
using System.Runtime.InteropServices.Marshalling;
78
using System.Text;
89

910
internal static partial class Interop
@@ -21,7 +22,7 @@ internal static partial class Sys
2122
/// Returns the number of bytes placed into the buffer on success; bufferSize if the buffer is too small; and -1 on error.
2223
/// </returns>
2324
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadLink", SetLastError = true)]
24-
private static partial int ReadLink(ref byte path, ref byte buffer, int bufferSize);
25+
private static partial int ReadLink([MarshalUsing(typeof(SpanOfCharAsUtf8StringMarshaller))] ReadOnlySpan<char> path, ref byte buffer, int bufferSize);
2526

2627
/// <summary>
2728
/// Takes a path to a symbolic link and returns the link target path.
@@ -32,18 +33,14 @@ internal static partial class Sys
3233
{
3334
const int StackBufferSize = 256;
3435

35-
// Use an initial buffer size that prevents disposing and renting
36-
// a second time when calling ConvertAndTerminateString.
37-
using var converter = new ValueUtf8Converter(stackalloc byte[StackBufferSize]);
3836
Span<byte> spanBuffer = stackalloc byte[StackBufferSize];
3937
byte[]? arrayBuffer = null;
40-
ref byte pathReference = ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path));
4138
while (true)
4239
{
4340
int error = 0;
4441
try
4542
{
46-
int resultLength = ReadLink(ref pathReference, ref MemoryMarshal.GetReference(spanBuffer), spanBuffer.Length);
43+
int resultLength = ReadLink(path, ref MemoryMarshal.GetReference(spanBuffer), spanBuffer.Length);
4744

4845
if (resultLength < 0)
4946
{

src/libraries/Common/src/Interop/Unix/System.Native/Interop.Rename.cs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using System;
55
using System.Runtime.InteropServices;
6-
using System.Text;
6+
using System.Runtime.InteropServices.Marshalling;
77

88
internal static partial class Interop
99
{
@@ -17,22 +17,7 @@ internal static partial class Sys
1717
/// <returns>
1818
/// Returns 0 on success; otherwise, returns -1
1919
/// </returns>
20-
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Rename", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)]
21-
internal static partial int Rename(string oldPath, string newPath);
22-
2320
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Rename", SetLastError = true)]
24-
internal static partial int Rename(ref byte oldPath, ref byte newPath);
25-
26-
internal static int Rename(ReadOnlySpan<char> oldPath, ReadOnlySpan<char> newPath)
27-
{
28-
ValueUtf8Converter converterNewPath = new(stackalloc byte[DefaultPathBufferSize]);
29-
ValueUtf8Converter converterOldPath = new(stackalloc byte[DefaultPathBufferSize]);
30-
int result = Rename(
31-
ref MemoryMarshal.GetReference(converterOldPath.ConvertAndTerminateString(oldPath)),
32-
ref MemoryMarshal.GetReference(converterNewPath.ConvertAndTerminateString(newPath)));
33-
converterNewPath.Dispose();
34-
converterOldPath.Dispose();
35-
return result;
36-
}
21+
internal static partial int Rename([MarshalUsing(typeof(SpanOfCharAsUtf8StringMarshaller))] ReadOnlySpan<char> oldPath, [MarshalUsing(typeof(SpanOfCharAsUtf8StringMarshaller))] ReadOnlySpan<char> newPath);
3722
}
3823
}

src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,16 @@
33

44
using System;
55
using System.Runtime.InteropServices;
6-
using System.Text;
6+
using System.Runtime.InteropServices.Marshalling;
77

88
internal static partial class Interop
99
{
1010
internal static partial class Sys
1111
{
1212
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Stat", SetLastError = true)]
13-
internal static partial int Stat(ref byte path, out FileStatus output);
14-
15-
internal static int Stat(ReadOnlySpan<char> path, out FileStatus output)
16-
{
17-
var converter = new ValueUtf8Converter(stackalloc byte[DefaultPathBufferSize]);
18-
int result = Stat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output);
19-
converter.Dispose();
20-
return result;
21-
}
13+
internal static partial int Stat([MarshalUsing(typeof(SpanOfCharAsUtf8StringMarshaller))] ReadOnlySpan<char> path, out FileStatus output);
2214

2315
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_LStat", SetLastError = true)]
24-
internal static partial int LStat(ref byte path, out FileStatus output);
25-
26-
internal static int LStat(ReadOnlySpan<char> path, out FileStatus output)
27-
{
28-
var converter = new ValueUtf8Converter(stackalloc byte[DefaultPathBufferSize]);
29-
int result = LStat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output);
30-
converter.Dispose();
31-
return result;
32-
}
16+
internal static partial int LStat([MarshalUsing(typeof(SpanOfCharAsUtf8StringMarshaller))] ReadOnlySpan<char> path, out FileStatus output);
3317
}
3418
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.CompilerServices;
5+
using System.Text;
6+
7+
namespace System.Runtime.InteropServices.Marshalling
8+
{
9+
/// <summary>
10+
/// Marshaller for ReadOnlySpan&lt;char&gt; to UTF-8 strings.
11+
/// </summary>
12+
[CustomMarshaller(typeof(ReadOnlySpan<char>), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
13+
internal static unsafe class SpanOfCharAsUtf8StringMarshaller
14+
{
15+
/// <summary>
16+
/// Custom marshaller to marshal a ReadOnlySpan&lt;char&gt; as a UTF-8 unmanaged string.
17+
/// </summary>
18+
internal ref struct ManagedToUnmanagedIn
19+
{
20+
/// <summary>
21+
/// Gets the requested buffer size for optimized marshalling.
22+
/// </summary>
23+
public static int BufferSize => 0x100;
24+
25+
private byte* _unmanagedValue;
26+
private bool _allocated;
27+
28+
/// <summary>
29+
/// Initializes the marshaller with a ReadOnlySpan&lt;char&gt; and requested buffer.
30+
/// </summary>
31+
/// <param name="managed">The managed ReadOnlySpan&lt;char&gt; with which to initialize the marshaller.</param>
32+
/// <param name="buffer">The request buffer whose size is at least <see cref="BufferSize"/>.</param>
33+
public void FromManaged(ReadOnlySpan<char> managed, Span<byte> buffer)
34+
{
35+
_allocated = false;
36+
37+
const int MaxUtf8BytesPerChar = 3;
38+
39+
// >= for null terminator
40+
// Use the cast to long to avoid the checked operation
41+
if ((long)MaxUtf8BytesPerChar * managed.Length >= buffer.Length)
42+
{
43+
// Calculate accurate byte count when the provided stack-allocated buffer is not sufficient
44+
int exactByteCount = checked(Encoding.UTF8.GetByteCount(managed) + 1); // + 1 for null terminator
45+
if (exactByteCount > buffer.Length)
46+
{
47+
buffer = new Span<byte>((byte*)NativeMemory.Alloc((nuint)exactByteCount), exactByteCount);
48+
_allocated = true;
49+
}
50+
}
51+
52+
// Unsafe.AsPointer is safe since buffer must be pinned
53+
_unmanagedValue = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer));
54+
55+
int byteCount = Encoding.UTF8.GetBytes(managed, buffer);
56+
buffer[byteCount] = 0; // null-terminate
57+
}
58+
59+
/// <summary>
60+
/// Converts the current managed ReadOnlySpan&lt;char&gt; to an unmanaged string.
61+
/// </summary>
62+
/// <returns>An unmanaged string.</returns>
63+
public byte* ToUnmanaged() => _unmanagedValue;
64+
65+
/// <summary>
66+
/// Frees any allocated unmanaged memory.
67+
/// </summary>
68+
public void Free()
69+
{
70+
if (_allocated)
71+
NativeMemory.Free(_unmanagedValue);
72+
}
73+
}
74+
}
75+
}

src/libraries/Common/src/System/Text/ValueUtf8Converter.cs

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@
273273
Link="Common\Interop\Unix\Interop.ResourceLimits.cs" />
274274
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.PathConf.cs"
275275
Link="Common\Interop\Unix\Interop.PathConf.cs" />
276-
<Compile Include="$(CommonPath)System\Text\ValueUtf8Converter.cs"
277-
Link="Common\System\Text\ValueUtf8Converter.cs" />
276+
<Compile Include="$(CommonPath)System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs"
277+
Link="Common\System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs" />
278278
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.WaitId.cs"
279279
Link="Common\Interop\Unix\Interop.WaitId.cs" />
280280
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.WaitPid.cs"

src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@
115115
Link="Common\Microsoft\Win32\SafeHandles\SafeCreateHandle.OSX.cs" />
116116
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeEventStreamHandle.OSX.cs"
117117
Link="Common\Microsoft\Win32\SafeHandles\SafeEventStreamHandle.OSX.cs" />
118-
<Compile Include="$(CommonPath)System\Text\ValueUtf8Converter.cs"
119-
Link="Common\System\Text\ValueUtf8Converter.cs" />
118+
<Compile Include="$(CommonPath)System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs"
119+
Link="Common\System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs" />
120120
</ItemGroup>
121121

122122
<ItemGroup>

src/libraries/System.Net.Ping/src/System.Net.Ping.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
Link="Common\Interop\Unix\System.Native\Interop.Socket.cs" />
6969
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.SocketAddress.cs"
7070
Link="Common\Interop\Unix\System.Native\Interop.SocketAddress.cs" />
71-
<Compile Include="$(CommonPath)System\Text\ValueUtf8Converter.cs"
72-
Link="Common\System\Text\ValueUtf8Converter.cs" />
71+
<Compile Include="$(CommonPath)System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs"
72+
Link="Common\System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs" />
7373
</ItemGroup>
7474

7575
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows' and '$(IsApplePlatform)' != 'true'">

src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
Link="Common\System\Net\NetworkInformation\UnixCommandLinePing.cs" />
3737
<Compile Include="$(CommonTestPath)System\Net\Capability.RawSocketPermissions.cs"
3838
Link="Common\System\Net\Capability.RawSocketPermissions.cs" />
39-
<Compile Include="$(CommonPath)System\Text\ValueUtf8Converter.cs"
40-
Link="Common\System\Text\ValueUtf8Converter.cs" />
39+
<Compile Include="$(CommonPath)System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs"
40+
Link="Common\System\Runtime\InteropServices\SpanOfCharAsUtf8StringMarshaller.cs" />
4141
</ItemGroup>
4242
</Project>

0 commit comments

Comments
 (0)