Search code examples
c#pointersunsafereference-type

How are unsafe pointers represented in C#


I was wondering how unsafe pointers to reference types are represented in C#. Given the following:

static unsafe void Main(string[] args)
{
    var student = new Student { Name = "Dan", Age = 34 };
    
    Student* myStdPtr = &student;

    student = null;
    Console.WriteLine(myStdPtr->Name);
}

class Student
{
    public string Name;
    public int Age;
}

it would seem that a Student pointer (Student*) points at the stack memory that holds the reference to the Student object on the heap. I say this because myStdPtr->Name comes back with a NullReferenceException. If the Student pointer was pointing at the heap object directly, it wouldn't have given me the NullReferenceException.

Having said that, I can safely run this code with no issues:

static unsafe void Main(string[] args)
{
    Student* student = getStudent();
    Console.WriteLine(student->Name);
}
static unsafe Student* getStudent()
{
    var student = new Student { Name = "Dan", Age = 34 };
    Student* studentPtr = &student;
    return studentPtr;
}
class Student
{
    public string Name;
    public int Age;
}

suggesting that the Student pointer does not point at the stack memory that holds the reference to the heap object. If it did, well that stack object is cleaned up when we leave the getStudent() function and that should give us a NullReferenceException when accessing student->Name.

So the question is, when I create an unsafe pointer to a reference type in C#, what is the pointer point at? The above two examples would suggest that it can't be the stack memory holding the reference, nor the heap memory that is the reference?


Solution

  • The student pointer does indeed point to the reference on the stack rather than the Student object on the heap. (In C++ terms it would be a Student**.)

    The reason it seems to work for your second example is because the stack happens to not have been overwritten yet (and the Student object on the heap has not been collected).

    If you change your code like so you can see how horribly dangerous it is:

    public static class Program
    {
        static unsafe void Main(string[] args)
        {
            Student* student = getStudent();
            printStudent(student);
        }
    
        static unsafe Student* getStudent()
        {
            var      student    = new Student { Name = "Dan", Age = 34 };
            Student* studentPtr = &student;
            return studentPtr;
        }
    
        static unsafe void printStudent(Student* pStudent)
        {
            var student = new Student { Name = "Fred", Age = 34 };
            Console.WriteLine(pStudent->Name); // Prints "Fred", not "Dan"
        }
    
        class Student
        {
            public string Name;
            public int    Age;
        }
    }
    

    Note how the pStudent pointer is now pointing to the new student reference on the stack, which therefore prints "Fred" rather than "Dan".

    Another example of badness:

    public static class Program
    {
        static unsafe void Main(string[] args)
        {
            Student* student = getStudent();
            Console.WriteLine(student->Name); // Prints "Dan"
            doSomething();
            Console.WriteLine(student->Name); // May or may not crash. Try running a few times.
                                              // If it doesn't crash, it doesn't print "Dan". 
        }
    
        static unsafe Student* getStudent()
        {
            var      student    = new Student { Name = "Dan", Age = 34 };
            Student* studentPtr = &student;
            return studentPtr;
        }
    
        static void doSomething()
        {
            new Random();
        }
    
        class Student
        {
            public string Name;
            public int    Age;
        }
    }