Skip to content
← Todas las publicaciones
· Delta1 Labs Glass.NETDecompilación.NETAnálisis a fondo

Decompilar funciones locales en Glass.NET: mangling de nombres y cierres por struct ref

Una función local no es una lambda, y el compilador lo demuestra en el IL. No hay asignación de delegado ni clase de cierre: una función local que no captura nada se convierte en un método estático normal con un nombre con mangling como <Run>g__Add|0_0, y una que captura se convierte en un método que recibe sus variables capturadas por ref en un struct de visualización de tipo valor, así que nada aterriza en el heap. Aquí está exactamente qué emite el compilador de C# para ambos tipos, cómo un decompilador que no reconoce el patrón te entrega el método con mangling y el struct ref, y cómo Glass.NET lo repliega a la función local que escribiste.

Una función local parece la hermana más callada de una lambda —un pequeño método metido dentro de otro método— y a menudo se asume que compilan a lo mismo. No es así, y la diferencia es toda la razón de que existan las funciones locales: sin delegado, sin cierre asignado en el heap. El compilador de C# reduce una función local a un método real, y cuando esa función captura una variable pasa el estado capturado por ref en un struct de tipo valor, así que no se asigna nada en el heap solo para llamar a un ayudante que escribiste en línea.

Esa reducción es lo bastante distintiva como para que leerla sea una buena forma de entender qué está haciendo realmente el compilador por ti. Este es un análisis práctico de exactamente eso: los nombres de método con mangling, el struct de visualización ref, el IL del sitio de llamada para los casos con y sin captura, y cómo Glass.NET reconoce toda la forma y la repliega a la función local que escribiste, en lugar de entregarte un método estático llamado <Sum>g__Add|0_0 y un struct que tienes que aplicar ingeniería inversa.

El código fuente

Un método, dos funciones locales. Add captura el total envolvente; Twice no captura nada y está marcada static para decirlo.

public static class Demo
{
    public static int Sum(int[] xs)
    {
        int total = 0;

        void Add(int v) => total += v;       // captures `total`
        static int Twice(int v) => v * 2;    // captures nothing

        foreach (var x in xs)
            Add(Twice(x));

        return total;
    }
}

Dos funciones locales que se leen casi idénticas en el código fuente compilan a dos formas muy distintas, porque una toca una variable envolvente y la otra no.

Qué emite el compilador

Twice es la fácil. No captura nada, así que se convierte en un método estático privado plano: lo único inusual es el nombre, al que el compilador aplica mangling para que nunca puedas escribirlo tú:

.method assembly hidebysig static int32 '<Sum>g__Twice|0_1'(int32 v) cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    ldarg.0
    ldc.i4.2
    mul
    ret
}

El nombre es <Sum>g__Twice|0_1: el método envolvente entre corchetes angulares, g__, el nombre original de la función local, una barra vertical y un par ordinal/contador que lo mantiene único. El nombre entre g__ y | es tu nombre, Twice, conservado tal cual, que es precisamente lo que permite a un decompilador recuperarlo.

Add es la interesante. Captura total, así que el compilador eleva total a un struct de visualización de tipo valor y da a Add un parámetro ref a él:

.class nested private sealed sequential ansi '<>c__DisplayClass0_0'
    extends [mscorlib]System.ValueType       // a struct, not a class
{
    .field public int32 total
}

.method assembly hidebysig static void '<Sum>g__Add|0_0'(
    int32 v,
    valuetype Demo/'<>c__DisplayClass0_0'& 'display')   // passed by ref
{
    ldarg.1              // ref display
    ldarg.1
    ldfld int32 Demo/'<>c__DisplayClass0_0'::total
    ldarg.0              // v
    add
    stfld int32 Demo/'<>c__DisplayClass0_0'::total
    ret
}

Este es el meollo. Una lambda que capturara total necesitaría un objeto de cierre de tipo referencia, asignado en el heap, con total como campo, invocado a través de un delegado Func/Action. La función local no necesita nada de eso: el tipo de visualización es un struct (extends System.ValueType), vive en la pila del llamante, y Add lo recibe por ref para que sus escrituras a total sean visibles de vuelta en Sum. Cero asignación, sin delegado.

El sitio de llamada en Sum hace explícito el paso por ref. El struct de visualización es una variable local; las llamadas toman su dirección con ldloca e invocan los métodos con mangling directamente con call, sin newobj, sin callvirt a través de un delegado:

