Add ReferenceCountedDisposable<T>

This class is meant to be used in the same types of places std::shared_ptr is used but with manual instead of automatic ref counting.

SubStorage now uses the class to optionally dispose its base storage object.
This commit is contained in:
Alex Barney 2020-06-28 18:02:13 -07:00
parent b6eac5ddb5
commit f02c84e8dd
5 changed files with 361 additions and 24 deletions

View file

@ -199,8 +199,8 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary
2,6203,,InvalidOpenModeForWrite, 2,6203,,InvalidOpenModeForWrite,
2,6300,6399,UnsupportedOperation, 2,6300,6399,UnsupportedOperation,
2,6302,,SubStorageNotResizable, 2,6302,,UnsupportedOperationInSubStorageSetSize,Attempted to resize a non-resizable SubStorage.
2,6303,,SubStorageNotResizableMiddleOfFile, 2,6303,,UnsupportedOperationInResizableSubStorageSetSize,Attempted to resize a SubStorage that wasn't located at the end of the base storage.
2,6304,,UnsupportedOperationInMemoryStorageSetSize, 2,6304,,UnsupportedOperationInMemoryStorageSetSize,
2,6306,,UnsupportedOperationInFileStorageOperateRange, 2,6306,,UnsupportedOperationInFileStorageOperateRange,
2,6310,,UnsupportedOperationInAesCtrExStorageWrite, 2,6310,,UnsupportedOperationInAesCtrExStorageWrite,
@ -247,7 +247,7 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary
2,6811,,RemapStorageMapFull, 2,6811,,RemapStorageMapFull,
2,6900,6999,BadState, 2,6900,6999,BadState,
2,6902,,SubStorageNotInitialized, 2,6902,,NotInitialized,
2,6905,,NotMounted, 2,6905,,NotMounted,
2,6906,,SaveDataIsExtending, 2,6906,,SaveDataIsExtending,

Can't render this file because it has a wrong number of fields in line 153.

View file

@ -385,10 +385,10 @@ namespace LibHac.Fs
/// <summary>Error code: 2002-6300; Range: 6300-6399; Inner value: 0x313802</summary> /// <summary>Error code: 2002-6300; Range: 6300-6399; Inner value: 0x313802</summary>
public static Result.Base UnsupportedOperation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6300, 6399); } public static Result.Base UnsupportedOperation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6300, 6399); }
/// <summary>Error code: 2002-6302; Inner value: 0x313c02</summary> /// <summary>Attempted to resize a non-resizable SubStorage.<br/>Error code: 2002-6302; Inner value: 0x313c02</summary>
public static Result.Base SubStorageNotResizable => new Result.Base(ModuleFs, 6302); public static Result.Base UnsupportedOperationInSubStorageSetSize => new Result.Base(ModuleFs, 6302);
/// <summary>Error code: 2002-6303; Inner value: 0x313e02</summary> /// <summary>Attempted to resize a SubStorage that wasn't located at the end of the base storage.<br/>Error code: 2002-6303; Inner value: 0x313e02</summary>
public static Result.Base SubStorageNotResizableMiddleOfFile => new Result.Base(ModuleFs, 6303); public static Result.Base UnsupportedOperationInResizableSubStorageSetSize => new Result.Base(ModuleFs, 6303);
/// <summary>Error code: 2002-6304; Inner value: 0x314002</summary> /// <summary>Error code: 2002-6304; Inner value: 0x314002</summary>
public static Result.Base UnsupportedOperationInMemoryStorageSetSize => new Result.Base(ModuleFs, 6304); public static Result.Base UnsupportedOperationInMemoryStorageSetSize => new Result.Base(ModuleFs, 6304);
/// <summary>Error code: 2002-6306; Inner value: 0x314402</summary> /// <summary>Error code: 2002-6306; Inner value: 0x314402</summary>
@ -476,7 +476,7 @@ namespace LibHac.Fs
/// <summary>Error code: 2002-6900; Range: 6900-6999; Inner value: 0x35e802</summary> /// <summary>Error code: 2002-6900; Range: 6900-6999; Inner value: 0x35e802</summary>
public static Result.Base BadState { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6900, 6999); } public static Result.Base BadState { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6900, 6999); }
/// <summary>Error code: 2002-6902; Inner value: 0x35ec02</summary> /// <summary>Error code: 2002-6902; Inner value: 0x35ec02</summary>
public static Result.Base SubStorageNotInitialized => new Result.Base(ModuleFs, 6902); public static Result.Base NotInitialized => new Result.Base(ModuleFs, 6902);
/// <summary>Error code: 2002-6905; Inner value: 0x35f202</summary> /// <summary>Error code: 2002-6905; Inner value: 0x35f202</summary>
public static Result.Base NotMounted => new Result.Base(ModuleFs, 6905); public static Result.Base NotMounted => new Result.Base(ModuleFs, 6905);
/// <summary>Error code: 2002-6906; Inner value: 0x35f402</summary> /// <summary>Error code: 2002-6906; Inner value: 0x35f402</summary>

