Décompiler les méthodes d'interface par défaut : un corps de méthode dans une interface, la classe qui n'a jamais rien redéfini, et le transtypage que l'on ne peut pas supprimer
Une méthode d'interface par défaut enfreint la plus ancienne règle qu'un décompilateur connaisse au sujet des interfaces — qu'elles contiennent des signatures, pas des corps. Depuis C# 8, une interface peut livrer une implémentation fonctionnelle, et une classe qui implémente l'interface peut ne rien fournir du tout et obtenir malgré tout le comportement. Cela laisse trois énigmes à Glass.NET : une méthode d'instance virtuelle dotée d'un corps, posée dans une interface (ni abstraite, ni méthode de classe), une classe dont les métadonnées ne contiennent ni redéfinition ni MethodImpl mais qui se comporte comme si elle en avait une, et un site d'appel qui doit passer par le type d'interface parce que le membre est invisible sur la classe. Cet article lit l'IL des trois cas — le newslot virtual doté d'un corps, le MethodImpl absent, le callvirt contre l'interface — et montre comment Glass reconstruit l'implémentation par défaut, laisse la classe implémentante correctement vide, et conserve le transtypage vers l'interface que le langage exige réellement.
Depuis qu’il existe des décompilateurs .NET, une règle a toujours tenu : les interfaces déclarent des signatures, les classes fournissent des corps. Une méthode d’interface par défaut, introduite avec C# 8 sur .NET Core 3.0, brise proprement cette règle — une interface peut désormais porter une implémentation fonctionnelle, et une classe qui implémente l’interface peut ne fournir rien et obtenir malgré tout le comportement. Cela laisse à Glass.NET trois choses à reconstruire que les décompilateurs plus anciens n’ont jamais eu à modéliser, et se tromper sur l’une d’elles produit du code qui paraît plausible mais ne compilera pas.
Un corps là où les interfaces n’en ont jamais eu
Partons de la source :
public interface ILogger
{
string Prefix => "[log]"; // default property getter
void Log(string message) // default method
=> Console.WriteLine($"{Prefix}: {message}");
}
public sealed class ConsoleLogger : ILogger { } // overrides nothing
ILogger.Log a un corps, et ConsoleLogger implémente l’interface sans écrire une seule ligne. Compilez et regardez l’interface en IL :
.class interface public abstract auto ansi ILogger
{
.method public hidebysig newslot virtual instance void
Log(string message) cil managed
{
.maxstack 3
ldarg.0
callvirt instance string ILogger::get_Prefix()
ldarg.1
...
call void [System.Console]System.Console::WriteLine(string)
ret
}
// get_Prefix is likewise newslot virtual instance with a body
}
La méthode est newslot virtual — la même forme qui rend n’importe quel membre d’interface dispatchable — mais, point crucial, elle n’est pas marquée abstract, et elle a un corps. Cette seule combinaison (non abstraite, virtuelle, corps présent, déclarée sur une interface) est l’indice. Un membre d’interface abstrait serait .method public hidebysig newslot abstract virtual instance void Log(string) cil managed { } — l’indicateur abstract, pas d’IL. Lorsque Glass.NET parcourt l’interface et trouve du code attaché à une méthode virtuelle non abstraite, il sait qu’il a affaire à une implémentation par défaut et rend le corps sur l’interface plutôt que de traiter l’interface comme un tas de signatures.
Notez ce que le corps peut toucher : une interface n’a pas de champs d’instance, de sorte que this à l’intérieur de Log ne peut atteindre que d’autres membres de l’interface, et il le fait virtuellement — callvirt ILogger::get_Prefix(). Glass le relit comme Prefix, dispatché via l’interface, ce qui est exactement ce que le langage veut dire.
La classe qui n’a rien redéfini
Passons à ConsoleLogger. La tentation — et le piège dans lequel tombe un décompilateur naïf — est de l’afficher avec une méthode Log « pour que le comportement ait un sens ». Mais les métadonnées sont catégoriques : la classe n’a pas de méthode Log, et il n’existe aucune ligne MethodImpl associant une méthode de classe à ILogger::Log. Elle est véritablement vide :
.class public auto ansi sealed ConsoleLogger
extends [System.Runtime]System.Object
implements ILogger
{
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed { ... } // just the ctor
}
Le comportement réside entièrement sur l’interface ; le runtime le trouve par dispatch d’interface au site d’appel. Glass.NET rend donc la classe exactement telle qu’elle est — public sealed class ConsoleLogger : ILogger { } — et ne synthétise aucune méthode pour expliquer quoi que ce soit. Un MethodImpl n’apparaît que lorsque la classe redéfinit réellement la valeur par défaut, et Glass montre alors la redéfinition. L’absence de MethodImpl est une information : elle signifie que la valeur par défaut a été acceptée.
Le transtypage que l’on ne peut pas supprimer
Voici le site d’appel, et la partie qui fait trébucher la reconstruction :
ILogger logger = new ConsoleLogger();
logger.Log("started"); // compiles
// new ConsoleLogger().Log("started"); // does NOT compile
Une méthode d’interface par défaut est un membre de l’interface, pas de la classe. Si vous conservez le récepteur en tant que ConsoleLogger, Log ne figure pas dans la liste des membres de ce type et l’appel ne compile pas. En IL, c’est pourquoi l’appel est toujours un callvirt contre la méthode d’interface, avec le récepteur typé comme l’interface :
newobj instance void ConsoleLogger::.ctor()
callvirt instance void ILogger::Log(string)
Lorsque Glass.NET reconstruit cela, il ne peut pas supprimer le transtypage ni retyper la variable en ConsoleLogger pour faire plus propre — cela émettrait une source qui ne compile pas. Le récepteur typé comme l’interface est une partie porteuse de la règle du langage, pas un résidu parasite. Glass conserve donc la variable en ILogger (ou insère le transtypage ((ILogger)x) là où la valeur d’origine était concrète), préservant la seule forme sous laquelle l’appel est légal. C’est la différence entre un décompilateur qui reproduit un comportement et un décompilateur qui reproduit un comportement compilable.
Le diamant, où la classe cesse d’être vide
Le seul cas où une classe implémentante est contrainte de porter une méthode est l’ambiguïté. Si une classe implémente deux interfaces qui fournissent chacune une valeur par défaut pour la même signature, le langage refuse de choisir à votre place — la classe doit redéfinir, et cette redéfinition porte une ligne MethodImpl associant la méthode de classe à l’emplacement d’interface :
interface IA { void M() => Console.WriteLine("A"); }
interface IB { void M() => Console.WriteLine("B"); }
class C : IA, IB
{
public void M() => ((IA)this).M(); // explicit base-interface call → IA's default
}
Cet appel explicite à la base se compile en un call non virtuel vers IA::M sur un this contraint à l’interface, ce qui est la façon de désigner l’implémentation par défaut d’une interface précise. Glass.NET voit le MethodImpl, rend C.M, et relit le call contraint sous la forme ((IA)this).M(). La présence ou l’absence d’un MethodImpl raconte donc toute l’histoire : absent, la valeur par défaut a été acceptée sans ambiguïté ; présent, il s’agit d’une redéfinition délibérée ou d’une levée d’ambiguïté forcée, et Glass montre laquelle.
À retenir
Les méthodes d’interface par défaut déplacent trois choses qu’un décompilateur tenait pour acquises. Le corps vit désormais sur l’interface — Glass le repère comme une méthode virtuelle non abstraite dotée d’IL et y rend l’implémentation par défaut. La classe implémentante peut légitimement être vide, sans méthode ni MethodImpl, et Glass se garde d’en inventer une pour combler le silence. Et le site d’appel doit passer par le type d’interface, de sorte que Glass conserve le transtypage que le langage exige au lieu de l’optimiser en un code qui ne compilera pas. Lisez les métadonnées littéralement — corps présent, MethodImpl absent, récepteur typé comme l’interface — et la fonctionnalité se décompile en une source qui à la fois se lit correctement et se compile.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.