F#-kompilierte Assemblys in einem C#-Decompiler dekompilieren: Unions, Records und Module lesen
F# kompiliert zum selben IL wie C#, daher zeigt ein C#-Decompiler F#-Assemblys als C# an. Hier erfahren Sie, wie discriminated Unions, Records, Module und curried Funktionen erscheinen — und was Ihnen die CompilationMapping-Metadaten verraten.
Falls Sie je eine F#-gebaute DLL in einem Decompiler geöffnet haben, in der Erwartung, let und match zu sehen, und stattdessen Klassen mit Namen wie Tag und NewCircle vorfanden, ist dieser Beitrag für Sie. F# und C# sind verschiedene Sprachen, aber sie teilen ein Back End: Beide kompilieren zu ECMA-335-CLI-Metadaten und IL. Ein Decompiler rekonstruiert C# aus diesem IL, daher liefert Ihnen das Richten eines C#-Decompilers wie Glass auf eine F#-Assembly eine getreue, lesbare — wenn auch leicht ungewohnte — C#-Darstellung der Ausgabe des F#-Compilers. Zu wissen, wie jedes F#-Konstrukt abgesenkt wird, verwandelt diese Darstellung von verwirrend zu offensichtlich.
Der Fingerabdruck: FSharp.Core und CompilationMapping
Zwei Dinge weisen eine Assembly sofort als F#-Herkunft aus. Erstens eine Referenz zu FSharp.Core.dll, der Laufzeit, von der jedes F#-Programm abhängt. Zweitens das Attribut [CompilationMapping], das über Typen und Member verstreut ist. Dieses Attribut trägt einen Wert der Enumeration SourceConstructFlags — Module, SumType, RecordType, UnionCase, Field, Value und Konsorten —, der festhält, was das Element im F#-Quellcode war. Ein C#-Decompiler gibt diese Attribute unverändert aus, sodass sie zugleich als Legende dienen: Ein Typ mit der Kennzeichnung SourceConstructFlags.SumType ist eine discriminated Union, einer mit Module ein F#-Modul, und so weiter.
Ein durchgearbeitetes Beispiel
Nehmen Sie ein kleines, idiomatisches Stück 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
Nach C# dekompiliert, zeigt Glass ungefähr diese Gestalt (zur Klarheit gekürzt):
[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; }
}
}
}
Jedes F#-Konstrukt ist nun als C#-Muster lesbar: Die Union wurde zu einer abstrakten Basis mit verschachtelten Unterklassen und einem ganzzahligen Tag; der Record wurde zu einer versiegelten Klasse, die das strukturelle Equals/GetHashCode/CompareTo trägt, das der F#-Compiler synthetisiert; das Modul wurde zu einer statischen Klasse; das match wurde zu einem switch über Tag. Das Lesen der Muster ist, sobald Sie das Mapping kennen, mechanisch.
Der Rest des Vokabulars
- Funktionen und Currying. Eine let-gebundene Funktion auf einem Modul ist eine statische Methode. Eine curried oder partiell angewandte Funktion taucht als
FSharpFunc<,>-Wert auf, oft mit einer vom Compiler erzeugten Closure-Klasse, die die erfassten Argumente hält. Code höherer Ordnung liest sich alsFSharpFunc-Aufrufe statt als Lambdas. - Tupel. F#-Tupel werden zu
System.Tuple<...>(Referenz-Tupel) oderSystem.ValueTuple<...>für Struct-Tupel; Elementnamen bleiben auf IL-Ebene nicht erhalten. - Options.
'T optionistFSharpOption<T>mitSome/None;Noneist die Nullreferenz, weshalb Sie manchmal Null-Prüfungen sehen, wo das F#Nonesagte. - Einstiegspunkt.
[<EntryPoint>]wird zu einemstatic int Main(string[]), gekennzeichnet mit[CompilationMapping(SourceConstructFlags.Value)]plus einer Markierung im Stil von[EntryPoint]. - Union-Fälle ohne Argumente (wie ein Tag im Stil von
None) werden üblicherweise als gecachte Singleton-Instanzen statt als frische Allokationen bereitgestellt.
Eine unbekannte F#-Bibliothek lesen
Hier verdient sich ein C#-Decompiler sein Geld: Sie haben eine F#-Drittanbieter-Assembly ohne Quellcode und müssen ihre öffentliche Oberfläche verstehen. Öffnen Sie die Assembly in Glass, und die [CompilationMapping]-Flags sagen Ihnen sofort, welche Typen Unions, welche Records und welche schlichte Klassen sind. Klappen Sie eine Union auf, um ihre Fälle und Payloads zu sehen; klappen Sie die statische Klasse eines Moduls auf, um die Funktionen zu lesen; folgen Sie einem switch (x.Tag), um zu sehen, wie eine Funktion über eine Union verzweigt. Weil F# Namen weder entfernt noch verstümmelt, sind die Bezeichner, die Sie sehen, genau jene, die der Autor geschrieben hat — die einzige Übersetzung, die Sie im Kopf leisten, ist F#-Konstrukt → C#-Gestalt, und die CompilationMapping-Legende liefert Ihnen diese gratis.
Die ehrliche Grenze: Sie lesen die abgesenkte Form. Computation Expressions wurden in Bind/Return-Aufrufe entzuckert, inline-Funktionen können in ihren Aufrufstellen verschwunden sein, und Active Patterns erscheinen als gewöhnliche Methodenaufrufe, die Options zurückgeben. Das Verhalten können Sie immer wiedergewinnen; das elegante F#, das es hervorgebracht hat, nicht immer.
Das mit Glass umsetzen
Glass ist ein kostenloser, VS-nativer .NET-Decompiler, und weil er C# aus CLI-Metadaten rekonstruiert, liest er F#-kompilierte Assemblys ebenso bereitwillig wie C#-Assemblys — er zeigt die [CompilationMapping]-Attribute, die Union-Klassenhierarchien und die Record-Gleichheitsmember, sodass Sie eine unbekannte F#-Bibliothek genauso navigieren wie jede .NET-Assembly. Es ist derselbe Arbeitsablauf wie beim Dekompilieren von C#-Records oder von Iterator-Zustandsmaschinen; F# fügt nur eine Legende hinzu, die man lernen muss.
Holen Sie es sich von der Glass-Seite — es ist kostenlos — und öffnen Sie Ihre erste F#-Assembly.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.