View file

@ -1,31 +1,64 @@
using System; using System;
using LibHac.Diag;
namespace LibHac.Fs namespace LibHac.Fs
{ {
public class SubStorage2 : IStorage public class SubStorage2 : IStorage
{ {
private ReferenceCountedDisposable<IStorage> SharedBaseStorage { get; set; }
private IStorage BaseStorage { get; } private IStorage BaseStorage { get; }
private long Offset { get; } private long Offset { get; }
private long Size { get; set; } private long Size { get; set; }
public bool IsResizable { get; set; } private bool IsResizable { get; set; }
public SubStorage2(IStorage baseStorage, long offset, long size) public SubStorage2(IStorage baseStorage, long offset, long size)
{ {
BaseStorage = baseStorage; BaseStorage = baseStorage;
Offset = offset; Offset = offset;
Size = size; Size = size;
IsResizable = false;
Assert.AssertTrue(IsValid());
Assert.AssertTrue(Offset >= 0);
Assert.AssertTrue(Size >= 0);
} }
public SubStorage2(SubStorage2 baseStorage, long offset, long size) public SubStorage2(ReferenceCountedDisposable<IStorage> sharedBaseStorage, long offset, long size)
{ {
BaseStorage = baseStorage.BaseStorage; SharedBaseStorage = sharedBaseStorage.AddReference();
Offset = baseStorage.Offset + offset; BaseStorage = SharedBaseStorage.Target;
Offset = offset;
Size = size; Size = size;
IsResizable = false;
Assert.AssertTrue(IsValid());
Assert.AssertTrue(Offset >= 0);
Assert.AssertTrue(Size >= 0);
}
public SubStorage2(SubStorage2 subStorage, long offset, long size)
{
BaseStorage = subStorage.BaseStorage;
Offset = subStorage.Offset + offset;
Size = size;
IsResizable = false;
Assert.AssertTrue(IsValid());
Assert.AssertTrue(Offset >= 0);
Assert.AssertTrue(Size >= 0);
Assert.AssertTrue(subStorage.Size >= offset + size);
}
private bool IsValid() => BaseStorage != null;
public void SetResizable(bool isResizable)
{
IsResizable = isResizable;
} }
protected override Result DoRead(long offset, Span<byte> destination) protected override Result DoRead(long offset, Span<byte> destination)
{ {
if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); if (!IsValid()) return ResultFs.NotInitialized.Log();
if (destination.Length == 0) return Result.Success; if (destination.Length == 0) return Result.Success;
if (!IsRangeValid(offset, destination.Length, Size)) return ResultFs.OutOfRange.Log(); if (!IsRangeValid(offset, destination.Length, Size)) return ResultFs.OutOfRange.Log();
@ -35,7 +68,7 @@ namespace LibHac.Fs
protected override Result DoWrite(long offset, ReadOnlySpan<byte> source) protected override Result DoWrite(long offset, ReadOnlySpan<byte> source)
{ {
if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); if (!IsValid()) return ResultFs.NotInitialized.Log();
if (source.Length == 0) return Result.Success; if (source.Length == 0) return Result.Success;
if (!IsRangeValid(offset, source.Length, Size)) return ResultFs.OutOfRange.Log(); if (!IsRangeValid(offset, source.Length, Size)) return ResultFs.OutOfRange.Log();
@ -45,24 +78,24 @@ namespace LibHac.Fs
protected override Result DoFlush() protected override Result DoFlush()
{ {
if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); if (!IsValid()) return ResultFs.NotInitialized.Log();
return BaseStorage.Flush(); return BaseStorage.Flush();
} }
protected override Result DoSetSize(long size) protected override Result DoSetSize(long size)
{ {
if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); if (!IsValid()) return ResultFs.NotInitialized.Log();
if (!IsResizable) return ResultFs.SubStorageNotResizable.Log(); if (!IsResizable) return ResultFs.UnsupportedOperationInSubStorageSetSize.Log();
if (size < 0 || Offset < 0) return ResultFs.InvalidSize.Log(); if (!IsOffsetAndSizeValid(Offset, size)) return ResultFs.InvalidSize.Log();
Result rc = BaseStorage.GetSize(out long baseSize); Result rc = BaseStorage.GetSize(out long currentSize);
if (rc.IsFailure()) return rc; if (rc.IsFailure()) return rc;
if (baseSize != Offset + Size) if (currentSize != Offset + Size)
{ {
// SubStorage cannot be resized unless it is located at the end of the base storage. // SubStorage cannot be resized unless it is located at the end of the base storage.
return ResultFs.SubStorageNotResizableMiddleOfFile.Log(); return ResultFs.UnsupportedOperationInResizableSubStorageSetSize.Log();
} }
rc = BaseStorage.SetSize(Offset + size); rc = BaseStorage.SetSize(Offset + size);
@ -76,10 +109,32 @@ namespace LibHac.Fs
{ {
size = default; size = default;
if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); if (!IsValid()) return ResultFs.NotInitialized.Log();
size = Size; size = Size;
return Result.Success; return Result.Success;
} }
protected override Result DoOperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan<byte> inBuffer)
{
if (!IsValid()) return ResultFs.NotInitialized.Log();
if (size == 0) return Result.Success;
if (!IsOffsetAndSizeValid(Offset, size)) return ResultFs.OutOfRange.Log();
return base.DoOperateRange(outBuffer, operationId, Offset + offset, size, inBuffer);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
SharedBaseStorage?.Dispose();
SharedBaseStorage = null;
}
base.Dispose(disposing);
}
} }
} }

