Skip to content
← Todas las publicaciones
· Delta1 Labs Decompilación.NETF#

Decompilar ensamblados compilados con F# en un decompilador de C#: leer uniones, records y módulos

F# compila al mismo IL que C#, así que un decompilador de C# muestra los ensamblados de F# como C#. Aquí tienes cómo aparecen las uniones discriminadas, los records, los módulos y las funciones currificadas, y qué te dicen los metadatos de CompilationMapping.

Si alguna vez has abierto una DLL compilada con F# en un decompilador esperando ver let y match y, en cambio, has encontrado clases con nombres como Tag y NewCircle, este artículo es para ti. F# y C# son lenguajes distintos, pero comparten un mismo back end: ambos compilan a metadatos CLI de ECMA-335 e IL. Un decompilador reconstruye C# a partir de ese IL, así que apuntar un decompilador de C# como Glass a un ensamblado de F# te da una representación en C# fiel y legible —aunque algo inusual— de la salida del compilador de F#. Saber cómo se rebaja cada construcción de F# convierte esa representación de desconcertante en obvia.

La huella: FSharp.Core y CompilationMapping

Dos cosas marcan de inmediato un ensamblado como de origen F#. Primero, una referencia a FSharp.Core.dll, el runtime del que depende todo programa F#. Segundo, el atributo [CompilationMapping] esparcido por tipos y miembros. Ese atributo porta un valor del enum SourceConstructFlags —Module, SumType, RecordType, UnionCase, Field, Value y compañía— que registra lo que el elemento era en el código fuente F#. Un decompilador de C# imprime estos atributos tal cual, así que funcionan además como leyenda: un tipo etiquetado como SourceConstructFlags.SumType es una unión discriminada, uno etiquetado como Module es un módulo de F#, y así sucesivamente.

F# source lowered to IL, then read back as C# F# source type Shape = | Circle of float type Point = {X;Y} IL + metadata classes + Tag [CompilationMapping] FSharp.Core ref Glass C# view abstract Shape + nested Circle Point : IEquatable Names are preserved, so the C# rendering is readable; the CompilationMapping flags say what each type originally was.

Un ejemplo trabajado

Toma un fragmento pequeño e idiomático de F#:

type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float

type Point = { X: float; Y: float }

module Geometry =
    let area shape =
        match shape with
        | Circle r -> System.Math.PI * r * r
        | Rectangle (w, h) -> w * h

Decompilado a C#, Glass muestra aproximadamente esta forma (recortada para mayor claridad):

[CompilationMapping(SourceConstructFlags.SumType)]
public abstract class Shape {
    public abstract int Tag { get; }               // discriminates the cases
    public static class Tags { public const int Circle = 0, Rectangle = 1; }

    [CompilationMapping(SourceConstructFlags.UnionCase, 0)]
    public class Circle : Shape {
        public readonly double radius;
        public override int Tag => 0;
    }
    [CompilationMapping(SourceConstructFlags.UnionCase, 1)]
    public class Rectangle : Shape {
        public readonly double width, height;
        public override int Tag => 1;
    }
}

[CompilationMapping(SourceConstructFlags.RecordType)]
public sealed class Point : IEquatable<Point>, IStructuralEquatable, IComparable<Point>, IComparable {
    public double X { get; }
    public double Y { get; }
    // structural Equals / GetHashCode / CompareTo generated by the F# compiler
}

[CompilationMapping(SourceConstructFlags.Module)]
public static class Geometry {
    public static double area(Shape shape) {
        switch (shape.Tag) {
            case 0: { double r = ((Shape.Circle)shape).radius; return Math.PI * r * r; }
            default: { var rc = (Shape.Rectangle)shape; return rc.width * rc.height; }
        }
    }
}

Cada construcción de F# es ahora legible como un patrón de C#: la unión se convirtió en una base abstracta con subclases anidadas y un Tag entero; el record se convirtió en una clase sellada que porta el Equals/GetHashCode/CompareTo estructural que sintetiza el compilador de F#; el módulo se convirtió en una clase estática; el match se convirtió en un switch sobre Tag. La lectura de patrones, una vez que conoces el mapeo, es mecánica.

El resto del vocabulario

  • Funciones y currificación. Una función ligada con let en un módulo es un método estático. Una función currificada o parcialmente aplicada aflora como un valor FSharpFunc<,>, a menudo con una clase de clausura generada por el compilador que contiene los argumentos capturados. El código de orden superior se lee como invocaciones de FSharpFunc en lugar de lambdas.
  • Tuplas. Las tuplas de F# se convierten en System.Tuple<...> (tuplas por referencia) o System.ValueTuple<...> para las tuplas de tipo struct; los nombres de los elementos no se preservan a nivel de IL.
  • Options. 'T option es FSharpOption<T> con Some/None; None es la referencia nula, razón por la que a veces ves comprobaciones de null donde el F# decía None.
  • Punto de entrada. [<EntryPoint>] se convierte en un static int Main(string[]) etiquetado con [CompilationMapping(SourceConstructFlags.Value)] más un marcado al estilo [EntryPoint].
  • Casos de unión sin argumentos (como una etiqueta estilo None) se exponen normalmente como instancias singleton en caché en lugar de asignaciones nuevas.

Leer una biblioteca F# desconocida

Aquí es donde un decompilador de C# se gana su sustento: tienes un ensamblado F# de terceros sin código fuente y necesitas entender su superficie pública. En Glass, abre el ensamblado y los flags [CompilationMapping] te dicen de inmediato qué tipos son uniones frente a records frente a clases corrientes. Expande una unión para ver sus casos y payloads; expande la clase estática de un módulo para leer las funciones; sigue un switch (x.Tag) para ver cómo una función se ramifica sobre una unión. Como F# no elimina ni retuerce los nombres, los identificadores que ves son los que escribió el autor: la única traducción que haces en tu cabeza es construcción-F# → forma-C#, y la leyenda de CompilationMapping te la entrega gratis.

El límite honesto: estás leyendo la forma rebajada. Las expresiones de computación se han desazucarado en llamadas a Bind/Return, las funciones inline pueden haberse esfumado en sus puntos de llamada y los patrones activos aparecen como llamadas a métodos corrientes que devuelven options. Siempre puedes recuperar el comportamiento; no siempre puedes recuperar el elegante F# que lo produjo.

Hacer esto con Glass

Glass es un decompilador de .NET gratuito y nativo de VS, y como reconstruye C# a partir de metadatos CLI lee los ensamblados compilados con F# con la misma facilidad que los de C# —mostrando los atributos [CompilationMapping], las jerarquías de clases de las uniones y los miembros de igualdad de los records— para que puedas navegar una biblioteca F# desconocida igual que harías con cualquier ensamblado .NET. Es el mismo flujo de trabajo que decompilar records de C# o máquinas de estado de iteradores; F# solo añade una leyenda más que aprender.

Consíguelo desde la página de Glass —es gratis— y abre tu primer ensamblado F#.

Prueba Nebula.NET

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