Add ref asms and unit tests for string.GetPinnableReference (dotnet/corefx#36777)
authorLevi Broderick <GrabYourPitchforks@users.noreply.github.com>
Thu, 11 Apr 2019 07:01:56 +0000 (00:01 -0700)
committerGitHub <noreply@github.com>
Thu, 11 Apr 2019 07:01:56 +0000 (00:01 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/4672a7b5d28070bd3dc8431887b3474a04249ccf

src/libraries/System.Runtime/ref/System.Runtime.cs
src/libraries/System.Runtime/src/ApiCompatBaseline.uapaot.txt
src/libraries/System.Runtime/tests/System/StringTests.netcoreapp.cs

index ed7a05c..78e8045 100644 (file)
@@ -2372,6 +2372,8 @@ namespace System
         public static int GetHashCode(System.ReadOnlySpan<char> value) { throw null; }
         public static int GetHashCode(System.ReadOnlySpan<char> 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; }
index aed70eb..f611e04 100644 (file)
@@ -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.Char>, 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.Char>, 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.Byte>, System.Text.Rune, System.Int32)' does not exist in the implementation but it does exist in the contract.
index 69a5bdd..366eef7 100644 (file)
@@ -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<char>.
+
+            Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in rChar), ref MemoryMarshal.GetReference((ReadOnlySpan<char>)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<NullReferenceException>(del);
+        }
+
+        [Theory]
         [InlineData("")]
         [InlineData("a")]
         [InlineData("\0")]