From ed85c2ab13892af6dda464ed0ab5cb35f0c43808 Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Thu, 11 Apr 2019 00:01:56 -0700 Subject: [PATCH] Add ref asms and unit tests for string.GetPinnableReference (dotnet/corefx#36777) Commit migrated from https://github.com/dotnet/corefx/commit/4672a7b5d28070bd3dc8431887b3474a04249ccf --- src/libraries/System.Runtime/ref/System.Runtime.cs | 2 + .../src/ApiCompatBaseline.uapaot.txt | 1 + .../tests/System/StringTests.netcoreapp.cs | 80 ++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index ed7a05c..78e8045 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -2372,6 +2372,8 @@ namespace System public static int GetHashCode(System.ReadOnlySpan value) { throw null; } public static int GetHashCode(System.ReadOnlySpan value, System.StringComparison comparisonType) { throw null; } public int GetHashCode(System.StringComparison comparisonType) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public ref readonly char GetPinnableReference() { throw null; } public System.TypeCode GetTypeCode() { throw null; } public int IndexOf(char value) { throw null; } public int IndexOf(char value, int startIndex) { throw null; } diff --git a/src/libraries/System.Runtime/src/ApiCompatBaseline.uapaot.txt b/src/libraries/System.Runtime/src/ApiCompatBaseline.uapaot.txt index aed70eb..f611e04 100644 --- a/src/libraries/System.Runtime/src/ApiCompatBaseline.uapaot.txt +++ b/src/libraries/System.Runtime/src/ApiCompatBaseline.uapaot.txt @@ -3,6 +3,7 @@ TypesMustExist : Type 'System.ArgIterator' does not exist in the implementation MembersMustExist : Member 'System.String System.Runtime.CompilerServices.RuntimeFeature.DefaultImplementationsOfInterfaces' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(System.RuntimeMethodHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(System.RuntimeMethodHandle, System.RuntimeTypeHandle[])' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'System.String.GetPinnableReference()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'System.Text.Rune.DecodeFromUtf16(System.ReadOnlySpan, System.Text.Rune, System.Int32)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'System.Text.Rune.DecodeLastFromUtf16(System.ReadOnlySpan, System.Text.Rune, System.Int32)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'System.Text.Rune.DecodeFromUtf8(System.ReadOnlySpan, System.Text.Rune, System.Int32)' does not exist in the implementation but it does exist in the contract. diff --git a/src/libraries/System.Runtime/tests/System/StringTests.netcoreapp.cs b/src/libraries/System.Runtime/tests/System/StringTests.netcoreapp.cs index 69a5bdd..366eef7 100644 --- a/src/libraries/System.Runtime/tests/System/StringTests.netcoreapp.cs +++ b/src/libraries/System.Runtime/tests/System/StringTests.netcoreapp.cs @@ -6,6 +6,8 @@ using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Microsoft.DotNet.RemoteExecutor; @@ -837,6 +839,84 @@ namespace System.Tests } [Theory] + [InlineData("")] // empty string + [InlineData("hello")] // non-empty string + public unsafe static void GetPinnableReference_ReturnsSameAsGCHandleAndLegacyFixed(string input) + { + Assert.NotNull(input); // test shouldn't have null input + + // First, ensure the value pointed to by GetPinnableReference is correct. + // It should point to the first character (or the null terminator for empty inputs). + + ref readonly char rChar = ref input.GetPinnableReference(); + Assert.Equal((input.Length > 0) ? input[0] : '\0', rChar); + + // Next, ensure that GetPinnableReference() and GCHandle.AddrOfPinnedObject agree + // on the address being returned. + + GCHandle gcHandle = GCHandle.Alloc(input, GCHandleType.Pinned); + try + { + Assert.Equal((IntPtr)Unsafe.AsPointer(ref Unsafe.AsRef(in rChar)), gcHandle.AddrOfPinnedObject()); + } + finally + { + gcHandle.Free(); + } + + // Next, ensure that GetPinnableReference matches the string projected as a ROS. + + Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in rChar), ref MemoryMarshal.GetReference((ReadOnlySpan)input))); + + // Finally, ensure that GetPinnableReference matches the legacy 'fixed' keyword. + + DynamicMethod dynamicMethod = new DynamicMethod("tester", typeof(bool), new[] { typeof(string) }); + ILGenerator ilGen = dynamicMethod.GetILGenerator(); + LocalBuilder pinnedLocal = ilGen.DeclareLocal(typeof(object), pinned: true); + + ilGen.Emit(OpCodes.Ldarg_0); // load 'input' and pin it + ilGen.Emit(OpCodes.Stloc, pinnedLocal); + + ilGen.Emit(OpCodes.Ldloc, pinnedLocal); // get the address of field 0 from pinned 'input' + ilGen.Emit(OpCodes.Conv_I); + + ilGen.Emit(OpCodes.Call, typeof(RuntimeHelpers).GetProperty("OffsetToStringData").GetMethod); // get pointer to start of string data + ilGen.Emit(OpCodes.Add); + + ilGen.Emit(OpCodes.Ldarg_0); // get value of input.GetPinnableReference() + ilGen.Emit(OpCodes.Callvirt, typeof(string).GetMethod("GetPinnableReference")); + + // At this point, the top of the evaluation stack is traditional (fixed char* = input) and input.GetPinnableReference(). + // Compare for equality and return. + + ilGen.Emit(OpCodes.Ceq); + ilGen.Emit(OpCodes.Ret); + + Assert.True((bool)dynamicMethod.Invoke(null, new[] { input })); + } + + [Fact] + public unsafe static void GetPinnableReference_WithNullInput_ThrowsNullRef() + { + // This test uses an explicit call instead of the normal callvirt that C# would emit. + // This allows us to make sure the NullReferenceException is coming from *within* + // the GetPinnableReference method rather than on the call site to that method. + + DynamicMethod dynamicMethod = new DynamicMethod("tester", typeof(void), Type.EmptyTypes); + ILGenerator ilGen = dynamicMethod.GetILGenerator(); + + ilGen.Emit(OpCodes.Ldnull); + ilGen.Emit(OpCodes.Call, typeof(string).GetMethod("GetPinnableReference")); + ilGen.Emit(OpCodes.Pop); + ilGen.Emit(OpCodes.Ret); + + Action del = (Action)dynamicMethod.CreateDelegate(typeof(Action)); + + Assert.NotNull(del); + Assert.Throws(del); + } + + [Theory] [InlineData("")] [InlineData("a")] [InlineData("\0")] -- 2.7.4