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.
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 deFSharpFuncplutôt que des lambdas. - Tuples. Les tuples F# deviennent
System.Tuple<...>(tuples par référence) ouSystem.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 optionestFSharpOption<T>avecSome/None;Noneest la référence nulle, raison pour laquelle on voit parfois des vérifications de null là où le F# disaitNone. - Point d’entrée.
[<EntryPoint>]devient unstatic 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.