From f02c84e8dd8e0fc6e69158c33b39e13b50b1b376 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 28 Jun 2020 18:02:13 -0700 Subject: [PATCH] Add ReferenceCountedDisposable 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. --- build/CodeGen/results.csv | 6 +- src/LibHac/Fs/ResultFs.cs | 10 +- src/LibHac/Fs/SubStorage2.cs | 83 +++++-- src/LibHac/FsSystem/SubStorage.cs | 4 +- src/LibHac/ReferenceCountedDisposable.cs | 282 +++++++++++++++++++++++ 5 files changed, 361 insertions(+), 24 deletions(-) create mode 100644 src/LibHac/ReferenceCountedDisposable.cs diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv index 3adccd8c..677019fc 100644 --- a/build/CodeGen/results.csv +++ b/build/CodeGen/results.csv @@ -199,8 +199,8 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 2,6203,,InvalidOpenModeForWrite, 2,6300,6399,UnsupportedOperation, -2,6302,,SubStorageNotResizable, -2,6303,,SubStorageNotResizableMiddleOfFile, +2,6302,,UnsupportedOperationInSubStorageSetSize,Attempted to resize a non-resizable SubStorage. +2,6303,,UnsupportedOperationInResizableSubStorageSetSize,Attempted to resize a SubStorage that wasn't located at the end of the base storage. 2,6304,,UnsupportedOperationInMemoryStorageSetSize, 2,6306,,UnsupportedOperationInFileStorageOperateRange, 2,6310,,UnsupportedOperationInAesCtrExStorageWrite, @@ -247,7 +247,7 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 2,6811,,RemapStorageMapFull, 2,6900,6999,BadState, -2,6902,,SubStorageNotInitialized, +2,6902,,NotInitialized, 2,6905,,NotMounted, 2,6906,,SaveDataIsExtending, diff --git a/src/LibHac/Fs/ResultFs.cs b/src/LibHac/Fs/ResultFs.cs index a1bf0ae9..4fa72bd5 100644 --- a/src/LibHac/Fs/ResultFs.cs +++ b/src/LibHac/Fs/ResultFs.cs @@ -385,10 +385,10 @@ namespace LibHac.Fs /// Error code: 2002-6300; Range: 6300-6399; Inner value: 0x313802 public static Result.Base UnsupportedOperation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6300, 6399); } - /// Error code: 2002-6302; Inner value: 0x313c02 - public static Result.Base SubStorageNotResizable => new Result.Base(ModuleFs, 6302); - /// Error code: 2002-6303; Inner value: 0x313e02 - public static Result.Base SubStorageNotResizableMiddleOfFile => new Result.Base(ModuleFs, 6303); + /// Attempted to resize a non-resizable SubStorage.
Error code: 2002-6302; Inner value: 0x313c02
+ public static Result.Base UnsupportedOperationInSubStorageSetSize => new Result.Base(ModuleFs, 6302); + /// Attempted to resize a SubStorage that wasn't located at the end of the base storage.
Error code: 2002-6303; Inner value: 0x313e02
+ public static Result.Base UnsupportedOperationInResizableSubStorageSetSize => new Result.Base(ModuleFs, 6303); /// Error code: 2002-6304; Inner value: 0x314002 public static Result.Base UnsupportedOperationInMemoryStorageSetSize => new Result.Base(ModuleFs, 6304); /// Error code: 2002-6306; Inner value: 0x314402 @@ -476,7 +476,7 @@ namespace LibHac.Fs /// Error code: 2002-6900; Range: 6900-6999; Inner value: 0x35e802 public static Result.Base BadState { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6900, 6999); } /// Error code: 2002-6902; Inner value: 0x35ec02 - public static Result.Base SubStorageNotInitialized => new Result.Base(ModuleFs, 6902); + public static Result.Base NotInitialized => new Result.Base(ModuleFs, 6902); /// Error code: 2002-6905; Inner value: 0x35f202 public static Result.Base NotMounted => new Result.Base(ModuleFs, 6905); /// Error code: 2002-6906; Inner value: 0x35f402 diff --git a/src/LibHac/Fs/SubStorage2.cs b/src/LibHac/Fs/SubStorage2.cs index 3f459d7d..22099adb 100644 --- a/src/LibHac/Fs/SubStorage2.cs +++ b/src/LibHac/Fs/SubStorage2.cs @@ -1,31 +1,64 @@ using System; +using LibHac.Diag; namespace LibHac.Fs { public class SubStorage2 : IStorage { + private ReferenceCountedDisposable SharedBaseStorage { get; set; } private IStorage BaseStorage { get; } private long Offset { get; } private long Size { get; set; } - public bool IsResizable { get; set; } + private bool IsResizable { get; set; } public SubStorage2(IStorage baseStorage, long offset, long size) { BaseStorage = baseStorage; Offset = offset; 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 sharedBaseStorage, long offset, long size) { - BaseStorage = baseStorage.BaseStorage; - Offset = baseStorage.Offset + offset; + SharedBaseStorage = sharedBaseStorage.AddReference(); + BaseStorage = SharedBaseStorage.Target; + Offset = offset; 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 destination) { - if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (!IsValid()) return ResultFs.NotInitialized.Log(); if (destination.Length == 0) return Result.Success; if (!IsRangeValid(offset, destination.Length, Size)) return ResultFs.OutOfRange.Log(); @@ -35,7 +68,7 @@ namespace LibHac.Fs protected override Result DoWrite(long offset, ReadOnlySpan source) { - if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (!IsValid()) return ResultFs.NotInitialized.Log(); if (source.Length == 0) return Result.Success; if (!IsRangeValid(offset, source.Length, Size)) return ResultFs.OutOfRange.Log(); @@ -45,24 +78,24 @@ namespace LibHac.Fs protected override Result DoFlush() { - if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (!IsValid()) return ResultFs.NotInitialized.Log(); return BaseStorage.Flush(); } protected override Result DoSetSize(long size) { - if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); - if (!IsResizable) return ResultFs.SubStorageNotResizable.Log(); - if (size < 0 || Offset < 0) return ResultFs.InvalidSize.Log(); + if (!IsValid()) return ResultFs.NotInitialized.Log(); + if (!IsResizable) return ResultFs.UnsupportedOperationInSubStorageSetSize.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 (baseSize != Offset + Size) + if (currentSize != Offset + Size) { // 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); @@ -76,10 +109,32 @@ namespace LibHac.Fs { size = default; - if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (!IsValid()) return ResultFs.NotInitialized.Log(); size = Size; return Result.Success; } + + protected override Result DoOperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan 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); + } } } diff --git a/src/LibHac/FsSystem/SubStorage.cs b/src/LibHac/FsSystem/SubStorage.cs index 90991dc7..7a287bc1 100644 --- a/src/LibHac/FsSystem/SubStorage.cs +++ b/src/LibHac/FsSystem/SubStorage.cs @@ -63,7 +63,7 @@ namespace LibHac.FsSystem protected override Result DoSetSize(long size) { - if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (BaseStorage == null) return ResultFs.NotInitialized.Log(); // todo: Add IsResizable member // if (!IsResizable) return ResultFs.SubStorageNotResizable.Log(); @@ -76,7 +76,7 @@ namespace LibHac.FsSystem if (baseSize != Offset + Length) { // 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); diff --git a/src/LibHac/ReferenceCountedDisposable.cs b/src/LibHac/ReferenceCountedDisposable.cs new file mode 100644 index 00000000..5943417a --- /dev/null +++ b/src/LibHac/ReferenceCountedDisposable.cs @@ -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 +{ + /// + /// A reference-counting wrapper which allows multiple uses of a single disposable object in code, which is + /// deterministically released (by calling ) when the last reference is + /// disposed. + /// + /// + /// Each instance of represents a counted reference (also + /// referred to as a reference 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 alive. 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: + /// + /// + /// The reference is explicitly released by a call to . + /// The reference is no longer in use by managed code and gets reclaimed by the garbage collector. + /// + /// + /// While each instance of 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 ), the target object will itself be deterministically released by a + /// call to 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. + /// + /// When using , certain steps must be taken to ensure the + /// target object is not disposed early. + /// + /// + /// Use consistently. In other words, do not mix code using + /// reference-counted wrappers with code that references to the target directly. + /// Only use the constructor one time per target object. + /// Additional references to the same target object must only be obtained by calling + /// . + /// Do not call on the target object directly. It will be called + /// automatically at the appropriate time, as described above. + /// + /// + /// All public methods on this type adhere to their pre- and post-conditions and will not invalidate state + /// even in concurrent execution. + /// + /// The type of disposable object. + public sealed class ReferenceCountedDisposable : IDisposable + where T : class, IDisposable + { + /// + /// The target of this reference. This value is initialized to a non- value in the + /// constructor, and set to when the current reference is disposed. + /// + /// + /// This value is only cleared in order to support cases where one or more references is garbage + /// collected without having called. + /// + private T? _instance; + + /// + /// The boxed reference count, which is shared by all references with the same object. + /// + /// + /// 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 should only + /// occur when this object is locked. + /// + /// 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. + /// + private readonly StrongBox _boxedReferenceCount; + + /// + /// Initializes a new reference counting wrapper around an object. + /// + /// + /// The reference count is initialized to 1. + /// + /// The object owned by this wrapper. + /// + /// If is . + /// + public ReferenceCountedDisposable(T instance) + : this(instance, new StrongBox(1)) + { + } + + private ReferenceCountedDisposable(T instance, StrongBox referenceCount) + { + _instance = instance ?? throw new ArgumentNullException(nameof(instance)); + + // The reference count has already been incremented for this instance + _boxedReferenceCount = referenceCount; + } + + /// + /// Gets the target object. + /// + /// + /// This call is not valid after is called. If this property or the target + /// object is used concurrently with a call to , it is possible for the code to be + /// using a disposed object. After the current instance is disposed, this property throws + /// . However, the exact time when this property starts throwing after + /// is called is unspecified; code is expected to not use this property or the object + /// it returns after any code invokes . + /// + /// The target object. + public T Target => _instance ?? throw new ObjectDisposedException(nameof(ReferenceCountedDisposable)); + + /// + /// Increments the reference count for the disposable object, and returns a new disposable reference to it. + /// + /// + /// 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. + /// + /// A new pointing to the same underlying object, if it + /// has not yet been disposed; otherwise, is thrown if this reference + /// to the underlying object has already been disposed. + public ReferenceCountedDisposable AddReference() + => TryAddReferenceImpl(_instance, _boxedReferenceCount) ?? + throw new ObjectDisposedException(nameof(ReferenceCountedDisposable)); + + /// + /// Increments the reference count for the disposable object, and returns a new disposable reference to it. + /// + /// + /// 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. + /// + /// A new pointing to the same underlying object, if it + /// has not yet been disposed; otherwise, if this reference to the underlying object + /// has already been disposed. + public ReferenceCountedDisposable? TryAddReference() + => TryAddReferenceImpl(_instance, _boxedReferenceCount); + + /// + /// Provides the implementation for and + /// . + /// + private static ReferenceCountedDisposable? TryAddReferenceImpl(T? target, StrongBox 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(target, referenceCount); + } + } + + /// + /// Releases the current reference, causing the underlying object to be disposed if this was the last + /// reference. + /// + /// + /// After this instance is disposed, the 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. + /// + 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(); + } + + /// + /// Represents a weak reference to a which is capable of + /// obtaining a new counted reference up until the point when the object is no longer accessible. + /// + public struct WeakReference + { + /// + /// DO NOT DISPOSE OF THE TARGET. + /// + private readonly WeakReference? _weakInstance; + private readonly StrongBox? _boxedReferenceCount; + + public WeakReference(ReferenceCountedDisposable 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(instance); + _boxedReferenceCount = referenceCount; + } + + /// + /// Increments the reference count for the disposable object, and returns a new disposable reference to + /// it. + /// + /// + /// Unlike , this method is capable of + /// adding a reference to the underlying instance all the way up to the point where it is finally + /// disposed. + /// + /// 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. + /// + /// A new pointing to the same underlying object, + /// if it has not yet been disposed; otherwise, if the underlying object has + /// already been disposed. + public ReferenceCountedDisposable? TryAddReference() + { + WeakReference? weakInstance = _weakInstance; + if (weakInstance == null || !weakInstance.TryGetTarget(out var target)) + { + return null; + } + + StrongBox? referenceCount = _boxedReferenceCount; + if (referenceCount == null) + { + return null; + } + + return TryAddReferenceImpl(target, referenceCount); + } + } + } +} \ No newline at end of file