Skip to content

[API Proposal]: System.Runtime.CompilerServices.Unsafe.AsPtrRef<T, U>(ref T* source) : ref U #62342

Open
@rickbrew

Description

@rickbrew

Background and motivation

I'm told that something similar to this has been suggested before but it was decided that it was "too niche."

Well, I'm finding that I could really use this! Without it, it's impossible to do certain operations on variables or fields that are pointers. No Interlocked.Exchange, no nothing. Generics don't work with pointers, but they do work with pointer-size structs that simply wrap the pointer (e.g. Ptr<T> which is just a struct { T* p; } plus all the constraints and casting operators you'd expect).

From what I can tell, this is the key method that's needed to bridge the gap between pointers and generics. No need for adding many other pieces of support in the language, compiler, runtime, etc. Just let us temporarily reinterpret a T* as a Ptr<T> or IntPtr to enable performing an operation that works fine on that type.

In interop code, it's more idiomatic and less kludgey to just use pointers, and it's unsavory to have to switch everything to pointer-wrapping structs just because pointers aren't compatible with many things in the compiler or the framework. Once you have a field or variable that's a T* you are completely locked out of important operations that are idiomatic in native/interop code. You just can't break the pointer out of its jail without some really weird hacks that the JIT optimizer likely doesn't stand a chance against (someone found a crazy way using function pointers to accomplish this, but because of that it involves a non-inlinable method call).

With this I can do Interlocked operations on pointers, I can do Volatile.Read() and Volatile.Write() on pointers, etc. I can reinterpret an IUnknown* to a ComPtr<IUnknown> (from TerraFX). And as long as the inverse method is available, I can convert back to pointers when needed. Having to sandwich these with Unsafe calls is also unsavory, but par for the course when working heavily with interop code and Unsafe.

With help from others (esp. @jakobbotsch) I was able to get a prototype of this and it does work. Not having this in the runtime is very inconvenient, but not completely blocking, as I could create a nuget package. However, having an assembly and nuget package for the sake of 1 method is a little heavy.

cc @Sergio0694 @jakobbotsch @tannergooding who were part of the discussion in Discord

API Proposal

namespace System.Runtime.CompilerServices
{
    public static class Unsafe
    {
        // This name was chosen in part so it does not sort next to other methods like AsRef, 
        // therefore less probability it could be accidentally used (via auto-complete or etc.).
        // sizeof(U) must equal sizeof(T*)
        public static ref U AsPtrRef<T, U>(ref T* source)
            where T : unmanaged
            where U : unmanaged

        // The inverse operation is needed as well, to convert from ref U back to ref T*
        public static ref U* AsPtrRef<T, U>(ref T source)
            where T : unmanaged
            where U : unmanaged

        // Might want to have `ref T**` versions as well, up to a reasonable arity. `ref T***` perhaps, but `ref T*******` is a bit much. `T***` does _very occasionally_ pop up in native interop code (pointer to 2-dimensional array).
    }
}

The IL for this is pretty straightforward, it's just ldarg.0 and ret, along with attributes to tag T and U as unmanaged. I did manage to get a working version of this with the help of @jakobbotsch

  .method public hidebysig static !!U&  AsPtrRef<valuetype .ctor (class [System.Runtime]System.ValueType modreq([System.Runtime.InteropServices]System.Runtime.InteropServices.UnmanagedType)) T,valuetype .ctor (class [System.Runtime]System.ValueType modreq([System.Runtime.InteropServices]System.Runtime.InteropServices.UnmanagedType)) U>(!!T*& p) cil managed
  {
    .param type T 
      .custom instance void System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = ( 01 00 00 00 ) 
    .param type U 
      .custom instance void System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = ( 01 00 00 00 ) 
    // Code size       2 (0x2)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ret
  } // end of method AsPtrRef

API Usage

public static unsafe T* InterlockedExchangeHelper<T>(ref T* p, T* newValue) 
    where T : unmanaged
{
    return (T*)Interlocked.Exchange(ref Unsafe.AsPtrRef<T, IntPtr>(ref p), (IntPtr)newValue);
}

public class MyComWrapper 
    : IDisposable
{
    private IUnknown* pObject;

    public MyComWrapper(IUnknown* pObject)
    {
        this.pObject = pObject;
        pObject->AddRef();
    }

    ~MyComWrapper()
    {
        Dispose(false);
    }

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

    private void Dispose(bool disposing)
    {
        // can't do this, as the `T` can't be a pointer
        //IUnknown* pObject = Interlocked.Exchange(ref this.pUnknown);

        // but this will work
        IUnknown* pObject = InterlockedExchangeHelper(ref this.pObject, null);

        if (pObject != null)
        {
            pObject->Release();
        }        
    }
}

Alternative Designs

No response

Risks

The naming of the method needs to be chosen very carefully.

If the non-pointer type being converted to/from is not at least pointer-sized, bad things can happen. But, this is Unsafe.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions