指向具有托管字段的“ref struct”指针的 C# 安全问题。忽略警告 CS8500

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

提问人:Canijo 提问时间:11/18/2023 最后编辑:Canijo 更新时间:11/20/2023 访问量:36

问:

请考虑以下代码:

    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传递一个 (),该 () 包含指向另一个带有托管字段 () 的指针。指针在外部是隐藏的,因为它是作为 .这可以通过从 C# 11.0 开始的“ref 字段”来完成,但是,我绑定到 9.0ref structRefStructWithPointerref structRefStructWithManagedFieldsref property

据我了解,直接获取指向托管引用的指针是不安全的,因为 GC 可能会移动它,因此指针可能无效。如果需要指针,则必须通过作用域内的关键字来“修复”它。fixed

获取指向任何指针也可能是不安全的,因为该结构可能作为另一个类的字段存在于堆中,因此,如果您想要指针,也必须“修复”它。但是,如果它是本地范围的 var(分配的堆栈),编译器将允许您避免修复它。struct

在没有修复的情况下,也允许使用指向 a 的指针,因为它可以确保存在于堆栈上。ref struct

我不明白的是,当这些结构包含托管引用时,编译器会发出警告。我知道这些托管引用本身可以移动,因为它们在堆上。但是,如果我们不是直接指向它们,而是指向包含它们的结构。为什么它很危险?

如果一个类通过 GC 移动,则必须更新对它的所有引用。因此,我假设包含在 中的托管引用也将得到更新。在任何安全的上下文中,其中的引用始终是安全的。如果“指向”结构存在于堆栈上,为什么不能通过指针(指向结构体)访问它?结构中包含的引用不会更新吗?ref structref struct

在提供的示例中,是否有任何方式可以调用或可能很危险?为什么?GetSomethingManaged()SetSomethingManaged()

我无法复制这种情况很危险。我不明白通过指针访问带有托管字段的 ref-struct 背后的危险,我理解的让我觉得它是安全的,而真正危险的是别的东西。

C# 指针 不安全的 托管 ref-struct

评论

0赞 Charlieface 11/19/2023
这在 C# 9 中根本不起作用,但专门放宽以产生警告而不是 learn.microsoft.com/en-us/dotnet/csharp/language-reference/......我不认为它区分了指针指向的事实,它将指向托管类型的指针的所有使用标记为不安全。ref struct
0赞 Charlieface 11/19/2023
“如果”指向“结构存在于堆栈上,为什么不能通过指针(指向结构体)访问它?结构中包含的引用不会更新吗?您可以忽略结构本身被移动的情况。虽然这不可能使用 ,但它似乎没有考虑到 。ref structref
0赞 Canijo 11/20/2023
这在 C# 9 @Charlieface中根本不起作用,我可以让它正确编译输出(我用一个很长的字符串替换了“bool someBool”,以确保输出不是偶然的“有效”)。它不会在 Unity 2023 中编译,我打算使用它,但它为我提供了一个针对 .NET 6.0 和 LangVersion 9.0 的标准 ConsoleApplication,只有警告。由于没有安全感,我不会这样做。但是,鉴于我看到了 propper 输出,并且我没有进行任何指针运算,只是指向堆栈分配的结构,因此在内存方面它似乎仍然“安全”。

答:

1赞 Canijo 11/24/2023 #1

回复自己。由于“范围”而存在警告。通过声明一个指针,即使它是指向“ref struct”的,你实际上也可以允许它转义,如果像这样使用:

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
    }
}

所以,我的教训是:警告就是警告,只要小心,真正尝试打破它,看看你是否在做坏事

编辑:天哪......这实际上也可以用普通的非托管结构来完成,即使没有警告......所以我取消了将其标记为答案,因为它只是指出了指针的一般危险,而不是专门解决该问题,警告指的是具有托管字段的结构......不好意思。