C# safety concerns for pointer to "ref struct" with managed fields. Ignoring warning CS8500

115 views Asked by At

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.

1

There are 1 answers

0
Canijo On

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:

public class Program
{
    static SomeContext someContext = new();
    static AnotherContext anotherContext = new();

    unsafe static void Main()
    {
        DoPointerThings(out var crash);

        crash.RefField.GetSomethingManaged(); /// boom, we are pointing to a ref struct that is not longer allocated
    }

    private unsafe static void DoPointerThings(out RefStructWithPointer output)
    {
        RefStructWithManagedFields foo = default;

        /// emits warning CS8500:
        /// "This takes the address of, gets the size of, or declares a pointer to a managed type ('RefStructWithManagedFields')
        output = new(&foo);

        someContext.DoSomething(ref output); /// this is  ok

        anotherContext.DoSomething(ref output); /// also ok
    }
}

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.