Skip to content
← Alle Beiträge
· Delta1 Labs Dekompilierung.NETF#

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.

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.

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 als FSharpFunc-Aufrufe statt als Lambdas.
  • Tupel. F#-Tupel werden zu System.Tuple<...> (Referenz-Tupel) oder System.ValueTuple<...> für Struct-Tupel; Elementnamen bleiben auf IL-Ebene nicht erhalten.
  • Options. 'T option ist FSharpOption<T> mit Some/None; None ist die Nullreferenz, weshalb Sie manchmal Null-Prüfungen sehen, wo das F# None sagte.
  • Einstiegspunkt. [<EntryPoint>] wird zu einem static 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.