Skip to content
← Alle Beiträge
· Delta1 Labs Glass.NETDekompilierung.NETTiefenanalyse

Default-Interface-Methoden dekompilieren: ein Methoden-Body im Interface, die Klasse, die nie überschrieben hat, und der Cast, den man nicht weglassen darf

Eine Default-Interface-Methode bricht die älteste Regel, die ein Decompiler über Interfaces kennt – dass sie Signaturen enthalten, keine Bodys. Seit C# 8 kann ein Interface eine funktionierende Implementierung mitbringen, und eine Klasse, die das Interface implementiert, kann überhaupt nichts beisteuern und erhält das Verhalten trotzdem. Das stellt Glass.NET vor drei Rätsel: eine virtuelle Instanzmethode mit Body, die in einem Interface sitzt (nicht abstrakt, keine Klassenmethode), eine Klasse, deren Metadaten weder Override noch MethodImpl enthalten und die sich dennoch verhält, als hätte sie eines, und eine Aufrufstelle, die über den Interface-Typ gehen muss, weil das Member auf der Klasse unsichtbar ist. Dieser Beitrag liest die IL für alle drei – das newslot virtual mit Body, das fehlende MethodImpl, das callvirt gegen das Interface – und zeigt, wie Glass die Default-Implementierung rekonstruiert, die implementierende Klasse korrekt leer lässt und den Interface-Cast beibehält, den die Sprache tatsächlich verlangt.

Solange es .NET-Decompiler gibt, galt eine Regel: Interfaces deklarieren Signaturen, Klassen liefern Bodys. Eine Default-Interface-Methode, eingeführt mit C# 8 auf .NET Core 3.0, bricht diese Regel sauber – ein Interface kann jetzt eine funktionierende Implementierung tragen, und eine Klasse, die das Interface implementiert, kann nichts beisteuern und erhält das Verhalten trotzdem. Das lässt Glass.NET mit drei Dingen zurück, die ältere Decompiler nie modellieren mussten – und wer eines davon falsch macht, erzeugt Code, der plausibel aussieht, aber nicht kompiliert.

Ein Body, wo Interfaces nie einen hatten

Beginnen wir mit dem Quelltext:

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 hat einen Body, und ConsoleLogger implementiert das Interface, ohne eine einzige Zeile zu schreiben. Kompilieren Sie es und sehen Sie sich das Interface in IL an:

.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
}

Die Methode ist newslot virtual – dieselbe Form, die jedes Interface-Member dispatchbar macht –, aber entscheidend: Sie ist nicht als abstract markiert, und sie hat einen Body. Diese eine Kombination (nicht abstrakt, virtuell, Body vorhanden, auf einem Interface deklariert) ist das Erkennungsmerkmal. Ein abstraktes Interface-Member wäre .method public hidebysig newslot abstract virtual instance void Log(string) cil managed { } – das abstract-Flag, keine IL. Wenn Glass.NET das Interface durchläuft und Code an einer nicht-abstrakten virtuellen Methode findet, weiß es, dass es eine Default-Implementierung vor sich hat, und rendert den Body auf dem Interface, statt das Interface als Haufen von Signaturen zu behandeln.

Beachten Sie, worauf der Body zugreifen kann: Ein Interface hat keine Instanzfelder, also kann this innerhalb von Log nur andere Interface-Member erreichen, und das tut es virtuell – callvirt ILogger::get_Prefix(). Glass liest das als Prefix zurück, über das Interface dispatcht, was genau dem entspricht, was die Sprache meint.

Body im Interface, nichts in der Klasseinterface ILoggernewslot virtual Log(string)NOT abstract — has IL bodythis.Prefix → callvirt ILoggersealed class ConsoleLogger: ILogger { }keine Methode, keine MethodImpl-ZeileimplementiertAufrufstelleILogger log = new ConsoleLogger();callvirt ILogger::Log(string)Receiver als Interface typisiertDispatchAuflösungKlasse hat kein Override → Interface-Dispatchlandet auf der Default-Implementierung von ILoggervs. abstractabstract-Flagkein BodyKlasse muss impl.

Die Klasse, die nichts überschrieben hat