View file

@ -63,7 +63,7 @@ namespace LibHac.FsSystem
protected override Result DoSetSize(long size) protected override Result DoSetSize(long size)
{ {
if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); if (BaseStorage == null) return ResultFs.NotInitialized.Log();
// todo: Add IsResizable member // todo: Add IsResizable member
// if (!IsResizable) return ResultFs.SubStorageNotResizable.Log(); // if (!IsResizable) return ResultFs.SubStorageNotResizable.Log();
@ -76,7 +76,7 @@ namespace LibHac.FsSystem
if (baseSize != Offset + Length) if (baseSize != Offset + Length)
{ {
// SubStorage cannot be resized unless it is located at the end of the base storage. // SubStorage cannot be resized unless it is located at the end of the base storage.
return ResultFs.SubStorageNotResizableMiddleOfFile.Log(); return ResultFs.UnsupportedOperationInResizableSubStorageSetSize.Log();
} }
rc = BaseStorage.SetSize(Offset + size); rc = BaseStorage.SetSize(Offset + size);

View file

@ -0,0 +1,282 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// This file was taken from the Roslyn repository
// https://github.com/dotnet/roslyn/blob/55c7d2f800eff819198dc76892cb4e7574d563c8/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ReferenceCountedDisposable.cs
#nullable enable
using System;
using System.Runtime.CompilerServices;
namespace LibHac
{
/// <summary>
/// A reference-counting wrapper which allows multiple uses of a single disposable object in code, which is
/// deterministically released (by calling <see cref="IDisposable.Dispose"/>) when the last reference is
/// disposed.
/// </summary>
/// <remarks>
/// <para>Each instance of <see cref="ReferenceCountedDisposable{T}"/> represents a counted reference (also
/// referred to as a <em>reference</em> in the following documentation) to a target object. Each of these
/// references has a lifetime, starting when it is constructed and continuing through its release. During
/// this time, the reference is considered <em>alive</em>. Each reference which is alive owns exactly one
/// reference to the target object, ensuring that it will not be disposed while still in use. A reference is
/// released through either of the following actions:</para>
///
/// <list type="bullet">
/// <item>The reference is explicitly released by a call to <see cref="Dispose"/>.</item>
/// <item>The reference is no longer in use by managed code and gets reclaimed by the garbage collector.</item>
/// </list>
///
/// <para>While each instance of <see cref="ReferenceCountedDisposable{T}"/> should be explicitly disposed when
/// the object is no longer needed by the code owning the reference, this implementation will not leak resources
/// in the event one or more callers fail to do so. When all references to an object are explicitly released
/// (i.e. by calling <see cref="Dispose"/>), the target object will itself be deterministically released by a
/// call to <see cref="IDisposable.Dispose"/> when the last reference to it is released. However, in the event
/// one or more references is not explicitly released, the underlying object will still become eligible for
/// non-deterministic release (i.e. finalization) as soon as each reference to it is released by one of the
/// two actions described previously.</para>
///
/// <para>When using <see cref="ReferenceCountedDisposable{T}"/>, certain steps must be taken to ensure the
/// target object is not disposed early.</para>
///
/// <list type="number">
/// <para>Use <see cref="ReferenceCountedDisposable{T}"/> consistently. In other words, do not mix code using
/// reference-counted wrappers with code that references to the target directly.</para>
/// <para>Only use the <see cref="ReferenceCountedDisposable{T}(T)"/> constructor one time per target object.
/// Additional references to the same target object must only be obtained by calling
/// <see cref="TryAddReference"/>.</para>
/// <para>Do not call <see cref="IDisposable.Dispose"/> on the target object directly. It will be called
/// automatically at the appropriate time, as described above.</para>
/// </list>
///
/// <para>All public methods on this type adhere to their pre- and post-conditions and will not invalidate state
/// even in concurrent execution.</para>
/// </remarks>
/// <typeparam name="T">The type of disposable object.</typeparam>
public sealed class ReferenceCountedDisposable<T> : IDisposable
where T : class, IDisposable
{
/// <summary>
/// The target of this reference. This value is initialized to a non-<see langword="null"/> value in the
/// constructor, and set to <see langword="null"/> when the current reference is disposed.
/// </summary>
/// <remarks>
/// <para>This value is only cleared in order to support cases where one or more references is garbage
/// collected without having <see cref="Dispose"/> called.</para>
/// </remarks>
private T? _instance;
/// <summary>
/// The boxed reference count, which is shared by all references with the same <see cref="Target"/> object.
/// </summary>
/// <remarks>
/// <para>This field serves as the synchronization object for the current type, since it is shared among all
/// counted reference to the same target object. Accesses to <see cref="StrongBox{T}.Value"/> should only
/// occur when this object is locked.</para>
///
/// <para>PERF DEV NOTE: A concurrent (but complex) implementation of this type with identical semantics is
/// available in source control history. The use of exclusive locks was not causing any measurable
/// performance overhead even on 28-thread machines at the time this was written.</para>
/// </remarks>
private readonly StrongBox<int> _boxedReferenceCount;
/// <summary>
/// Initializes a new reference counting wrapper around an <see cref="IDisposable"/> object.
/// </summary>
/// <remarks>
/// <para>The reference count is initialized to 1.</para>
/// </remarks>
/// <param name="instance">The object owned by this wrapper.</param>
/// <exception cref="ArgumentNullException">
/// If <paramref name="instance"/> is <see langword="null"/>.
/// </exception>
public ReferenceCountedDisposable(T instance)
: this(instance, new StrongBox<int>(1))
{
}
private ReferenceCountedDisposable(T instance, StrongBox<int> referenceCount)
{
_instance = instance ?? throw new ArgumentNullException(nameof(instance));
// The reference count has already been incremented for this instance
_boxedReferenceCount = referenceCount;
}
/// <summary>
/// Gets the target object.
/// </summary>
/// <remarks>
/// <para>This call is not valid after <see cref="Dispose"/> is called. If this property or the target
/// object is used concurrently with a call to <see cref="Dispose"/>, it is possible for the code to be
/// using a disposed object. After the current instance is disposed, this property throws
/// <see cref="ObjectDisposedException"/>. However, the exact time when this property starts throwing after
/// <see cref="Dispose"/> is called is unspecified; code is expected to not use this property or the object
/// it returns after any code invokes <see cref="Dispose"/>.</para>
/// </remarks>
/// <value>The target object.</value>
public T Target => _instance ?? throw new ObjectDisposedException(nameof(ReferenceCountedDisposable<T>));
/// <summary>
/// Increments the reference count for the disposable object, and returns a new disposable reference to it.
/// </summary>
/// <remarks>
/// <para>The returned object is an independent reference to the same underlying object. Disposing of the
/// returned value multiple times will only cause the reference count to be decreased once.</para>
/// </remarks>
/// <returns>A new <see cref="ReferenceCountedDisposable{T}"/> pointing to the same underlying object, if it
/// has not yet been disposed; otherwise, <see cref="ObjectDisposedException"/> is thrown if this reference
/// to the underlying object has already been disposed.</returns>
public ReferenceCountedDisposable<T> AddReference()
=> TryAddReferenceImpl(_instance, _boxedReferenceCount) ??
throw new ObjectDisposedException(nameof(ReferenceCountedDisposable<T>));
/// <summary>
/// Increments the reference count for the disposable object, and returns a new disposable reference to it.
/// </summary>
/// <remarks>
/// <para>The returned object is an independent reference to the same underlying object. Disposing of the
/// returned value multiple times will only cause the reference count to be decreased once.</para>
/// </remarks>
/// <returns>A new <see cref="ReferenceCountedDisposable{T}"/> pointing to the same underlying object, if it
/// has not yet been disposed; otherwise, <see langword="null"/> if this reference to the underlying object
/// has already been disposed.</returns>
public ReferenceCountedDisposable<T>? TryAddReference()
=> TryAddReferenceImpl(_instance, _boxedReferenceCount);
/// <summary>
/// Provides the implementation for <see cref="TryAddReference"/> and
/// <see cref="WeakReference.TryAddReference"/>.
/// </summary>
private static ReferenceCountedDisposable<T>? TryAddReferenceImpl(T? target, StrongBox<int> referenceCount)
{
lock (referenceCount)
{
if (referenceCount.Value == 0)
{
// The target is already disposed, and cannot be reused
return null;
}
if (target == null)
{
// The current reference has been disposed, so even though it isn't disposed yet we don't have a
// reference to the target
return null;
}
checked
{
referenceCount.Value++;
}
// Must return a new instance, in order for the Dispose operation on each individual instance to
// be idempotent.
return new ReferenceCountedDisposable<T>(target, referenceCount);
}
}
/// <summary>
/// Releases the current reference, causing the underlying object to be disposed if this was the last
/// reference.
/// </summary>
/// <remarks>
/// <para>After this instance is disposed, the <see cref="TryAddReference"/> method can no longer be used to
/// object a new reference to the target, even if other references to the target object are still in
/// use.</para>
/// </remarks>
public void Dispose()
{
T? instanceToDispose = null;
lock (_boxedReferenceCount)
{
if (_instance == null)
{
// Already disposed; allow multiple without error.
return;
}
_boxedReferenceCount.Value--;
if (_boxedReferenceCount.Value == 0)
{
instanceToDispose = _instance;
}
// Ensure multiple calls to Dispose for this instance are a NOP.
_instance = null;
}
instanceToDispose?.Dispose();
}
/// <summary>
/// Represents a weak reference to a <see cref="ReferenceCountedDisposable{T}"/> which is capable of
/// obtaining a new counted reference up until the point when the object is no longer accessible.
/// </summary>
public struct WeakReference
{
/// <summary>
/// DO NOT DISPOSE OF THE TARGET.
/// </summary>
private readonly WeakReference<T>? _weakInstance;
private readonly StrongBox<int>? _boxedReferenceCount;
public WeakReference(ReferenceCountedDisposable<T> reference)
: this()
{
if (reference == null)
{
throw new ArgumentNullException(nameof(reference));
}
T? instance = reference._instance;
var referenceCount = reference._boxedReferenceCount;
if (instance == null)
{
// The specified reference is already not valid. This case is supported by WeakReference (not
// unlike `new System.WeakReference(null)`), but we return early to avoid an unnecessary
// allocation in this case.
return;
}
_weakInstance = new WeakReference<T>(instance);
_boxedReferenceCount = referenceCount;
}
/// <summary>
/// Increments the reference count for the disposable object, and returns a new disposable reference to
/// it.
/// </summary>
/// <remarks>
/// <para>Unlike <see cref="ReferenceCountedDisposable{T}.TryAddReference"/>, this method is capable of
/// adding a reference to the underlying instance all the way up to the point where it is finally
/// disposed.</para>
///
/// <para>The returned object is an independent reference to the same underlying object. Disposing of
/// the returned value multiple times will only cause the reference count to be decreased once.</para>
/// </remarks>
/// <returns>A new <see cref="ReferenceCountedDisposable{T}"/> pointing to the same underlying object,
/// if it has not yet been disposed; otherwise, <see langword="null"/> if the underlying object has
/// already been disposed.</returns>
public ReferenceCountedDisposable<T>? TryAddReference()
{
WeakReference<T>? weakInstance = _weakInstance;
if (weakInstance == null || !weakInstance.TryGetTarget(out var target))
{
return null;
}
StrongBox<int>? referenceCount = _boxedReferenceCount;
if (referenceCount == null)
{
return null;
}
return TryAddReferenceImpl(target, referenceCount);
}
}
}
}