Consider the following code:
public class SomeClass
{
public bool someBool;
}
public ref struct RefStructWithManagedFields
{
private SomeClass? _managedField;
internal void SetSomethingManaged(SomeClass something)
{
_managedField = something;
}
internal SomeClass? GetSomethingManaged()
{
return _managedField;
}
}
public unsafe ref struct RefStructWithPointer
{
/// emits warning CS8500:
/// "This takes the address of, gets the size of, or declares a pointer to a managed type ('RefStructWithManagedFields')
private readonly RefStructWithManagedFields* _pointer;
public ref RefStructWithManagedFields RefField { get => ref *_pointer; }
internal RefStructWithPointer(RefStructWithManagedFields* pointer)
{
_pointer = pointer;
}
}
class SomeContext
{
private SomeClass someClass = new();
public void DoSomething(ref RefStructWithPointer structWithPointer)
{
// set a managed field on a struct internally referenced with a pointer
structWithPointer.RefField.SetSomethingManaged(someClass);
}
}
class AnotherContext
{
public void DoSomething(ref RefStructWithPointer structWithPointer)
{
// get a managed field from a struct internally referenced with a pointer
SomeClass? someOtherClass = structWithPointer.RefField.GetSomethingManaged();
Console.WriteLine(someOtherClass?.someBool);
}
}
public class Program
{
static SomeContext someContext = new();
static AnotherContext anotherContext = new();
unsafe static void Main()
{
RefStructWithManagedFields foo = default;
/// emits warning CS8500:
/// "This takes the address of, gets the size of, or declares a pointer to a managed type ('RefStructWithManagedFields')
RefStructWithPointer bar = new(&foo);
someContext.DoSomething(ref bar);
/// garbage for the sake of argument
GeneratedALotOfGarbage();
GC.Collect();
anotherContext.DoSomething(ref bar);
}
}
Program
is passing around a ref struct
(RefStructWithPointer
) that contains a pointer to another ref struct
with managed fields (RefStructWithManagedFields
). Pointer is hidden from outside as it is returned as a ref property
. This could be done with "ref fields" from C# 11.0 onwards, however, I'm bound to 9.0
From what I understand, taking a pointer to a managed reference directly, is unsafe because the GC might move it, and so the pointer might be invalid. If you want a pointer, you must "fix" it through the fixed
keyword within a scope.
Taking a pointer to any struct
might also be unsafe because that struct might live within the heap as a field of another class, and thus if you want a pointer, just must also "fix" it. However, the compiler will allow you to avoid fixing it if it is a locally scoped var (stack allocated).
Taking a pointer to a ref struct
is also allowed without a fix as it is ensured to live on the stack.
What I don't understand is the compiler warning when those structs contain managed references. I understand that those managed references themselves, can move as they are on the heap. But if we are not taking a pointer directly to them, but to the struct that contains them. Why is it dangerous?
If a class gets moved through GC, all references to it must be updated. So I assume that the managed references contained within the ref struct
will also get updated. In any safe context, the references within the ref struct
are always safe. Why accessing it through a pointer (to the struct) might not be, if the "pointed" struct lives on the stack? Will the references contained in the struct not get updated?
In the provided example, is there any way that calling GetSomethingManaged()
or SetSomethingManaged()
might be dangerous? Why?
I'm unable to replicate a scenario where this is dangerous. I'm not understanding the danger behind accessing through pointer a ref-struct with managed fields, what I understand makes me feel it is safe, and what is actually dangerous is something else.
Replying to myself. The warning exists due to "scoping". By declaring a pointer, even if it is to a ´ref struct´, you can actually allow it to escape, if used like this:
So, my lesson is: warnings are warnings, just be carefull and really try to break it to see if you are doing bad things
EDIT: Gosh... this could actually also be done with normal unmanaged structs, even if there is no warning... so I unmarked this as the answer, as it just points out a danger with pointers in general, and not specifically addressing the issue with the warning refering to structs with managed fields...sorry.