Nun zu ConsoleLogger. Die Versuchung – und die Falle, in die ein naiver Decompiler tappt – besteht darin, sie mit einer Log-Methode zu zeigen, „damit das Verhalten Sinn ergibt“. Aber die Metadaten sind unerbittlich: Die Klasse hat keine Log-Methode, und es gibt keine MethodImpl-Zeile, die eine Klassenmethode auf ILogger::Log abbildet. Sie ist tatsächlich leer:

.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
}

Das Verhalten lebt vollständig auf dem Interface; die Runtime findet es an der Aufrufstelle über Interface-Dispatch. Glass.NET rendert die Klasse also genau so, wie sie ist – public sealed class ConsoleLogger : ILogger { } – und synthetisiert keine Methode, um irgendetwas zu erklären. Ein MethodImpl erscheint nur, wenn die Klasse den Default tatsächlich überschreibt, und dann zeigt Glass das Override. Das Fehlen eines MethodImpl ist Information: Es bedeutet, dass der Default akzeptiert wurde.

Der Cast, den man nicht weglassen darf

Hier die Aufrufstelle, und der Teil, der die Rekonstruktion ins Stolpern bringt:

ILogger logger = new ConsoleLogger();
logger.Log("started");          // compiles

// new ConsoleLogger().Log("started");   // does NOT compile

Eine Default-Interface-Methode ist ein Member des Interfaces, nicht der Klasse. Halten Sie den Receiver als ConsoleLogger, steht Log nicht in der Member-Liste dieses Typs, und der Aufruf kompiliert nicht. In IL ist das der Grund, warum der Aufruf immer ein callvirt gegen die Interface-Methode ist, mit dem Receiver als Interface typisiert:

newobj     instance void ConsoleLogger::.ctor()
callvirt   instance void ILogger::Log(string)

Wenn Glass.NET das rekonstruiert, darf es den Cast nicht weglassen oder die Variable zum Aufräumen des Codes als ConsoleLogger umtypisieren – das würde Quelltext erzeugen, der nicht kompiliert. Der als Interface typisierte Receiver ist ein tragender Teil der Sprachregel, kein übrig gebliebenes Rauschen. Glass belässt die Variable also bei ILogger (oder fügt den ((ILogger)x)-Cast ein, wo der ursprüngliche Wert konkret war) und bewahrt damit die eine Form, in der der Aufruf legal ist. Das ist der Unterschied zwischen einem Decompiler, der Verhalten reproduziert, und einem, der kompilierbares Verhalten reproduziert.

Der Diamant, bei dem die Klasse aufhört, leer zu sein

Der eine Fall, in dem eine implementierende Klasse gezwungen ist, eine Methode zu tragen, ist Mehrdeutigkeit. Implementiert eine Klasse zwei Interfaces, die jeweils einen Default für dieselbe Signatur liefern, weigert sich die Sprache, für Sie zu wählen – die Klasse muss überschreiben, und dieses Override trägt eine MethodImpl-Zeile, die die Klassenmethode auf den Interface-Slot abbildet:

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
}

Dieser explizite Basisaufruf kompiliert zu einem nicht-virtuellen call auf IA::M mit einem auf das Interface constrainten this – so benennt man die Default-Implementierung eines bestimmten Interfaces. Glass.NET sieht das MethodImpl, rendert C.M und liest den constrainten call als die Form ((IA)this).M() zurück. Das Vorhandensein oder Fehlen eines MethodImpl ist also die ganze Geschichte: Fehlt es, wurde der Default eindeutig akzeptiert; ist es da, handelt es sich um ein bewusstes Override oder eine erzwungene Disambiguierung, und Glass zeigt, welches von beiden.

Das Fazit

Default-Interface-Methoden verschieben drei Dinge, die ein Decompiler früher als selbstverständlich voraussetzte. Der Body lebt jetzt auf dem Interface – Glass erkennt ihn als nicht-abstrakte virtuelle Methode mit IL und rendert den Default dort. Die implementierende Klasse darf legitim leer sein, ohne Methode und ohne MethodImpl, und Glass widersteht der Versuchung, eine zu erfinden, um die Stille zu füllen. Und die Aufrufstelle muss über den Interface-Typ gehen, also behält Glass den Cast, den die Sprache verlangt, statt ihn zu Code wegzuoptimieren, der nicht kompiliert. Lesen Sie die Metadaten wörtlich – Body vorhanden, MethodImpl abwesend, Receiver als Interface typisiert –, und das Feature dekompiliert zu Quelltext, der sich sowohl richtig liest als auch baut.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.