Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MarshalString.FromAbiUnsafe API #1400

Merged
merged 3 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Tests/UnitTest/TestComponentCSharp_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -1281,6 +1282,38 @@ public unsafe void TestFactoryCast()
Assert.Equal("ComImports", MarshalString.FromAbi(hstr));
}

[Fact]
public unsafe void TestMarshalString_FromAbiUnsafe()
{
// The span must be empty and point to a null-terminated buffer (HSTRING-s are null-terminated too)
var span = MarshalString.FromAbiUnsafe(IntPtr.Zero);
Assert.Equal(0, span.Length);
Assert.True(MemoryMarshal.GetReference(span) == '\0');

// Same thing but with round-tripping from a null string
var hstr = MarshalString.FromManaged(null);
span = MarshalString.FromAbiUnsafe(hstr);
Assert.Equal(0, span.Length);
Assert.True(MemoryMarshal.GetReference(span) == '\0');
MarshalString.DisposeAbi(hstr);

// Same thing but with an empty string (equivalent to null)
hstr = MarshalString.FromManaged("");
span = MarshalString.FromAbiUnsafe(hstr);
Assert.Equal(0, span.Length);
Assert.True(MemoryMarshal.GetReference(span) == '\0');
MarshalString.DisposeAbi(hstr);

// Marshal from some non-null, non-empty string. We want to check that both the span has the expected content,
// but also that it's correctly null-terminated (outside of its bounds). This is always safe to access, like
// before, because the memory should point to the HSTRING buffer, which is always null-terminated as well.
hstr = MarshalString.FromManaged(nameof(TestMarshalString_FromAbiUnsafe));
span = MarshalString.FromAbiUnsafe(hstr);
Assert.True(span.SequenceEqual(nameof(TestMarshalString_FromAbiUnsafe)));
Assert.True(Unsafe.Add(ref MemoryMarshal.GetReference(span), span.Length) == '\0');
MarshalString.DisposeAbi(hstr);
}

[Fact]
public void TestFundamentalGeneric()
{
Expand Down
39 changes: 35 additions & 4 deletions src/WinRT.Runtime/Marshalers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public unsafe IntPtr GetAbi()
{
IntPtr hstring;
Debug.Assert(_header == IntPtr.Zero);
_header = Marshal.AllocHGlobal(Unsafe.SizeOf<HSTRING_HEADER>());
_header = Marshal.AllocHGlobal(sizeof(HSTRING_HEADER));
Marshal.ThrowExceptionForHR(Platform.WindowsCreateStringReference(
(ushort*)chars, value.Length, (IntPtr*)_header, &hstring));
return hstring;
Expand Down Expand Up @@ -171,6 +171,37 @@ public static unsafe string FromAbi(IntPtr value)
return new string(buffer, 0, (int)length);
}

/// <summary>
/// Marshals an input <c>HSTRING</c> value to a <see cref="ReadOnlySpan{T}"/> value.
/// </summary>
/// <param name="value">The input <c>HSTRING</c> value to marshal.</param>
/// <returns>The resulting <see cref="ReadOnlySpan{T}"/> value.</returns>
/// <remarks>
/// <para>
/// This method is equivalent to <see cref="FromAbi"/>, but it does not create a new <see cref="string"/> instance.
/// Doing so makes it zero-allocation, but extra care should be taken by callers to ensure that the returned value
/// does not escape the scope where the source <c>HSTRING</c> is valid.
/// </para>
/// <para>
/// For instance, if this method is invoked in the scope of a method that receives the <c>HSTRING</c> value as one of
/// its parameters, the resulting <see cref="ReadOnlySpan{T}"/> is always valid for the scope of such method. But, if
/// the <c>HSTRING</c> was created by reference in a given scope, the resulting <see cref="ReadOnlySpan{T}"/> value
/// will also only be valid within such scope, and should not be used outside of it.
/// </para>
/// </remarks>
public static unsafe ReadOnlySpan<char> FromAbiUnsafe(IntPtr value)
manodasanW marked this conversation as resolved.
Show resolved Hide resolved
{
if (value == IntPtr.Zero)
{
return "".AsSpan();
}

uint length;
char* buffer = Platform.WindowsGetStringRawBuffer(value, &length);

return new(buffer, (int)length);
}

public static unsafe IntPtr FromManaged(string value)
{
if (value is null)
Expand Down Expand Up @@ -218,7 +249,7 @@ public static unsafe MarshalerArray CreateMarshalerArray(string[] array)
try
{
var length = array.Length;
m._array = Marshal.AllocCoTaskMem(length * Marshal.SizeOf<IntPtr>());
m._array = Marshal.AllocCoTaskMem(length * sizeof(IntPtr));
m._marshalers = new MarshalString[length];
var elements = (IntPtr*)m._array.ToPointer();
for (int i = 0; i < length; i++)
Expand Down Expand Up @@ -286,7 +317,7 @@ public static unsafe (int length, IntPtr data) FromManagedArray(string[] array)
try
{
var length = array.Length;
data = Marshal.AllocCoTaskMem(length * Marshal.SizeOf<IntPtr>());
data = Marshal.AllocCoTaskMem(length * sizeof(IntPtr));
var elements = (IntPtr*)data;
for (i = 0; i < length; i++)
{
Expand Down Expand Up @@ -1194,7 +1225,7 @@ private static Func<T, IObjectReference> BindCreateMarshaler()

#if EMBED
internal
#else
#else
public
#endif
static class MarshalInspectable<
Expand Down
3 changes: 2 additions & 1 deletion src/WinRT.Runtime/MatchingRefApiCompatBaseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,5 @@ TypesMustExist : Type 'WinRT.StructTypeDetails<T, TAbi>' does not exist in the r
MembersMustExist : Member 'public void WinRT.WindowsRuntimeTypeAttribute..ctor(System.String, System.String)' does not exist in the reference but it does exist in the implementation.
MembersMustExist : Member 'public System.String WinRT.WindowsRuntimeTypeAttribute.GuidSignature.get()' does not exist in the reference but it does exist in the implementation.
TypesMustExist : Type 'WinRT.WinRTExposedTypeAttribute' does not exist in the reference but it does exist in the implementation.
Total Issues: 121
MembersMustExist : Member 'public System.ReadOnlySpan<System.Char> WinRT.MarshalString.FromAbiUnsafe(System.IntPtr)' does not exist in the reference but it does exist in the implementation.
Total Issues: 122
Loading