This is the twentieth sixth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
We know C# has multiple signature (interface) and implementation inheritance. The latter doesn’t support full polymorphic invocations, though, but we already fixed it. We also know how to emulate state inheritance in Java and that can be almost directly translated to C#. Today we’ll see how to hack multiple identity inheritance in C#.
Word of warning: this is super hacky and requires a lot of attention (just like most things on this blog, though).
Let’s start with the following classes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class Base1 { public int field; public void PrintInt() { Console.WriteLine(field); } } class Base2 { public float field; public void PrintFloat() { Console.WriteLine(field); } } class Base3 { public short field1; public short field2; public void PrintFields() { Console.WriteLine(field1); Console.WriteLine(field2); } } class Base4 { public string field; public void PrintString() { Console.WriteLine(field); } } |
They have the same physical structure in .NET Framework 4.5 on x86 architecture. Each instance has sync block (4 bytes), method handle (4 bytes), fields occupying 4 bytes (either one field like integer/float/string or two fields taking 2 bytes each). We’d like to create a class which can behave like any of these, just like with inheritance.
The idea is simple: we’ll create a fake class with matching size and holders for fields of each base class. We’ll dynamically change type as needed (morph the instance) and save/restore fields.
First, we need to have holders for base instances:
1 2 3 4 5 6 7 8 9 10 |
public class CurrentState { public Dictionary holders; public Type lastRealType; public CurrentState(params object[] inheritedTypes) { holders = inheritedTypes.ToDictionary(t => t.GetType(), t => t); } } |
Now we need to have an interface to access the state in whichever type we are now:
1 2 3 4 |
interface MultipleBase { CurrentState CurrentState(); } |
Now we need to create subclasses with the state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class FakeChild1 : Base1, MultipleBase { // Types don't matter, size does public CurrentState currentState; public CurrentState CurrentState() => currentState; } class FakeChild2 : Base2, MultipleBase { // Types don't matter, size does public CurrentState currentState; public CurrentState CurrentState() => currentState; } class FakeChild3 : Base3, MultipleBase { // Types don't matter, size does public CurrentState currentState; public CurrentState CurrentState() => currentState; } class FakeChild4 : Base4, MultipleBase { // Types don't matter, size does public CurrentState currentState; public CurrentState CurrentState() => currentState; } |
Notice how each subclass inherits the fields from the base class and also adds one more field for the state. Also, we inherit so we can use the subclass as a base class as needed.
Now it’s the time for the morphing logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
static class MultipleBaseExtensions { public static RealType Morph(this MultipleBase self) where FakeType : class where RealType : class { object holder; var currentState = self.CurrentState(); var lastRealType = currentState.lastRealType; if (lastRealType != null) { holder = currentState.holders[lastRealType]; foreach (var field in lastRealType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { field.SetValue(holder, field.GetValue(self)); } } ChangeType(typeof(FakeType), self); holder = currentState.holders[typeof(RealType)]; foreach (var field in typeof(RealType).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { field.SetValue(self, field.GetValue(holder)); } currentState.lastRealType = typeof(RealType); return (RealType)self; } private static void ChangeType(Type t, MultipleBase self) { unsafe { TypedReference selfReference = __makeref(self); *(IntPtr*)*(IntPtr*)*(IntPtr*)&selfReference = t.TypeHandle.Value; } } } |
We extract the current type (after last morphing) and we save all the fields on the side to the specific holder. Then, we change the type (morph) and then restore fields for new type using holder instance. We change the type by modifying the method handle in place.
It’s crucial here to understand that we assume that all instances have the same size and that the currentState
field is always in the same place. We need to have the same size in each of the fake subclasses to support proper heap scanning. Otherwise GC will crash. We need to have currentState
field in the same place otherwise we won’t find it after morphing.
Now the demo:
1 2 3 4 |
MultipleBase child = new FakeChild1 { currentState = new CurrentState(new Base1(), new Base2(), new Base3(), new Base4()) }; |
So we start with instance of any fake subclass and create holders as needed. Next, we morph and modify fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
Console.WriteLine("Base1"); Base1 base1 = child.Morph(); base1.field = 123; base1.PrintInt(); Console.WriteLine(); Console.WriteLine("Base2"); Base2 base2 = child.Morph(); base2.field = 456.0f; base2.PrintFloat(); Console.WriteLine(); Console.WriteLine("Base3"); Base3 base3 = child.Morph(); base3.field1 = 789; base3.field2 = 987; base3.PrintFields(); Console.WriteLine(); Console.WriteLine("Base4"); Base4 base4 = child.Morph(); base4.field = "Abrakadabra"; base4.PrintString(); Console.WriteLine(); Console.WriteLine("Base3 again"); base3 = child.Morph(); base3.PrintFields(); Console.WriteLine(); Console.WriteLine("Base2 again"); base2 = child.Morph(); base2.PrintFloat(); Console.WriteLine(); Console.WriteLine("Base1 again"); base1 = child.Morph(); base1.PrintInt(); |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Base1 123 Base2 456 Base3 789 987 Base4 Abrakadabra Base3 again 789 987 Base2 again 456 Base1 again 123 |
So you can see that we can morph the instance and change fields, and then we can morph back and restore fields. Obviously, multithreading scenario here would be pretty tricky. However, we sort of hacked the instance to support multiple base classes in a sort of generic way.