.locals init (valuetype Demo/'<>c__DisplayClass0_0' display, ...)
ldloca.s   display
ldflda     int32 Demo/'<>c__DisplayClass0_0'::total   // &display.total = 0
...
// inside the loop, for Add(Twice(x)):
ldloca.s   x_current
call       int32 Demo::'<Sum>g__Twice|0_1'(int32)     // Twice(x) — plain static call
ldloca.s   display
call       void  Demo::'<Sum>g__Add|0_0'(int32, valuetype Demo/'<>c__DisplayClass0_0'&)
fuentevoid Add(int v) => total += v;captura totalIL reducidostruct <>c__DisplayClass0_0 { int total; }<Sum>g__Add|0_0( int v, ref display)ldloca.s displaycall <Sum>g__Add|0_0sin newobj · sin delegado · en la pilareconstrucción de Glassvoid Add(int v) => total += v;función local anidada

Qué muestra un decompilador ingenuo

Un decompilador que no modela el patrón es fiel a los metadatos y por tanto ilegible. Lista <Sum>g__Twice|0_1 y <Sum>g__Add|0_0 como métodos estáticos hermanos de la clase —no anidados en Sum en absoluto— y saca a la superficie el struct de visualización como un tipo real con un parámetro ref que te deja interpretar:

// Naive output: structure lost, the capture mechanism exposed.
[StructLayout(LayoutKind.Auto)]
private struct <>c__DisplayClass0_0 { public int total; }

[CompilerGenerated]
private static void <Sum>g__Add|0_0(int v, ref <>c__DisplayClass0_0 display)
    => display.total += v;

[CompilerGenerated]
private static int <Sum>g__Twice|0_1(int v) => v * 2;

public static int Sum(int[] xs)
{
    <>c__DisplayClass0_0 display = default;
    display.total = 0;
    foreach (int x in xs)
        <Sum>g__Add|0_0(<Sum>g__Twice|0_1(x), ref display);
    return display.total;
}

Todo eso es correcto y nada de ello es lo que escribiste. La localidad se ha perdido —Add y Twice ya no están dentro de Sum— y el cableado ref display que el compilador añadió por ti es ahora algo que un lector tiene que descifrar.

Qué reconstruye Glass

Glass está construido sobre ICSharpCode.Decompiler, el motor detrás de ILSpy, que reconoce toda la forma: el patrón de nombre <Envolvente>g__Nombre|n_m, la marca [CompilerGenerated] y el struct de visualización de tipo valor pasado por ref. Revierte las tres. Los métodos con mangling vuelven a anidarse dentro de Sum como funciones locales nombradas por el texto entre g__ y |; el struct de visualización y su hilado ref se disuelven de vuelta en una variable capturada plana; y el static de Twice se restaura porque su firma no muestra estado capturado:

// Glass output: the local functions you actually wrote.
public static int Sum(int[] xs)
{
    int total = 0;

    void Add(int v) => total += v;
    static int Twice(int v) => v * 2;

    foreach (int x in xs)
        Add(Twice(x));

    return total;
}

Vale la pena nombrar la diferencia con el caso de la lambda, porque Glass también la muestra correctamente. Si Add se hubiera escrito como Action<int> add = v => total += v;, la reconstrucción mostraría un delegado respaldado por un cierre de tipo referencia: un newobj de una clase de visualización, variables capturadas como sus campos, e invocación a través de Action. Aquí no hay delegado por ninguna parte, y Glass no inventa uno: muestra un método anidado y una variable local capturada ordinaria, que es la verdad del IL. Leer los dos uno al lado del otro en Glass es la forma más clara de ver, en tu propio código, por qué la recomendación “prefiere una función local a una lambda cuando no estás almacenando un delegado” trata de asignaciones, no de estilo.

Cuándo bajar a la vista de IL

La reconstrucción es lo que quieres la mayoría de las veces, pero unas pocas cosas sobre las funciones locales solo son visibles por debajo de ella, y Glass te deja bajar directamente al IL para el mismo miembro:

  • ¿La captura fue por ref o por valor? El struct de visualización se pasa por ref para que las escrituras se propaguen; confirmarlo en el IL (valuetype ...DisplayClass...&) es cómo razonas si una mutación dentro de la función local es vista por el llamante.
  • ¿El compilador eliminó el struct de visualización por completo? Para una función genuinamente sin captura verás un método estático desnudo con solo sus parámetros declarados y sin struct: la prueba de que marcarla static no costó nada ni asignó nada.
  • Colisiones de ordinales y sobrecargas. El sufijo |n_m desambigua múltiples funciones locales y múltiples ámbitos léxicos; la vista de IL es donde emparejas un método con mangling específico con una posición de código específica cuando un método tiene varias.

Lee el C# para recuperar intención y estructura; baja al IL cuando necesites demostrarte a ti mismo exactamente cómo se cableó la captura. Que ambas vistas concuerden —una función local anidada arriba, un método estático con mangling y un struct ref debajo— es todo el punto: Glass te muestra el código que escribiste sin ocultar la maquinaria que lo hace barato.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.