# Decompiling default interface methods: a method body inside an interface, the class that never overrode it, and the cast you cannot drop


For as long as there have been .NET decompilers, one rule held: interfaces declare signatures, classes supply bodies. A default interface method, introduced in C# 8 on .NET Core 3.0, breaks that rule cleanly — an interface can now carry a working implementation, and a class that implements the interface can supply *nothing* and still get the behaviour. That leaves [Glass.NET](https://delta1labs.com/products/glass) with three things to reconstruct that older decompilers never had to model, and getting any of them wrong produces code that looks plausible but will not compile.

## A body where interfaces never had one

Start with the source:

```csharp
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` has a body, and `ConsoleLogger` implements the interface without writing a single line. Compile it and look at the interface in 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
}
```

The method is `newslot virtual` — the same shape that makes any interface member dispatchable — but, crucially, it is **not** marked `abstract`, and it **has a body**. That single combination (non-abstract, virtual, body present, declared on an interface) is the tell. An abstract interface member would be `.method public hidebysig newslot abstract virtual instance void Log(string) cil managed { }` — the `abstract` flag, no IL. When Glass.NET walks the interface and finds code attached to a non-abstract virtual, it knows it is looking at a default implementation and renders the body on the interface rather than treating the interface as a pile of signatures.

Note what the body can touch: an interface has no instance fields, so `this` inside `Log` can only reach other interface members, and it does so virtually — `callvirt ILogger::get_Prefix()`. Glass reads that back as `Prefix`, dispatched through the interface, which is exactly what the language means.

<figure class="my-8"><svg viewBox="0 0 680 250" role="img" aria-label="Diagram contrasting an abstract interface method and a default interface method, and showing dispatch. On the left, the interface ILogger holds Log as a newslot virtual instance method that is not abstract and has a body. Below it, the class ConsoleLogger implements ILogger but has no method and no MethodImpl row. On the right, a call site holds the object as ILogger and does callvirt to ILogger::Log; because the class has no override, interface dispatch resolves to the default implementation on the interface. A panel notes that an abstract member would have the abstract flag and no body, while a default member drops abstract and carries IL, and that the call must go through the interface type." style="width:100%;height:auto"><defs><style>.ti{font:600 12px system-ui;fill:#1547e0}.h{font:600 11px system-ui;fill:#0f172a}.x{font:10px system-ui;fill:#475569}.mono{font:10px ui-monospace,Consolas,monospace;fill:#1e293b}.iface{fill:#eef4ff;stroke:#8eb4ff}.cls{fill:#f1f5f9;stroke:#cbd5e1}.call{fill:#fef3c7;stroke:#fbbf24}.note{fill:#dcfce7;stroke:#4ade80}.ar{stroke:#64748b;stroke-width:1.4;fill:none;marker-end:url(#a)}.lbl{font:600 9.5px system-ui;fill:#64748b}</style><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#64748b"/></marker></defs><text class="ti" x="12" y="18">Body on the interface, nothing on the class</text><rect class="iface" x="12" y="30" width="230" height="86" rx="5"/><text class="h" x="22" y="50">interface ILogger</text><text class="mono" x="22" y="70">newslot virtual Log(string)</text><text class="mono" x="22" y="86">NOT abstract — has IL body</text><text class="x" x="22" y="104">this.Prefix → callvirt ILogger</text><rect class="cls" x="12" y="134" width="230" height="74" rx="5"/><text class="h" x="22" y="154">sealed class ConsoleLogger</text><text class="mono" x="22" y="174">: ILogger { }</text><text class="x" x="22" y="192">no method, no MethodImpl row</text><path class="ar" d="M127,116 L127,134"/><text class="lbl" x="134" y="128">implements</text><rect class="call" x="300" y="40" width="240" height="78" rx="5"/><text class="h" x="310" y="60">call site</text><text class="mono" x="310" y="80">ILogger log = new ConsoleLogger();</text><text class="mono" x="310" y="98">callvirt ILogger::Log(string)</text><text class="x" x="310" y="112">receiver typed as the interface</text><path class="ar" d="M300,96 C270,96 262,80 244,73"/><text class="lbl" x="250" y="60">dispatch</text><rect class="note" x="300" y="134" width="368" height="74" rx="5"/><text class="h" x="310" y="154">Resolution</text><text class="x" x="310" y="172">class has no override → interface dispatch</text><text class="x" x="310" y="190">lands on ILogger's default implementation</text><rect class="iface" x="560" y="40" width="108" height="78" rx="5"/><text class="h" x="570" y="60">vs abstract</text><text class="x" x="570" y="78">abstract flag</text><text class="x" x="570" y="94">no body</text><text class="x" x="570" y="110">class must impl</text></svg></figure>

## The class that overrode nothing

Now `ConsoleLogger`. The temptation — and the trap a naive decompiler falls into — is to show it with a `Log` method "so the behaviour makes sense." But the metadata is adamant: the class has no `Log` method, and there is no `MethodImpl` row mapping a class method onto `ILogger::Log`. It is genuinely empty:

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

The behaviour lives entirely on the interface; the runtime finds it through interface dispatch at the call site. So Glass.NET renders the class exactly as it is — `public sealed class ConsoleLogger : ILogger { }` — and does not synthesise a method to explain anything. A `MethodImpl` appears only when the class *actually* overrides the default, and then Glass shows the override. Absence of a `MethodImpl` is information: it means the default was accepted.

## The cast you cannot drop

Here is the call site, and the part that trips reconstruction:

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

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

A default interface method is a member of the *interface*, not of the class. If you hold the receiver as `ConsoleLogger`, `Log` is not in that type's member list and the call does not compile. In IL this is why the call is always a `callvirt` against the interface method with the receiver typed as the interface:

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

When Glass.NET reconstructs this, it **cannot** drop the cast or re-type the variable as `ConsoleLogger` to tidy the code up — doing so would emit source that fails to compile. The interface-typed receiver is a load-bearing part of the language rule, not leftover noise. So Glass keeps the variable at `ILogger` (or inserts the `((ILogger)x)` cast where the original value was concrete), preserving the one shape in which the call is legal. This is the difference between a decompiler that reproduces behaviour and one that reproduces *compilable* behaviour.

## The diamond, where the class stops being empty

The one case where an implementing class is *forced* to carry a method is ambiguity. If a class implements two interfaces that each provide a default for the same signature, the language refuses to pick for you — the class must override, and that override carries a `MethodImpl` row mapping the class method onto the interface slot:

```csharp
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
}
```

That explicit base call compiles to a non-virtual `call` to `IA::M` on a `this` constrained to the interface, which is how you name a *specific* interface's default implementation. Glass.NET sees the `MethodImpl`, renders `C.M`, and reads the constrained `call` back as the `((IA)this).M()` form. So the presence or absence of a `MethodImpl` is the whole story: absent means the default was accepted unambiguously; present means a deliberate override or a forced disambiguation, and Glass shows which.

## The takeaway

Default interface methods move three things a decompiler used to take for granted. The body now lives on the interface — Glass spots it as a non-abstract virtual with IL and renders the default there. The implementing class may legitimately be empty, with no method and no `MethodImpl`, and Glass resists inventing one to fill the silence. And the call site must go through the interface type, so Glass keeps the cast that the language requires rather than optimising it away into code that will not compile. Read the metadata literally — body present, `MethodImpl` absent, receiver typed as the interface — and the feature decompiles into source that both reads correctly and builds.

