Description
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
.