0

I am looking for a way to check at runtime which overrides of GetHashCode do nothing else than to simply call a particular static method. I'm close, but there are minor differences that I cannot explain yet.

On the MethodInfo objects of the overrides in question I call GetMethodBody().GetILAsByteArray(). The idea is to compare those implementations to a set of likely candidates. A GetHashCode override that simply calls our static method tends to look like one of three possibilities:

// Option 1 - Expression-bodied
public override int GetHashCode() => StaticClass.StaticMethod();
// Option 2 - Method-bodied
public override int GetHashCode()
{
    return StaticClass.StaticMethod();
}
// Option 3 - Method-bodied with intermediate var
public override int GetHashCode()
{
    var hashCode = StaticClass.StaticMethod();
    return hashCode;
}

For simplicity, we will only consider option 1, the expression-bodied implementation.

I have already established that the following do not affect the result:

  • Namespace
  • Access modifier
  • Type nesting (i.e. types nested inside another type)
  • Type sealing (public [sealed] class)
  • Method sealing (public [sealed] override int GetHashCode)

The test solution contains a console application, a class library (which also provides StaticClass.StaticMethod), and a second class library. (Everything references the first class library, and the console application also references the second class library.)

I am comparing the identical, expression-bodied GetHashCode overrides defined by the following classes:

  • Lib (a class that lives in the same class library responsible for StaticClass.StaticMethod)
  • OtherLib (a class that lives in the other class library)
  • ConsoleApplication (a class that lives in the console application)
  • LibGenerated (a class generated at runtime by the first class library, into new assembly AssemblyA).
  • Lib2Generated (a class generated at runtime by the other class library, into new assembly AssemblyB).
  • ConsoleApplicationGenerated (a class generated at runtime by the console application, into new assembly AssemblyC).

Here are the byte arrays of the corresponding implementations in Debug mode:

Lib:                            02|40|19|00|00|06|42
OtherLib:                       02|40|13|00|00|10|42
ConsoleApplication:             02|40|14|00|00|10|42
LibGenerated:                   02|40|01|00|00|10|42
Lib2Generated:                  02|40|01|00|00|10|42
ConsoleApplicationGenerated:    02|40|01|00|00|10|42

Here they are in Release mode (where the only difference is that ConsoleApplication now matches OtherLib):

Lib:                            02|40|19|00|00|06|42
OtherLib:                       02|40|13|00|00|10|42
ConsoleApplication:             02|40|13|00|00|10|42
LibGenerated:                   02|40|01|00|00|10|42
Lib2Generated:                  02|40|01|00|00|10|42
ConsoleApplicationGenerated:    02|40|01|00|00|10|42

Looking at the results, we can note the following:

  • The hardcoded type in the same assembly as StaticClass.StaticMethod is the only one that has a different second-to-last byte.
  • In Debug mode, the third byte is different for each of the hardcoded types, and they differ from the generated types as well.
  • In Release mode, the third byte is different for the assembly containing StaticClass.StaticMethod, whereas the other hardcoded implementations have the exact same method bodies.
  • The generated types have the exact same method bodies (even though they, too, live in different assemblies, with differently named modules).

Naturally, Release mode is of primary interest here.

My questions are as follows:

  1. What explains the differences in the IL method bodies?
  2. How can I determine at runtime, in a somewhat reliable way, whether the implementations do the same thing?

Update based on comments by thehenny:

The call from the assembly that contains StaticClass.StaticMethod is different because it uses MethodDef (which apparently can be used to call a method in the same assembly), whereas external assemblies use MethodRef.

Additional question: Let's say the StaticClass is in a NuGet package, and the logic to test methods for a particular implementation is in there as well. Now this package needs an example implementation to compare methods to. However, it cannot define that by itself, as it will differ (because of MethodDef). It can't reference a subdependency either, because to access StaticClass.StaticMethod would need to access it in return, creating a circular reference. If we could create an assembly at runtime that produces the MethodRef result similar to that of a hardcoded implementation in a separate assembly, this would solve the problem. How can do achieve that? Or actually, it is the third byte that differs here, so the difference might be in another detail entirely. What is it?

Timo
  • 7,992
  • 4
  • 49
  • 67
  • 1
    1) Probably one time the call opcodes operand is a MethodRef token the other time a MethodDef token. 2) You could resolve the token and then you know if they point to the same thing, look here: https://github.com/jbevain/mono.reflection/blob/master/Mono.Reflection/MethodBodyReader.cs – thehennyy Apr 05 '20 at 13:29
  • @thehennyy I will try it out. Could you explain what the difference is between MethodRef and MethodDef and why they would differ here? – Timo Apr 05 '20 at 13:31
  • @thehennyy The linked class seems to require to types that I do not have, `ByteBuffer` and `Instruction`. Are these Mono-specific? – Timo Apr 05 '20 at 13:35
  • 1
    Afaik a MethodDef token can be used in the assembly containing the implementation while the a MethodRef has to be used in an external assembly. See the ECMA-335 spec III.3.19. About the two classes, they are not something fancy, just containers. For your problem look for the "resolve[...]" calls in the referenced MethodBodyReader. – thehennyy Apr 05 '20 at 13:45
  • @thehennyy Awesome. I can confirm that you're right: The calls from the same assembly use MethodDef, whereas the others use MethodRef. Any idea why the runtime-emitted types are different from both the other variants? And more importantly, if the static class is in a NuGet package ("AssemblyA"), could it produce the variant that would be produced by another assembly? I presume it would need to reference another assembly, "AssemblyB", but AssemblyB would need to reference AssemblyA to access that static class. Circular reference! **If I could emit the MethodRef result, this might be solved.** – Timo Apr 05 '20 at 13:59
  • 1
    Neither a MethodRef nor a MethodDef metadata token is stable across multiple compilations. Also each token is only meaningful inside the assembly (/module) it defines. Thus the only reliable method to compare these tokens is to resolve them first and then compare the resulting `MethodInfo` or `TypeInfo` instances. – thehennyy Apr 06 '20 at 09:53

0 Answers0