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.
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 deFSharpFuncen lugar de lambdas. - Tuplas. Las tuplas de F# se convierten en
System.Tuple<...>(tuplas por referencia) oSystem.ValueTuple<...>para las tuplas de tipo struct; los nombres de los elementos no se preservan a nivel de IL. - Options.
'T optionesFSharpOption<T>conSome/None;Nonees la referencia nula, razón por la que a veces ves comprobaciones de null donde el F# decíaNone. - Punto de entrada.
[<EntryPoint>]se convierte en unstatic 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.