Skip to content
← Tous les articles
· Delta1 Labs Décompilation.NETF#

Décompiler des assemblages compilés en F# dans un décompilateur C# : lire les unions, les enregistrements et les modules

F# compile vers le même IL que C#, donc un décompilateur C# affiche les assemblages F# en C#. Voici comment apparaissent les unions discriminées, les enregistrements, les modules et les fonctions curryfiées — et ce que vous disent les métadonnées CompilationMapping.

Si vous avez déjà ouvert une DLL compilée en F# dans un décompilateur en espérant voir let et match et que vous avez plutôt trouvé des classes aux noms comme Tag et NewCircle, cet article est pour vous. F# et C# sont des langages différents, mais ils partagent un même back end : tous deux compilent vers des métadonnées CLI ECMA-335 et de l’IL. Un décompilateur reconstruit du C# à partir de cet IL, donc pointer un décompilateur C# comme Glass sur un assemblage F# vous donne un rendu C# fidèle et lisible — quoique légèrement inhabituel — de la sortie du compilateur F#. Savoir comment chaque construction F# est abaissée fait passer ce rendu de déroutant à évident.

L’empreinte : FSharp.Core et CompilationMapping

Deux choses marquent immédiatement un assemblage comme d’origine F#. D’abord, une référence à FSharp.Core.dll, le runtime dont dépend tout programme F#. Ensuite, l’attribut [CompilationMapping] parsemé sur les types et les membres. Cet attribut porte une valeur de l’énumération SourceConstructFlags — Module, SumType, RecordType, UnionCase, Field, Value et compagnie — qui enregistre ce que l’élément était dans le code source F#. Un décompilateur C# imprime ces attributs tels quels, ils font donc aussi office de légende : un type étiqueté SourceConstructFlags.SumType est une union discriminée, un type étiqueté Module est un module F#, et ainsi de suite.

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 exemple détaillé

Prenez un petit fragment idiomatique 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

Décompilé en C#, Glass montre à peu près cette forme (tronquée pour la clarté) :

[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; }
        }
    }
}

Chaque construction F# est désormais lisible comme un motif C# : l’union est devenue une base abstraite avec des sous-classes imbriquées et un Tag entier ; l’enregistrement est devenu une classe scellée portant les Equals/GetHashCode/CompareTo structurels que le compilateur F# synthétise ; le module est devenu une classe statique ; le match est devenu un switch sur Tag. La lecture des motifs, une fois le mapping connu, est mécanique.

Le reste du vocabulaire

  • Fonctions et curryfication. Une fonction liée par let sur un module est une méthode statique. Une fonction curryfiée ou partiellement appliquée fait surface sous forme d’une valeur FSharpFunc<,>, souvent avec une classe de clôture générée par le compilateur qui contient les arguments capturés. Le code d’ordre supérieur se lit comme des invocations de FSharpFunc plutôt que des lambdas.
  • Tuples. Les tuples F# deviennent System.Tuple<...> (tuples par référence) ou System.ValueTuple<...> pour les tuples de type struct ; les noms d’éléments ne sont pas préservés au niveau de l’IL.
  • Options. 'T option est FSharpOption<T> avec Some/None ; None est la référence nulle, raison pour laquelle on voit parfois des vérifications de null là où le F# disait None.
  • Point d’entrée. [<EntryPoint>] devient un static int Main(string[]) étiqueté avec [CompilationMapping(SourceConstructFlags.Value)] plus un marquage de style [EntryPoint].
  • Cas d’union sans argument (comme une étiquette de style None) sont généralement exposés comme des instances singleton en cache plutôt que comme de nouvelles allocations.

Lire une bibliothèque F# inconnue

C’est là qu’un décompilateur C# gagne son pain : vous avez un assemblage F# tiers sans code source, et vous devez comprendre sa surface publique. Dans Glass, ouvrez l’assemblage, et les flags [CompilationMapping] vous disent immédiatement quels types sont des unions, lesquels sont des enregistrements, lesquels sont de simples classes. Dépliez une union pour voir ses cas et ses payloads ; dépliez la classe statique d’un module pour lire les fonctions ; suivez un switch (x.Tag) pour voir comment une fonction se ramifie sur une union. Comme F# ne supprime ni ne mutile les noms, les identifiants que vous voyez sont ceux qu’a écrits l’auteur — la seule traduction que vous faites dans votre tête est construction-F# → forme-C#, et la légende CompilationMapping vous la donne gratuitement.

La limite honnête : vous lisez la forme abaissée. Les expressions de calcul ont été désucrées en appels Bind/Return, les fonctions inline peuvent s’être évanouies dans leurs sites d’appel, et les patterns actifs apparaissent comme des appels de méthode ordinaires renvoyant des options. Vous pouvez toujours récupérer le comportement ; vous ne pouvez pas toujours récupérer l’élégant F# qui l’a produit.

Faire cela avec Glass

Glass est un décompilateur .NET gratuit et natif de VS, et parce qu’il reconstruit du C# à partir des métadonnées CLI, il lit les assemblages compilés en F# aussi facilement que ceux en C# — en montrant les attributs [CompilationMapping], les hiérarchies de classes d’union et les membres d’égalité des enregistrements — pour que vous puissiez naviguer dans une bibliothèque F# inconnue de la même façon que n’importe quel assemblage .NET. C’est le même flux de travail que décompiler des enregistrements C# ou des machines à états d’itérateurs ; F# ajoute juste une légende à apprendre.

Récupérez-le depuis la page Glass — c’est gratuit — et ouvrez votre premier assemblage F#.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.