Skip to content

Commit

Permalink
Merge with release 5.12.1
Browse files Browse the repository at this point in the history
  • Loading branch information
sakno committed Aug 19, 2024
2 parents db7a2bf + 1f48b6b commit be8f725
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 37 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
====

# 08-19-2024
<a href="https://www.nuget.org/packages/dotnext/5.12.1">DotNext 5.12.1</a>
* Added support of static field references to `DotNext.Runtime.ValueReference<T>` data type

<a href="https://www.nuget.org/packages/dotnext.threading/5.12.1">DotNext.Threading 5.12.1</a>
* Smallish performance improvements of `RandomAccessCache<TKey, TValue>` class

# 08-13-2024
<a href="https://www.nuget.org/packages/dotnext/5.12.0">DotNext 5.12.0</a>
* Added `DotNext.Runtime.ValueReference<T>` data type that allows to obtain async-friendly managed pointer to the field
Expand Down
26 changes: 5 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,13 @@ All these things are implemented in 100% managed code on top of existing .NET AP
* [NuGet Packages](https://www.nuget.org/profiles/rvsakno)

# What's new
Release Date: 08-14-2024
Release Date: 08-19-2024

<a href="https://www.nuget.org/packages/dotnext/5.12.0">DotNext 5.12.0</a>
* Added `DotNext.Runtime.ValueReference<T>` data type that allows to obtain async-friendly managed pointer to the field
* Deprecation of `ConcurrentCache<TKey, TValue>` in favor of `RandomAccessCache<TKey, TValue>`
<a href="https://www.nuget.org/packages/dotnext/5.12.1">DotNext 5.12.1</a>
* Added support of static field references to `DotNext.Runtime.ValueReference<T>` data type

<a href="https://www.nuget.org/packages/dotnext.metaprogramming/5.12.0">DotNext.Metaprogramming 5.12.0</a>
* Updated dependencies

<a href="https://www.nuget.org/packages/dotnext.unsafe/5.12.0">DotNext.Unsafe 5.12.0</a>
* Updated dependencies

<a href="https://www.nuget.org/packages/dotnext.threading/5.12.0">DotNext.Threading 5.12.0</a>
* Introduced async-friendly `RandomAccessCache<TKey, TValue>` as a replacement for deprecated `ConcurrentCache<TKey, TValue>`. It uses [SIEVE](https://cachemon.github.io/SIEVE-website/) eviction algorithm.

<a href="https://www.nuget.org/packages/dotnext.io/5.12.0">DotNext.IO 5.12.0</a>
* Updated dependencies

<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.12.0">DotNext.Net.Cluster 5.12.0</a>
* Fixed cancellation of `PersistentState` async methods

<a href="https://www.nuget.org/packages/dotnext.aspnetcore.cluster/5.12.0">DotNext.AspNetCore.Cluster 5.12.0</a>
* Fixed cancellation of `PersistentState` async methods
<a href="https://www.nuget.org/packages/dotnext.threading/5.12.1">DotNext.Threading 5.12.1</a>
* Smallish performance improvements of `RandomAccessCache<TKey, TValue>` class

Changelog for previous versions located [here](./CHANGELOG.md).

Expand Down
106 changes: 105 additions & 1 deletion src/DotNext.Tests/Runtime/ValueReferenceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace DotNext.Runtime;

Expand Down Expand Up @@ -84,18 +85,121 @@ public static void MutableEmptyRef()
var reference = default(ValueReference<float>);
True(reference.IsEmpty);
Null(reference.ToString());

Span<float> span = reference;
True(span.IsEmpty);
}

[Fact]
public static void ImmutableEmptyRef()
{
var reference = default(ReadOnlyValueReference<float>);
True(reference.IsEmpty);
Null(reference.ToString());

ReadOnlySpan<float> span = reference;
True(span.IsEmpty);
}

[Fact]
public static void AnonymousValue()
{
var reference = new ValueReference<int>(42);
Equal(42, reference.Value);

ReadOnlyValueReference<int> roRef = reference;
Equal(42, roRef.Value);
}

[Fact]
public static void StaticObjectAccess()
{
var reference = new ValueReference<string>(ref MyClass.StaticObject)
{
Value = "Hello, world",
};

GC.Collect(3, GCCollectionMode.Forced, true, true);
GC.WaitForPendingFinalizers();

True(reference == new ValueReference<string>(ref MyClass.StaticObject));
Same(MyClass.StaticObject, reference.Value);
}

[Fact]
public static void StaticValueTypeAccess()
{
var reference = new ReadOnlyValueReference<int>(in MyClass.StaticValueType);
MyClass.StaticValueType = 42;

GC.Collect(3, GCCollectionMode.Forced, true, true);
GC.WaitForPendingFinalizers();

True(reference == new ReadOnlyValueReference<int>(in MyClass.StaticValueType));
Equal(MyClass.StaticValueType, reference.Value);
}

[Fact]
public static void IncorrectReference()
{
byte[] empty = [];
Throws<ArgumentOutOfRangeException>(() => new ValueReference<byte>(empty, ref MemoryMarshal.GetArrayDataReference(empty)));
Throws<ArgumentOutOfRangeException>(() => new ReadOnlyValueReference<byte>(empty, ref MemoryMarshal.GetArrayDataReference(empty)));
}

[Fact]
public static void ReferenceSize()
{
Equal(Unsafe.SizeOf<ValueReference<float>>(), nint.Size + nint.Size);
}

[Fact]
public static void BoxedValueInterop()
{
var boxedInt = BoxedValue<int>.Box(42);
ValueReference<int> reference = boxedInt;

boxedInt.Value = 56;
Equal(boxedInt.Value, reference.Value);
}

[Fact]
public static void ArrayCovariance()
{
string[] array = ["a", "b"];
Throws<ArrayTypeMismatchException>(() => new ValueReference<object>(array, 0));

var roRef = new ReadOnlyValueReference<object>(array, 1);
Equal("b", roRef.Value);
}

[Fact]
public static void SpanInterop()
{
var reference = new ValueReference<int>(42);
Span<int> span = reference;
Equal(1, span.Length);

True(Unsafe.AreSame(in reference.Value, in span[0]));
}

[Fact]
public static void ReadOnlySpanInterop()
{
ReadOnlyValueReference<int> reference = new ValueReference<int>(42);
ReadOnlySpan<int> span = reference;
Equal(1, span.Length);

True(Unsafe.AreSame(in reference.Value, in span[0]));
}

private record class MyClass : IResettable
{
internal static string StaticObject;

[FixedAddressValueType]
internal static int StaticValueType;

internal int Field;
internal string AnotherField;

Expand Down
2 changes: 1 addition & 1 deletion src/DotNext.Threading/DotNext.Threading.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<ImplicitUsings>true</ImplicitUsings>
<IsTrimmable>true</IsTrimmable>
<Features>nullablePublicOnly</Features>
<VersionPrefix>5.12.0</VersionPrefix>
<VersionPrefix>5.12.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<Authors>.NET Foundation and Contributors</Authors>
<Product>.NEXT Family of Libraries</Product>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private void Reset()
{
version++;
continuationState = null;
Interlocked.Exchange(ref continuation, null);
continuation = null;
}

ValueTaskSourceStatus IValueTaskSource<bool>.GetStatus(short token)
Expand Down
2 changes: 1 addition & 1 deletion src/DotNext/DotNext.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Authors>.NET Foundation and Contributors</Authors>
<Company />
<Product>.NEXT Family of Libraries</Product>
<VersionPrefix>5.12.0</VersionPrefix>
<VersionPrefix>5.12.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<AssemblyName>DotNext</AssemblyName>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
8 changes: 8 additions & 0 deletions src/DotNext/Runtime/BoxedValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ namespace DotNext.Runtime;
[return: NotNullIfNotNull(nameof(value))]
public static explicit operator BoxedValue<T>?(in T? value) => TryBox(in value);

/// <summary>
/// Obtains a reference to the boxed value.
/// </summary>
/// <param name="boxedValue">Boxed value.</param>
/// <returns>Mutable reference to the boxed value.</returns>
public static implicit operator ValueReference<T>(BoxedValue<T> boxedValue)
=> new(boxedValue, ref boxedValue.Value);

/// <inheritdoc />
public abstract override bool Equals([NotNullWhen(true)] object? obj); // abstract to avoid inlining by AOT/JIT

Expand Down
108 changes: 104 additions & 4 deletions src/DotNext/Runtime/ValueReference.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

Expand All @@ -12,6 +14,7 @@ namespace DotNext.Runtime;
/// <param name="fieldRef">The reference to the field.</param>
/// <typeparam name="T">The type of the field.</typeparam>
[StructLayout(LayoutKind.Auto)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public readonly struct ValueReference<T>(object owner, ref T fieldRef) :
IEquatable<ValueReference<T>>,
IEqualityOperators<ValueReference<T>, ValueReference<T>, bool>
Expand All @@ -23,11 +26,46 @@ public readonly struct ValueReference<T>(object owner, ref T fieldRef) :
/// </summary>
/// <param name="array">The array.</param>
/// <param name="index">The index of the array element.</param>
/// <exception cref="ArrayTypeMismatchException">
/// <typeparamref name="T"/> is a reference type, and <paramref name="array" /> is not an array of type <typeparamref name="T"/>.
/// </exception>
public ValueReference(T[] array, int index)
: this(array, ref array[index])
{
}

private ValueReference(StrongBox<T> box)
: this(box, ref box.Value!)
{
}

/// <summary>
/// Creates a reference to the anonymous value.
/// </summary>
/// <param name="value">The anonymous value.</param>
public ValueReference(T value)
: this(new StrongBox<T> { Value = value })
{
}

/// <summary>
/// Creates a reference to a static field.
/// </summary>
/// <remarks>
/// If <typeparamref name="T"/> is a value type then your static field MUST be marked
/// with <see cref="FixedAddressValueTypeAttribute"/>. Otherwise, the behavior is unpredictable.
/// Correctness of this constructor is based on the fact that static fields are stored
/// as elements of <see cref="object"/> array allocated by the runtime in the Pinned Object Heap.
/// It means that the address of the field cannot be changed by GC.
/// </remarks>
/// <param name="staticFieldRef">A reference to the static field.</param>
/// <seealso href="https://devblogs.microsoft.com/dotnet/internals-of-the-poh/">Internals of the POH</seealso>
[CLSCompliant(false)]
public ValueReference(ref T staticFieldRef)
: this(Sentinel.Instance, ref staticFieldRef)
{
}

/// <summary>
/// Gets a value indicating that is reference is empty.
/// </summary>
Expand Down Expand Up @@ -80,21 +118,57 @@ public bool Equals(ValueReference<T> reference)
/// <returns>The immutable field reference.</returns>
public static implicit operator ReadOnlyValueReference<T>(ValueReference<T> reference)
=> Unsafe.BitCast<ValueReference<T>, ReadOnlyValueReference<T>>(reference);

/// <summary>
/// Gets a span over the referenced value.
/// </summary>
/// <param name="reference">The value reference.</param>
/// <returns>The span that contains <see cref="Value"/>; or empty span if <paramref name="reference"/> is empty.</returns>
public static implicit operator Span<T>(ValueReference<T> reference)
=> reference.IsEmpty ? new() : new(ref reference.Value);
}

/// <summary>
/// Represents a mutable reference to the field.
/// Represents an immutable reference to the field.
/// </summary>
/// <param name="owner">An object that owns the field.</param>
/// <param name="fieldRef">The reference to the field.</param>
/// <typeparam name="T">The type of the field.</typeparam>
[StructLayout(LayoutKind.Auto)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public readonly struct ReadOnlyValueReference<T>(object owner, ref readonly T fieldRef) :
IEquatable<ReadOnlyValueReference<T>>,
IEqualityOperators<ReadOnlyValueReference<T>, ReadOnlyValueReference<T>, bool>
{
private readonly nint offset = RawData.GetOffset(owner, in fieldRef);

/// <summary>
/// Creates a reference to an array element.
/// </summary>
/// <param name="array">The array.</param>
/// <param name="index">The index of the array element.</param>
public ReadOnlyValueReference(T[] array, int index)
: this(array, in array[index])
{
}

/// <summary>
/// Creates a reference to a static field.
/// </summary>
/// <remarks>
/// If <typeparamref name="T"/> is a value type then your static field MUST be marked
/// with <see cref="FixedAddressValueTypeAttribute"/>. Otherwise, the behavior is unpredictable.
/// Correctness of this constructor is based on the fact that static fields are stored
/// as elements of <see cref="object"/> array allocated by the runtime in the Pinned Object Heap.
/// It means that the address of the field cannot be changed by GC.
/// </remarks>
/// <param name="staticFieldRef">A reference to the static field.</param>
/// <seealso href="https://devblogs.microsoft.com/dotnet/internals-of-the-poh/">Internals of the POH</seealso>
public ReadOnlyValueReference(ref readonly T staticFieldRef)
: this(Sentinel.Instance, in staticFieldRef)
{
}

/// <summary>
/// Gets a value indicating that is reference is empty.
/// </summary>
Expand Down Expand Up @@ -139,19 +213,45 @@ public bool Equals(ReadOnlyValueReference<T> reference)
/// <returns><see langword="true"/> if both references are not equal; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(ReadOnlyValueReference<T> x, ReadOnlyValueReference<T> y)
=> x.Equals(y) is false;

/// <summary>
/// Gets a span over the referenced value.
/// </summary>
/// <param name="reference">The value reference.</param>
/// <returns>The span that contains <see cref="Value"/>; or empty span if <paramref name="reference"/> is empty.</returns>
public static implicit operator ReadOnlySpan<T>(ReadOnlyValueReference<T> reference)
=> reference.IsEmpty ? new() : new(in reference.Value);
}

[SuppressMessage("Performance", "CA1812", Justification = "Used for reinterpret cast")]
file sealed class RawData
file abstract class RawData
{
// TODO: Replace with public counterpart in future
private static readonly Func<object, nuint>? GetRawObjectDataSize;

[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(RuntimeHelpers))]
static RawData()
{
const BindingFlags flags = BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Static;
GetRawObjectDataSize = typeof(RuntimeHelpers)
.GetMethod(nameof(GetRawObjectDataSize), flags, [typeof(object)])
?.CreateDelegate<Func<object, nuint>>();
}

private byte data;

private RawData() => throw new NotImplementedException();

internal static nint GetOffset<T>(object owner, ref readonly T field)
internal static nint GetOffset<T>(object owner, ref readonly T field, [CallerArgumentExpression(nameof(field))] string? paramName = null)
{
ref var rawData = ref Unsafe.As<RawData>(owner).data;
return Unsafe.ByteOffset(in rawData, in Intrinsics.ChangeType<T, byte>(in field));
var offset = Unsafe.ByteOffset(in rawData, in Intrinsics.ChangeType<T, byte>(in field));

// Ensure that the reference is an interior pointer to the field inside the object
if (GetRawObjectDataSize is not null && owner != Sentinel.Instance && (nuint)(offset + Unsafe.SizeOf<T>()) > GetRawObjectDataSize(owner))
throw new ArgumentOutOfRangeException(paramName);

return offset;
}

internal static ref T GetObjectData<T>(object owner, nint offset)
Expand Down
Loading

0 comments on commit be8f725

Please sign in to comment.