Question

Dispose(bool) for a managed wrapper, is it native or managed in the end?

In the dispose pattern documentation, this is suggested on how to dispose object:

protected virtual void Dispose(bool disposing)
{
    if (_disposed)
    {
        return;
    }

    if (disposing)
    {
        // TODO: dispose managed state (managed objects).
    }

    // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
    // TODO: set large fields to null.

    _disposed = true;
}

Now, suppose I have a managed wrapper that allocates aligned unmanaged memory:

using System.Runtime.InteropServices;
using Whatever.Extensions;

namespace XYZ.Extensions;

public sealed class NativeMemory<T> : DisposableAsync where T : unmanaged
{
    public unsafe NativeMemory(uint count, uint alignment = 1)
    {
        ArgumentOutOfRangeException.ThrowIfZero(alignment);

        Length = count * (uint)sizeof(T);

        Pointer = (nint)NativeMemory.AlignedAlloc(Length, alignment);

        Manager = new NativeMemoryManager<T>((T*)Pointer, (int)Length);

        Manager.Memory.Span.Clear();
    }

    public NativeMemoryManager<T> Manager { get; }

    public uint Length { get; }

    public nint Pointer { get; }

    protected override ValueTask DisposeAsyncCore()
    {
        DisposeNative();

        return ValueTask.CompletedTask;
    }

    protected override void DisposeNative()
    {
        DisposePointer();
    }

    private unsafe void DisposePointer()
    {
        NativeMemory.AlignedFree(Pointer.ToPointer());
    }
}

Memory manager:

using System.Buffers;

namespace ISO9660.Extensions;

public sealed unsafe class NativeMemoryManager<T>(T* pointer, int length)
    : MemoryManager<T> where T : unmanaged
{
    private T* Pointer { get; } = pointer;

    private int Length { get; } = length;

    protected override void Dispose(bool disposing)
    {
        // NOP
    }

    public override Span<T> GetSpan()
    {
        return new Span<T>(Pointer, Length);
    }

    public override MemoryHandle Pin(int elementIndex = 0)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(elementIndex);

        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(elementIndex, Length);

        return new MemoryHandle(Pointer + elementIndex);
    }

    public override void Unpin()
    {
        // NOP
    }
}

Disposable helper:

using System;

#nullable disable
namespace Whatever.Extensions
{
  public abstract class Disposable : IDisposable
  {
    protected bool IsDisposed { get; set; }

    public void Dispose()
    {
      this.Dispose(true);
      GC.SuppressFinalize((object) this);
    }

    protected virtual void Dispose(bool disposing)
    {
      if (this.IsDisposed)
        return;
      this.DisposeNative();
      if (disposing)
        this.DisposeManaged();
      this.IsDisposed = true;
    }

    protected virtual void DisposeManaged() { }

    protected virtual void DisposeNative() { }

    ~Disposable() => this.Dispose(false);
  }
}

DisposableAsync helper:

using System;
using System.Threading.Tasks;

#nullable enable
namespace Whatever.Extensions
{
  public abstract class DisposableAsync : Disposable, IAsyncDisposable
  {
    public async ValueTask DisposeAsync()
    {
      DisposableAsync disposableAsync = this;
      await disposableAsync.DisposeAsyncCore().ConfigureAwait(false);
      GC.SuppressFinalize((object) disposableAsync);
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
      await new ValueTask().ConfigureAwait(false);
    }
  }
}

Now things are ambiguous... Is that a managed or unmanaged thing to dispose of?

Question:

When disposing NativeMemory<T>, should I consider it as managed or unmanaged?

 3  63  3
1 Jan 1970

Solution

 1

NativeMemory<T> is managed object, owning the unmanaged resources. So

  • NativeMemory<T> instances should be disposed in the managed disposal branch (when disposing==true)
  • Inside the NativeMemory<T>.Dispose() implementation we need free unmanaged resources, owned by this instance of the NativeMemory<T> in the unmanaged disposal branch (when disposing==false). (so, the NativeMemory.AlignedFree should be in the disposing==false branch).

P.S. I'm a little bit confused about the NativeMemoryManager<T> field. Does it owns the unmanaged memory or not? If the real owner is the NativeMemoryManager<T>, then the answer should be different.

P.P.S. You may consider to implement a wrapper inherited from the SafeHandle or one of its standard inheritors. SafeHandle can give you more reliable (optionally owning) managed wrapper for the unmanaged resources.

2024-07-15
Serg