Skip to content

Commit

Permalink
Add MemoryMarshal.GetArrayDataReference(Array) (dotnet#53562)
Browse files Browse the repository at this point in the history
  • Loading branch information
GrabYourPitchforks authored Jun 3, 2021
1 parent 253a253 commit 3e1ddb0
Show file tree
Hide file tree
Showing 18 changed files with 135 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Internal.Runtime.CompilerServices;

namespace System
Expand Down Expand Up @@ -225,8 +226,8 @@ private static unsafe void Copy(Array sourceArray, int sourceIndex, Array destin

nuint elementSize = (nuint)pMT->ComponentSize;
nuint byteCount = (uint)length * elementSize;
ref byte src = ref Unsafe.AddByteOffset(ref sourceArray.GetRawArrayData(), (uint)sourceIndex * elementSize);
ref byte dst = ref Unsafe.AddByteOffset(ref destinationArray.GetRawArrayData(), (uint)destinationIndex * elementSize);
ref byte src = ref Unsafe.AddByteOffset(ref MemoryMarshal.GetArrayDataReference(sourceArray), (uint)sourceIndex * elementSize);
ref byte dst = ref Unsafe.AddByteOffset(ref MemoryMarshal.GetArrayDataReference(destinationArray), (uint)destinationIndex * elementSize);

if (pMT->ContainsGCPointers)
Buffer.BulkMoveWithWriteBarrier(ref dst, ref src, byteCount);
Expand Down Expand Up @@ -278,7 +279,7 @@ public static unsafe void Clear(Array array)

MethodTable* pMT = RuntimeHelpers.GetMethodTable(array);
nuint totalByteLength = pMT->ComponentSize * array.NativeLength;
ref byte pStart = ref array.GetRawArrayData();
ref byte pStart = ref MemoryMarshal.GetArrayDataReference(array);

if (!pMT->ContainsGCPointers)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,6 @@ internal static unsafe nuint GetRawObjectDataSize(object obj)
return rawSize;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe ref byte GetRawArrayData(this Array array) =>
// See comment on RawArrayData for details
ref Unsafe.AddByteOffset(ref Unsafe.As<RawData>(array).Data, (nuint)GetMethodTable(array)->BaseSize - (nuint)(2 * sizeof(IntPtr)));

internal static unsafe ushort GetElementSize(this Array array)
{
Debug.Assert(ObjectHasComponentSize(array));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace System.Runtime.InteropServices
{
public static partial class MemoryMarshal
public static unsafe partial class MemoryMarshal
{
/// <summary>
/// Returns a reference to the 0th element of <paramref name="array"/>. If the array is empty, returns a reference to where the 0th element
Expand All @@ -20,7 +20,29 @@ public static partial class MemoryMarshal
/// </remarks>
[Intrinsic]
[NonVersionable]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T GetArrayDataReference<T>(T[] array) =>
ref Unsafe.As<byte, T>(ref Unsafe.As<RawArrayData>(array).Data);

/// <summary>
/// Returns a reference to the 0th element of <paramref name="array"/>. If the array is empty, returns a reference to where the 0th element
/// would have been stored. Such a reference may be used for pinning but must never be dereferenced.
/// </summary>
/// <exception cref="NullReferenceException"><paramref name="array"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The caller must manually reinterpret the returned <em>ref byte</em> as a ref to the array's underlying elemental type,
/// perhaps utilizing an API such as <em>System.Runtime.CompilerServices.Unsafe.As</em> to assist with the reinterpretation.
/// This technique does not perform array variance checks. The caller must manually perform any array variance checks
/// if the caller wishes to write to the returned reference.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref byte GetArrayDataReference(Array array)
{
// If needed, we can save one or two instructions per call by marking this method as intrinsic and asking the JIT
// to special-case arrays of known type and dimension.

// See comment on RawArrayData (in RuntimeHelpers.CoreCLR.cs) for details
return ref Unsafe.AddByteOffset(ref Unsafe.As<RawData>(array).Data, (nuint)RuntimeHelpers.GetMethodTable(array)->BaseSize - (nuint)(2 * sizeof(IntPtr)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public static MethodIL EmitIL(MethodDesc method)
Debug.Assert(((MetadataType)method.OwningType).Name == "MemoryMarshal");
string methodName = method.Name;

if (method.Instantiation.Length != 1)
{
return null; // we only handle the generic method GetArrayDataReference<T>(T[])
}

if (methodName == "GetArrayDataReference")
{
ILEmitter emit = new ILEmitter();
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,6 @@ DEFINE_METHOD(RUNTIME_HELPERS, IS_REFERENCE_OR_CONTAINS_REFERENCES, IsRefer
DEFINE_METHOD(RUNTIME_HELPERS, IS_BITWISE_EQUATABLE, IsBitwiseEquatable, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, GET_METHOD_TABLE, GetMethodTable, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, GET_RAW_DATA, GetRawData, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, GET_RAW_ARRAY_DATA, GetRawArrayData, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, GET_UNINITIALIZED_OBJECT, GetUninitializedObject, SM_Type_RetObj)
DEFINE_METHOD(RUNTIME_HELPERS, ENUM_EQUALS, EnumEquals, NoSig)
DEFINE_METHOD(RUNTIME_HELPERS, ENUM_COMPARE_TO, EnumCompareTo, NoSig)
Expand Down Expand Up @@ -762,7 +761,8 @@ DEFINE_METHOD(UNSAFE, PTR_WRITE_UNALIGNED, WriteUnaligned, GM_P
DEFINE_METHOD(UNSAFE, SKIPINIT, SkipInit, GM_RefT_RetVoid)

DEFINE_CLASS(MEMORY_MARSHAL, Interop, MemoryMarshal)
DEFINE_METHOD(MEMORY_MARSHAL, GET_ARRAY_DATA_REFERENCE, GetArrayDataReference, NoSig)
DEFINE_METHOD(MEMORY_MARSHAL, GET_ARRAY_DATA_REFERENCE_SZARRAY, GetArrayDataReference, GM_ArrT_RetRefT)
DEFINE_METHOD(MEMORY_MARSHAL, GET_ARRAY_DATA_REFERENCE_MDARRAY, GetArrayDataReference, SM_Array_RetRefByte)

DEFINE_CLASS(INTERLOCKED, Threading, Interlocked)
DEFINE_METHOD(INTERLOCKED, COMPARE_EXCHANGE_T, CompareExchange, GM_RefT_T_T_RetT)
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/vm/ilmarshalers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3663,7 +3663,7 @@ void ILArrayWithOffsetMarshaler::EmitConvertSpaceAndContentsCLRToNativeTemp(ILCo
EmitLoadNativeValue(pslILEmit); // dest

pslILEmit->EmitLDLOC(m_dwPinnedLocalNum);
pslILEmit->EmitCALL(METHOD__RUNTIME_HELPERS__GET_RAW_ARRAY_DATA, 1, 1);
pslILEmit->EmitCALL(METHOD__MEMORY_MARSHAL__GET_ARRAY_DATA_REFERENCE_MDARRAY, 1, 1);
pslILEmit->EmitCONV_I();

EmitLoadManagedValue(pslILEmit);
Expand Down Expand Up @@ -3707,7 +3707,7 @@ void ILArrayWithOffsetMarshaler::EmitConvertContentsNativeToCLR(ILCodeStream* ps
pslILEmit->EmitSTLOC(m_dwPinnedLocalNum);

pslILEmit->EmitLDLOC(m_dwPinnedLocalNum);
pslILEmit->EmitCALL(METHOD__RUNTIME_HELPERS__GET_RAW_ARRAY_DATA, 1, 1);
pslILEmit->EmitCALL(METHOD__MEMORY_MARSHAL__GET_ARRAY_DATA_REFERENCE_MDARRAY, 1, 1);
pslILEmit->EmitCONV_I();

pslILEmit->EmitLDLOC(m_dwOffsetLocalNum);
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/jitinterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7228,7 +7228,7 @@ bool getILIntrinsicImplementationForMemoryMarshal(MethodDesc * ftn,

mdMethodDef tk = ftn->GetMemberDef();

if (tk == CoreLibBinder::GetMethod(METHOD__MEMORY_MARSHAL__GET_ARRAY_DATA_REFERENCE)->GetMemberDef())
if (tk == CoreLibBinder::GetMethod(METHOD__MEMORY_MARSHAL__GET_ARRAY_DATA_REFERENCE_SZARRAY)->GetMemberDef())
{
mdToken tokRawSzArrayData = CoreLibBinder::GetField(FIELD__RAW_ARRAY_DATA__DATA)->GetMemberDef();

Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/vm/metasig.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ DEFINE_METASIG(GM(RefT_IntPtr_RetRefT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, r(M(0))
DEFINE_METASIG(GM(PtrVoid_Int_RetPtrVoid, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, P(v) i, P(v)))
DEFINE_METASIG(GM(RefT_RetVoid, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, r(M(0)), v))

DEFINE_METASIG(GM(ArrT_RetRefT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, a(M(0)), r(M(0))))
DEFINE_METASIG_T(SM(Array_RetRefByte, C(ARRAY), r(b)))

DEFINE_METASIG_T(SM(SafeHandle_RefBool_RetIntPtr, C(SAFE_HANDLE) r(F), I ))
DEFINE_METASIG_T(SM(SafeHandle_RetVoid, C(SAFE_HANDLE), v ))

Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Memory/ref/System.Memory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ public static partial class MemoryMarshal
public static unsafe ReadOnlySpan<char> CreateReadOnlySpanFromNullTerminated(char* value) { throw null; }
public static System.Span<T> CreateSpan<T>(ref T reference, int length) { throw null; }
public static ref T GetArrayDataReference<T>(T[] array) { throw null; }
public static ref byte GetArrayDataReference(System.Array array) { throw null; }
public static ref T GetReference<T>(System.ReadOnlySpan<T> span) { throw null; }
public static ref T GetReference<T>(System.Span<T> span) { throw null; }
public static T Read<T>(System.ReadOnlySpan<byte> source) where T : struct { throw null; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Xunit;

namespace System.SpanTests
{
Expand All @@ -13,25 +14,67 @@ public static partial class MemoryMarshalTests
[ActiveIssue("https://github.com/dotnet/runtime/issues/36885", TestPlatforms.tvOS)]
public static void GetArrayDataReference_NullInput_ThrowsNullRef()
{
Assert.Throws<NullReferenceException>(() => MemoryMarshal.GetArrayDataReference((object[])null));
Assert.Throws<NullReferenceException>(() => MemoryMarshal.GetArrayDataReference<object>((object[])null));
Assert.Throws<NullReferenceException>(() => MemoryMarshal.GetArrayDataReference((Array)null));
}

[Fact]
public static void GetArrayDataReference_NonEmptyInput_ReturnsRefToFirstElement()
{
int[] theArray = new int[] { 10, 20, 30 };
Assert.True(Unsafe.AreSame(ref theArray[0], ref MemoryMarshal.GetArrayDataReference(theArray)));
// szarray
int[] szArray = new int[] { 10, 20, 30 };
Assert.True(Unsafe.AreSame(ref szArray[0], ref MemoryMarshal.GetArrayDataReference(szArray)));
Assert.True(Unsafe.AreSame(ref szArray[0], ref Unsafe.As<byte, int>(ref MemoryMarshal.GetArrayDataReference((Array)szArray))));

// mdarray, rank 2
int[,] mdArrayRank2 = new int[3, 2];
Assert.True(Unsafe.AreSame(ref mdArrayRank2[0, 0], ref Unsafe.As<byte, int>(ref MemoryMarshal.GetArrayDataReference(mdArrayRank2))));

// mdarray, custom bounds
// there's no baseline way to get a ref to element (10, 20, 30), so we'll deref the result of GetArrayDataReference and compare
Array mdArrayCustomBounds = Array.CreateInstance(typeof(int), new[] { 1, 2, 3 }, new int[] { 10, 20, 30 });
mdArrayCustomBounds.SetValue(0x12345678, new[] { 10, 20, 30 });
Assert.Equal(0x12345678, Unsafe.As<byte, int>(ref MemoryMarshal.GetArrayDataReference(mdArrayCustomBounds)));
}

[Fact]
public static unsafe void GetArrayDataReference_EmptyInput_ReturnsRefToWhereFirstElementWouldBe()
public static unsafe void GetArrayDataReference_EmptyInput_ReturnsRefToWhereFirstElementWouldBe_SzArray()
{
int[] theArray = new int[0];

ref int theRef = ref MemoryMarshal.GetArrayDataReference(theArray);

Assert.True(Unsafe.AsPointer(ref theRef) != null);
Assert.True(Unsafe.AreSame(ref theRef, ref MemoryMarshal.GetReference(theArray.AsSpan())));

ref int theMdArrayRef = ref Unsafe.As<byte, int>(ref MemoryMarshal.GetArrayDataReference((Array)theArray)); // szarray passed to generalized Array helper
Assert.True(Unsafe.AreSame(ref theRef, ref theMdArrayRef));
}

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public static unsafe void GetArrayDataReference_EmptyInput_ReturnsRefToWhereFirstElementWouldBe_MdArray(int rank)
{
// First, compute how much distance there is between the start of the object data and the first element
// of a baseline (non-empty) array.

int[] lowerDims = Enumerable.Range(100, rank).ToArray();
int[] lengths = Enumerable.Range(1, rank).ToArray();

Array baselineArray = Array.CreateInstance(typeof(int), lengths, lowerDims);
IntPtr baselineOffset = Unsafe.ByteOffset(ref Unsafe.As<RawObject>(baselineArray).Data, ref MemoryMarshal.GetArrayDataReference(baselineArray));

// Then, perform the same calculation with an empty array of equal rank, and ensure the offsets are identical.

lengths = new int[rank]; // = { 0, 0, 0, ... }

Array emptyArray = Array.CreateInstance(typeof(int), lengths, lowerDims);
IntPtr emptyArrayOffset = Unsafe.ByteOffset(ref Unsafe.As<RawObject>(emptyArray).Data, ref MemoryMarshal.GetArrayDataReference(emptyArray));

Assert.Equal(baselineOffset, emptyArrayOffset);
}

[Fact]
Expand All @@ -45,5 +88,10 @@ public static void GetArrayDataReference_IgnoresArrayVarianceChecks()

Assert.True(Unsafe.AreSame(ref refObj, ref Unsafe.As<string, object>(ref strArr[0])));
}

private sealed class RawObject
{
internal byte Data;
}
}
}
2 changes: 1 addition & 1 deletion src/libraries/System.Private.CoreLib/src/System/Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2374,7 +2374,7 @@ private void InsertionSort(int lo, int hi)
}

private static Span<T> UnsafeArrayAsSpan<T>(Array array, int adjustedIndex, int length) =>
new Span<T>(ref Unsafe.As<byte, T>(ref array.GetRawArrayData()), array.Length).Slice(adjustedIndex, length);
new Span<T>(ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(array)), array.Length).Slice(adjustedIndex, length);

public IEnumerator GetEnumerator()
{
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/System.Private.CoreLib/src/System/Buffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public static unsafe void BlockCopy(Array src, int srcOffset, Array dst, int dst
if ((uSrcLen < uSrcOffset + uCount) || (uDstLen < uDstOffset + uCount))
throw new ArgumentException(SR.Argument_InvalidOffLen);

Memmove(ref Unsafe.AddByteOffset(ref dst.GetRawArrayData(), uDstOffset), ref Unsafe.AddByteOffset(ref src.GetRawArrayData(), uSrcOffset), uCount);
Memmove(ref Unsafe.AddByteOffset(ref MemoryMarshal.GetArrayDataReference(dst), uDstOffset), ref Unsafe.AddByteOffset(ref MemoryMarshal.GetArrayDataReference(src), uSrcOffset), uCount);
}

public static int ByteLength(Array array)
Expand Down Expand Up @@ -94,7 +94,7 @@ public static byte GetByte(Array array, int index)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index);
}

return Unsafe.Add<byte>(ref array.GetRawArrayData(), index);
return Unsafe.Add<byte>(ref MemoryMarshal.GetArrayDataReference(array), index);
}

public static void SetByte(Array array, int index, byte value)
Expand All @@ -105,7 +105,7 @@ public static void SetByte(Array array, int index, byte value)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index);
}

Unsafe.Add<byte>(ref array.GetRawArrayData(), index) = value;
Unsafe.Add<byte>(ref MemoryMarshal.GetArrayDataReference(array), index) = value;
}

internal static unsafe void ZeroMemory(byte* dest, nuint len)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public IntPtr AddrOfPinnedObject()
}

Debug.Assert(target is Array);
return (IntPtr)Unsafe.AsPointer(ref Unsafe.As<Array>(target).GetRawArrayData());
return (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(Unsafe.As<Array>(target)));
}

return (IntPtr)Unsafe.AsPointer(ref target.GetRawData());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public static unsafe IntPtr UnsafeAddrOfPinnedArrayElement(Array arr, int index)
if (arr is null)
throw new ArgumentNullException(nameof(arr));

void* pRawData = Unsafe.AsPointer(ref arr.GetRawArrayData());
void* pRawData = Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(arr));
return (IntPtr)((byte*)pRawData + (uint)index * (nuint)arr.GetElementSize());
}

Expand Down
22 changes: 3 additions & 19 deletions src/mono/System.Private.CoreLib/src/System/Array.Mono.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace System
public partial class Array
{
[StructLayout(LayoutKind.Sequential)]
private sealed class RawData
internal sealed class RawData
{
public IntPtr Bounds;
// The following is to prevent a mismatch between the managed and runtime
Expand Down Expand Up @@ -67,7 +67,7 @@ public static unsafe void Clear(Array array)
if (array == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);

ref byte ptr = ref array.GetRawSzArrayData();
ref byte ptr = ref MemoryMarshal.GetArrayDataReference(array);
nuint byteLength = array.NativeLength * (nuint)(uint)array.GetElementSize() /* force zero-extension */;

if (RuntimeHelpers.ObjectHasReferences(array))
Expand All @@ -90,7 +90,7 @@ public static unsafe void Clear(Array array, int index, int length)
if (index < lowerBound || offset < 0 || length < 0 || (uint)(offset + length) > numComponents)
ThrowHelper.ThrowIndexOutOfRangeException();

ref byte ptr = ref Unsafe.AddByteOffset(ref array.GetRawSzArrayData(), (uint)offset * (nuint)elementSize);
ref byte ptr = ref Unsafe.AddByteOffset(ref MemoryMarshal.GetArrayDataReference(array), (uint)offset * (nuint)elementSize);
nuint byteLength = (uint)length * (nuint)elementSize;

if (RuntimeHelpers.ObjectHasReferences(array))
Expand Down Expand Up @@ -450,22 +450,6 @@ public int GetUpperBound(int dimension)
return GetLowerBound(dimension) + GetLength(dimension) - 1;
}

[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref byte GetRawSzArrayData()
{
// TODO: Missing intrinsic in interpreter
return ref Unsafe.As<RawData>(this).Data;
}

[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref byte GetRawArrayData()
{
// TODO: Missing intrinsic in interpreter
return ref Unsafe.As<RawData>(this).Data;
}

[Intrinsic]
internal int GetElementSize() => GetElementSize();

Expand Down
Loading

0 comments on commit 3e1ddb0

Please sign in to comment.