Why does casting a struct to a similar class sort-of work?

Viewed 1112

Warning: This is merely an exercise for those whose are passionate about breaking stuff to understand their mechanics.

I was exploring the limits of what I could accomplish in C# and I wrote a ForceCast() function to perform a brute-force cast without any type checks. Never consider using this function in production code.

I wrote a class called Original and a struct called LikeOriginal, both with two integer variables. In Main() I created a new variable called orig and set it to a new instance of Original with a=7 and b=20. When orig is cast into LikeOriginal and stored in casted, the values of cG and dG become undefined, which is to be expected as LikeOriginal is a struct and class instances contain more metadata than struct instances thus causing memory layout mismatch.

Example Output:

Casted Original to LikeOriginal
1300246376, 542
1300246376, 542
added 3
Casted LikeOriginal back to Original
1300246379, 545

Notice, however, that when I call casted.Add(3) and cast back to Original and print the values of a and b, surprisingly they are successfully incremented by 3, and this has been repeatable.

What is confusing me is the fact that casting the class to the struct will cause cG and dG to map to class metadata, but when they are modified and cast back to a class, they map correctly with a and b.

Why is this the case?

The code used:

using System;
using System.Runtime.InteropServices;

namespace BreakingStuff {
    public class Original {
        public int a, b;

        public Original(int a, int b)
        {
            this.a = a;
            this.b = b;
        }

        public void Add(int val)
        {
        }
    }

    public struct LikeOriginal {
        public int cG, dG;

        public override string ToString() {
            return cG + ", " + dG;
        }

        public void Add(int val) {
            cG += val;
            dG += val;
        }
    }

    public static class Program {
        public unsafe static void Main() {
            Original orig = new Original(7, 20);
            LikeOriginal casted = ForceCast<Original, LikeOriginal>(orig);
            Console.WriteLine("Casted Original to LikeOriginal");
            Console.WriteLine(casted.cG + ", " + casted.dG);
            Console.WriteLine(casted.ToString());
            casted.Add(3);
            Console.WriteLine("added 3");
            orig = ForceCast<LikeOriginal, Original>(casted);
            Console.WriteLine("Casted LikeOriginal back to Original");
            Console.WriteLine(orig.a + ", " + orig.b);
            Console.ReadLine();
        }

        //performs a pointer cast but with the same memory layout.
        private static unsafe TOut ForceCast<TIn, TOut>(this TIn input) {
            GCHandle handle = GCHandle.Alloc(input);
            TOut result = Read<TOut>(GCHandle.ToIntPtr(handle));
            handle.Free();
            return result;
        }

        private static unsafe T Read<T>(this IntPtr address) {
            T obj = default(T);
            if (address == IntPtr.Zero)
                return obj;
            TypedReference tr = __makeref(obj);
            *(IntPtr*) (&tr) = address;
            return __refvalue(tr, T);
        }
    }
}
2 Answers
Related