The runtime theme of the .NET platform is covered in great detail. However, the work of the JIT itself, the resulting code and interaction with the runtime environment is not very.
Well, fix it!
We learn the reasons for the lack of inheritance from structures, the nature of unbound delegates.
')
And also ... calling any methods on any objects without reflection.
â–Ś Genesis of Value-types
Structures in .NET are, on the one hand, structures in the classical understanding of a given word (layout, mutability, etc.), on the other hand, they support OOP and the .NET environment in principle (methods ToString, GetHashCode; inheritance from System.ValueType, which in turn from System.Object; etc.).
To better understand why structures cannot be inherited from other types, it is necessary to go to the level of organization of methods in the CLR.
Instance-level methods have an implicit argument this. In fact, it is clear. JIT, by compiling code, creates a signature of the following form:
ReturnType MethodName(Type this, …arguments…)
But this is for reference types.
For significant:
ReturnType MethodName(ref Type this, …arguments…)
Yes Yes! This is done to support the variability of structures, i.e. so we can modify this.
So why not inherit structures from other types?
Answer the question: what if this is a virtual method of the base reference class? How to be a JIT compiler? No Constantly guessing and generating various code specializations (with semantics byval and byref), besides dispatching a table of virtual methods, is inefficient. Boxing is added to properly serve the virtual method.
But ... Methods
ToString ,
GetHashCode ,
Equals are virtual methods of the reference ancestor class
System.Object ?!
These are exceptions. JIT knows this and generates a binding and specialization only for these methods.
â–Ś Unbound Delegates
Reflection in .NET allows us to create a delegate for both static methods and instances.
However, there is a small problem - for instances you need to create a delegate in a new way.
Consider an example: class Program { static void Main(string[] args) { var calc = new Calc() { FirstOperand = 2 }; var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (Func<int, int>)Delegate.CreateDelegate( typeof(Func<int, int>), calc, addMethodInfo); Console.WriteLine(addDelegate(2));
Unbound delegates come to the
rescue , i.e. unbound However, they have one feature: a different signature where they add (yes, you guessed correctly) the first argument - a link to the instance.
Those. unbound delegates are references to the “real” method.
So, the signature
Add (int secondOperand) turns into
Add ( Calc this , int secondOperand) .
Check: class Program { static void Main(string[] args) { var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (Func<Calc, int, int>)Delegate.CreateDelegate( typeof(Func<Calc, int, int>), null, addMethodInfo); Console.WriteLine(addDelegate(new Calc(), 2));
Remember the question about the signatures of the structure methods? Declare the type Calc as a
struct and run. ArgumentException? Yes?
We need to pass the argument to
this byref to Func <Calc, int, int>, but how ?!
Declare your delegate FuncByRef delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2);
Change the code: class Program { delegate TResult FuncByRef<T1, in T2, out TResult>(ref T1 arg1, T2 arg2); static void Main(string[] args) { var addMethodInfo = typeof(Calc).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); var addDelegate = (FuncByRef<Calc, int, int>)Delegate.CreateDelegate( typeof(FuncByRef<Calc, int, int>), null, addMethodInfo); var calc = new Calc(); calc.FirstOperand = 123; Console.WriteLine(addDelegate(ref calc, 2));
â–Ś Verification evasion
Consider a simple application:
class Program { static void Main(string[] args) { CallTest(new object()); CallTestWithExlicitCasting(new object()); Console.Read(); } static void CallTest(object target) { Program p = target as Program; p.Test(); } static void CallTestWithExlicitCasting(object target) { Program p = (Program)target; p.Test(); } public void Test() { Console.WriteLine("Test"); } }
As you can see, the application will fall from a NullReferenceException when calling CallTest ().
Well, correct this situation. To do this, run
ildasm .
Visual Studio Command Promt -> ildasm

Next
File -> Dump -> Save as dialog -> msiltricks_patch.il
Open the saved file msiltricks_patch.il in your favorite editor and go to the body of the CallTest method:
.method private hidebysig static void CallTest(object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: isinst MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program::Test() IL_000d: ret } // end of method Program::CallTest
Delete the term IL_0001: isinst MSILTricks.Program, i.e. call the
isinst op code (also
known as as in C #).
Do the same with the CallTestWithExlicitCasting method:
.method private hidebysig static void CallTestWithExlicitCasting(object target) cil managed { // Code size 14 (0xe) .maxstack 1 .locals init ([0] class MSILTricks.Program p) IL_0000: ldarg.0 IL_0001: castclass MSILTricks.Program IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void MSILTricks.Program::Test() IL_000d: ret } // end of method Program::CallTestWithExlicitCasting
Remove the term
IL_0001: castclass MSILTricks.Program
, i.e. a call to the
castclass opcode (also
known as an explicit cast operator in C #).
Visual Studio Command Promt -> cd [your saved file dir]
Visual Studio Command Promt -> ilasm msiltricks_patch.il
Run
msiltricks_patch.exeNot a single exception, even
AccessViolationException .
Haha
The fact is that our
Test method has no side effects, and also does not use
this in its body.
Conclusion: we are working with “hardware” and the variables of reference types are just addresses in memory, i.e.
DWORD ; type casting, etc. are nothing more than an abstraction and “protection” at the compilation stage. The central processor works with addresses in memory. The CLR provides these addresses, the JIT compiles the code considering them.
Your KO :)
And, yes, the
callvirt instruction does not check for the “correctness” of the object.
To get AccessViolationException, you can add, for example, a virtual method to the Program class and call it in the Test method.