Decompilar métodos de interfaz por defecto: un cuerpo de método dentro de una interfaz, la clase que nunca lo sobrescribió y el cast que no puedes eliminar
Un método de interfaz por defecto rompe la regla más antigua que un decompilador conoce sobre las interfaces: que contienen firmas, no cuerpos. Desde C# 8 una interfaz puede incluir una implementación funcional, y una clase que implementa la interfaz puede no aportar absolutamente nada y aun así obtener el comportamiento. Eso deja tres rompecabezas para Glass.NET: un método de instancia virtual con cuerpo dentro de una interfaz (ni abstracto ni método de clase), una clase cuyos metadatos no contienen ningún override ni ningún MethodImpl pero que se comporta como si los tuviera, y un sitio de llamada que debe pasar por el tipo de la interfaz porque el miembro es invisible en la clase. Esta entrada lee el IL de los tres —el newslot virtual con cuerpo, el MethodImpl ausente, el callvirt contra la interfaz— y muestra cómo Glass reconstruye la implementación por defecto, deja la clase implementadora correctamente vacía y conserva el cast a la interfaz que el lenguaje realmente exige.
Desde que existen decompiladores de .NET, una regla se ha mantenido: las interfaces declaran firmas, las clases aportan cuerpos. Un método de interfaz por defecto, introducido en C# 8 sobre .NET Core 3.0, rompe esa regla limpiamente: una interfaz ahora puede llevar una implementación funcional, y una clase que implementa la interfaz puede aportar nada y aun así obtener el comportamiento. Eso deja a Glass.NET tres cosas que reconstruir que los decompiladores más antiguos nunca tuvieron que modelar, y equivocarse en cualquiera de ellas produce código que parece plausible pero no compila.
Un cuerpo donde las interfaces nunca tuvieron uno
Empieza por el código fuente:
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 tiene cuerpo, y ConsoleLogger implementa la interfaz sin escribir una sola línea. Compílalo y mira la interfaz 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
}
El método es newslot virtual —la misma forma que hace despachable a cualquier miembro de interfaz—, pero, y esto es crucial, no está marcado como abstract, y tiene cuerpo. Esa combinación concreta (no abstracto, virtual, con cuerpo presente, declarado en una interfaz) es la pista. Un miembro de interfaz abstracto sería .method public hidebysig newslot abstract virtual instance void Log(string) cil managed { }: el flag abstract, sin IL. Cuando Glass.NET recorre la interfaz y encuentra código adjunto a un virtual no abstracto, sabe que está ante una implementación por defecto y renderiza el cuerpo en la interfaz en lugar de tratar la interfaz como un montón de firmas.
Fíjate en lo que puede tocar el cuerpo: una interfaz no tiene campos de instancia, así que this dentro de Log solo puede alcanzar otros miembros de la interfaz, y lo hace virtualmente: callvirt ILogger::get_Prefix(). Glass lo lee de vuelta como Prefix, despachado a través de la interfaz, que es exactamente lo que el lenguaje quiere decir.
La clase que no sobrescribió nada
Ahora ConsoleLogger. La tentación —y la trampa en la que cae un decompilador ingenuo— es mostrarla con un método Log «para que el comportamiento tenga sentido». Pero los metadatos son tajantes: la clase no tiene ningún método Log, y no hay ninguna fila MethodImpl que mapee un método de la clase sobre ILogger::Log. Está genuinamente vacía:
.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
}
El comportamiento vive por completo en la interfaz; el runtime lo encuentra mediante despacho de interfaz en el sitio de llamada. Así que Glass.NET renderiza la clase exactamente como es —public sealed class ConsoleLogger : ILogger { }— y no sintetiza ningún método para explicar nada. Un MethodImpl aparece solo cuando la clase sobrescribe realmente el valor por defecto, y entonces Glass muestra el override. La ausencia de un MethodImpl es información: significa que el valor por defecto se aceptó.
El cast que no puedes eliminar
Aquí está el sitio de llamada, y la parte que hace tropezar a la reconstrucción:
ILogger logger = new ConsoleLogger();
logger.Log("started"); // compiles
// new ConsoleLogger().Log("started"); // does NOT compile
Un método de interfaz por defecto es un miembro de la interfaz, no de la clase. Si mantienes el receptor como ConsoleLogger, Log no está en la lista de miembros de ese tipo y la llamada no compila. En IL, por eso la llamada es siempre un callvirt contra el método de la interfaz con el receptor tipado como la interfaz:
newobj instance void ConsoleLogger::.ctor()
callvirt instance void ILogger::Log(string)
Cuando Glass.NET reconstruye esto, no puede eliminar el cast ni volver a tipar la variable como ConsoleLogger para ordenar el código: hacerlo emitiría código fuente que no compila. El receptor tipado como la interfaz es una parte estructural de la regla del lenguaje, no ruido sobrante. Así que Glass mantiene la variable como ILogger (o inserta el cast ((ILogger)x) donde el valor original era concreto), preservando la única forma en la que la llamada es legal. Esta es la diferencia entre un decompilador que reproduce el comportamiento y uno que reproduce un comportamiento compilable.
El diamante, donde la clase deja de estar vacía
El único caso en que una clase implementadora se ve obligada a llevar un método es la ambigüedad. Si una clase implementa dos interfaces que aportan cada una un valor por defecto para la misma firma, el lenguaje se niega a elegir por ti: la clase debe sobrescribir, y ese override lleva una fila MethodImpl que mapea el método de la clase sobre el slot de la interfaz:
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
}
Esa llamada explícita a la base se compila como un call no virtual a IA::M sobre un this restringido a la interfaz, que es la forma de nombrar la implementación por defecto de una interfaz concreta. Glass.NET ve el MethodImpl, renderiza C.M y lee de vuelta el call restringido como la forma ((IA)this).M(). Así que la presencia o ausencia de un MethodImpl es toda la historia: ausente significa que el valor por defecto se aceptó sin ambigüedad; presente significa un override deliberado o una desambiguación forzada, y Glass muestra cuál.
La conclusión
Los métodos de interfaz por defecto mueven tres cosas que un decompilador daba por sentadas. El cuerpo ahora vive en la interfaz: Glass lo detecta como un virtual no abstracto con IL y renderiza ahí el valor por defecto. La clase implementadora puede estar legítimamente vacía, sin método y sin MethodImpl, y Glass se resiste a inventar uno para llenar el silencio. Y el sitio de llamada debe pasar por el tipo de la interfaz, así que Glass conserva el cast que el lenguaje exige en lugar de optimizarlo hasta convertirlo en código que no compila. Lee los metadatos al pie de la letra —cuerpo presente, MethodImpl ausente, receptor tipado como la interfaz— y la característica se decompila en código fuente que a la vez se lee correctamente y compila.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.