Kompilatory C# i Visual Basic były napisane w C++. Co jest logiczne. Bo C# i Visual Basic były nowymi językami. Nie możesz napisać kompilatora nowego języka w samym języku, który dopiero się tworzy.
Wybór ten jednak stworzył pewien problem. Ekipa pracująca nad językiem C# musiała też znać C++.
Aby dodać nową funkcjonalność języka C# lub Visual Basic trzeba było się mocno wysilić.
Kompilatory nie mogły używać swojego języka.
Ile mamy kompilatorów
Mam do dyspozycji polecenia, dzięki którym możemy skompilować kod bez korzystania z Visual Studio.
- Dla C# csc.exe
- Dla Visual Basic vbc.exe
W samym Visual Studio istnieje też kompilator działający w tle. Ma on za zadanie powiedzieć nam, co jest nie tak w naszym w kodzie, zanim skompilujemy kod. Kompilator ten działa z mniejszą częstotliwością i jest tolerancyjny do niekompletnego kodu, który właśnie uzupełniamy.
W Visual Studio istnieje także mały okienkowy kompilator „Immediate Window”. Pozwala on na uruchomienie kodu w trakcie debugowania kodu. Kompilator w „Immediate Window” korzysta tylko z niektórych wyrażeń języka. Przykładowo w nim nie można używać wyrażeń LINQ.
Dlaczego powstał więc kompilator Roslyn i dlaczego został wydany na świat?
Wraz z C# 6.0 można zauważyć dużo testów i prototypownia nowych funkcjonalności. W tej edycji pojawiło się dużo drobnych, miłych funkcji w samym języku. Normalnie nie było to możliwe, bo budowa kompilatora to poważna sprawa i dodanie nawet najmniejszej funkcji liczy się z wieloma problemami. Powstanie kompilatora Roslyn na pewno ułatwiło wielu pomysłodawcom języka osobom życie.
Jednym z celów kompilatora Roslyn było zbudowanie bogatego API opartego:
- Na drzewach składniowych
- Semantycznej analizie
- Modelu kompilacyjnym
Co to wszystko znaczy, Zajrzymy do projektów rozszerzających kompilator i się przekonamy.
Roslyn też rozwiązuje problem związany z posiadaniem wielu wersji kompilatorów. Mamy kompilator działający w tle Visual Studio i np. kompilator, który działa w „Immediate Window”.
Powinien być jeden kompilator.
Roslyn jest także otwarty, czyli pozwala na użycie dodatków od firm trzecich. Eliminuje to prywatność tego API jak wielu innych API w Visual Studio. Wiele innych edytorów powinno też mieć możliwość korzystania z kompilatora Roslyn w swoim własnym środowisku, niekoniecznie w systemie Windows.
Teraz, ponieważ cały kod kompilatora Roslyn jest dostępny, oznacza to, że każdy może podejrzeć kod działania kompilatora, co tworzy wiele możliwości edukacyjnych.
Roslyn jest więc oprogramowaniem Open Source.
Koniec z czarnymi skrzynkami kompilatora.
Co tak naprawdę dzieje się w środku kompilatora. Kompilator analizuje tekst w tym wypadku pliki .cs. Kompilator analizują kod, patrzy na symbole oraz na wyrażenia importujące działania z innych bibliotek.
Później kompilator robi powiązania i analizuje zmienne. Pod koniec tego procesu kompilator przerobi kod, na inny kod, który nazywa się IL, a on zapisuje się do biblioteki dll.
W wielkim skrócie tak właśnie działa kompilator.
Teraz możemy zajrzeć do skrzynek i nawet je przerobić do swoich własnych potrzeb.
Kompilator Roslyn jest napisany w C#, co oznacza, że każdy krok działania kompilatora został przetłumaczony na klasy i metody.
Co ma sens. Bo API kompilatora jak można się, domyśleć musi, być skomplikowane, w końcu nawet analiza tego prostego kodu może być łamaniem głowy.
int i = 12; Console.WriteLine(i);
Każdy element tego kodu jest pewnym wyrażeniem, które musi być przeanalizowane.
Nawet przez kompilator działający w tle.
W końcu na jakiejś podstawie Visual Studio koloruje nam składnie kodu. Na jakiejś podstawie Visual Studio formatuje kod.
Wewnątrz kompilatora Roslyn mamy więc „Syntax Tree API”, który właśnie analizuje składnię tekstową i układa to wszystko w strukturę drzewiastą.
Kompilator Roslyn ma też „Symbol API”, który nam powie, że wyrażenie „Console” pochodzi od innej biblioteki. Na bazie tego API Visual Studio ma funkcjonalność „Navigate to”.
Przycisk F12, który nawiguje nas do kodu danej klasy.
Na tym oczywiście zabawa się nie kończy. Potrzebne jest api do analizowania powiązań zmiennych i przepływu kodu. Od tego jest wiele pod API, ale można je zgrupować pod nazwą „Binding and Flow Analysis APIs”.
Bez nich nie byłoby mowy o zmianie nazwy klasy w wielu miejscach, w całym kodzie, w Visual Studio za pomocą naciśnięcia jednego guzika.
W takim wypadku API sprawdza, gdzie klasa istnieje i zmienia jej nazwę w kodzie. Kompilator jest istotnym narzędziem, bo występuje w wielu miejscach. Nie służy on tylko do wypluwania pliku dll czy exe.
Występuje on w oknie Solucji, w samym projekcie „.sln” i wielu innych funkcjach Visual Studio.
Roslyn też posiada "Emit API", którego celem jest utworzenie kodu IL. Obecnie to API daje możliwość edycji kodu w trakcie debugowania i kontynuowania debugowania po zmianie kodu.
csc.exe co nowego
Zobaczmy, co potrafi kompilator, zaczynając od okna poleceń Visual Studio. Już na tym poziomie możemy zobaczyć, co nowego jest w tym kompilatorze. Co on potrafi.
Wchodzimy do folderu, gdzie są skróty do Visual Studio 2015 Preview i w folderze Visual Studio Tools odnajdujemy „Developer Command Prompt for CS2014”.
Wpisując do konsoli „csc /?” mogę znaleźć wszystkie polecenia związane z kompilatorem Rolsyn.
Jeśli nigdy nie bawiłeś się kompilatorem na poziomie poleceń tekstowych, to jest to dobry początek. Poleceń jest dużo.
Skorzystanie z kompilatora w wierszu poleceń jest dosyć proste. Wystarczy napisać csc i podać nazwę pliku bądź jego listę.
using System;
class Hello
{
static public void Write()
{
Console.WriteLine("Hello");
}
static public void Main(string[] prams)
{
Write();
}
}
Został on domyślnie skompilowany do pliku exe.
Jeśli chciałbym skompilować plik cs do biblioteki, musiałbym określić parametr target i parametr out określający nazwę pliku dll.
Czy coś nowego jest w kompilatorze na poziomie wiersza poleceń.
Po pierwsze została dodana opcji analizy bibliotek.
Istnieje też możliwość kompilowania plików współbieżnie.
Jeśli spodobała ci się idea kompilacji kodu z poziomu konsoli. Spójrz na polecenie .MSBuild.exe, które nie współpracuje z plikami CS, ale z projektami Visual Studio. Polecenie te jest przydatne, gdy chcemy skompilować listę projektów Visual Studio bez uruchamiania samego Visual Studio.
Przejdźmy do prawdziwych projektów Roslyn.
Zaczynamy przygodę : pierwsze rozszerzanie Roslyn
Kod źródłowy kompilatora Roslyn. jest dostępne na CodePlex.
Polecam tę stronę Do przeglądania klas wewnątrz API Roslyn.
http://source.roslyn.codeplex.com/
Aby zacząć zabawę z kompilatorem obecnie trzeba zainstalować Visual Studio 2015 Preview oraz Visual Studio Preview SDK.
Do kolorowania składni trzeba zainstalować dodatek .NET Compiler Platform Syntax Visualizer.
https://visualstudiogallery.msdn.microsoft.com/70e184da-9b3a-402f-b210-d62a898e2887
Kolejna ważna rzecz to zbiór projektów, które pomogą nam zrozumieć jak rozszerzać kompilator Roslyn.
https://visualstudiogallery.msdn.microsoft.com/849f3ab1-05cf-4682-b4af-ef995e2aa1a5
Mając, to wszystko jesteś gotowy. W Visual Studio 2015 pod zakładką Extensibility powinieneś znaleźć projekty jak:
- Code Refactoring VSIX
- Diagnostics with Code Fix
- Compiler Platform Console Application
Są to właśnie projekty rozszerzające działania kompilatora Roslyn.
Wybierz projekt „Diagnostics with Code Fix”
Zanim zaczniesz działać, sprawdź, czy posiadasz dodatkowe okno „Roslyn Syntax Visualizer”.
Powinieneś je znaleźć w View –> Other Windows –> Roslyn Syntax Visualizer.
W oknie możesz zobaczyć wyrażenie drzewiaste obecnego pliku .cs.
Daje to trochę do myślenia jak kod C# jest analizowany. Przykładowo wszystko oznaczone słowem „Trail” nie ma wpływu na działanie kodu. Są to komentarze i białe znaki.
Wyrażenia drzewiaste Roslyn przydadzą się później, na razie piszemy rozszerzenie a la Hello World.
Wróćmy więc do projektu “Diagnostic with Code Fix”.
W projekcie są dwa bardzo ważne pliki. Pierwszy z nich to “DiagnosticAnalyzer”
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class Analyzer1Analyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "Analyzer1";
internal const string Title = "Type name contains lowercase letters";
internal const string MessageFormat = "Type name '{0}' contains lowercase letters";
internal const string Category = "Naming";
internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
// TODO: Consider registering other actions that act on syntax instead of or in addition to symbols
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}
private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
// TODO: Replace the following code with your own analysis, generating Diagnostic objects for any issues you find
var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
// Find just those named type symbols with names containing lowercase letters.
if (namedTypeSymbol.Name.ToCharArray().Any(char.IsLower))
{
// For all such symbols, produce a diagnostic.
var diagnostic = Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
}
}
Implementuje on klasę abstrakcyjną DiagnosticAnalyzer. Na tej podstawie wiemy, że klasa tak będzie rozszerzać zachowanie analizy symboli w kodzie. A dokładnie mówiąc metoda “AnalyzeSymbol” uruchomi się, gdy najedziemy kursorem myszki np. na nazwę zmiennej.
Klasa ta będzie sprawdzać, czy dany symbol jest napisany małą literą. Jeśli tak jest, wyświetli się nasz komunikat.
Jest to prosty przykład, ale fajnie, że już wiemy, co się dzieje w tym projekcie, mimo iż nie napisaliśmy jeszcze żadnego kodu.
Sprawa się jeszcze tutaj nie kończy, bo po naszej wymyślonej analizie kodu Visual Studio będzie oczekiwać od naszego rozszerzenia podania listy rozwiązań tego problemu.
Do akcji wkracza klasa “CodeFixProvider”.
[ExportCodeFixProvider("Analyzer1CodeFixProvider", LanguageNames.CSharp), Shared]
public class Analyzer1CodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
{
return ImmutableArray.Create(Analyzer1Analyzer.DiagnosticId);
}
public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task ComputeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
// TODO: Replace the following code with your own analysis, generating a CodeAction for each fix to suggest
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
// Find the type declaration identified by the diagnostic.
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First();
// Register a code action that will invoke the fix.
context.RegisterFix(
CodeAction.Create("Make uppercase", c => MakeUppercaseAsync(context.Document, declaration, c)),
diagnostic);
}
private async Task<Solution> MakeUppercaseAsync(Document document, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken)
{
// Compute new uppercase name.
var identifierToken = typeDecl.Identifier;
var newName = identifierToken.Text.ToUpperInvariant();
// Get the symbol representing the type to be renamed.
var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl, cancellationToken);
// Produce a new solution that has all references to that type renamed, including the declaration.
var originalSolution = document.Project.Solution;
var optionSet = originalSolution.Workspace.Options;
var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false);
// Return the new solution with the now-uppercase type name.
return newSolution;
}
}
Visual Studio wtedy w tej klasie wywoła metodę „GetFixesAsync”. Metoda ta zwróci tablice operacji. Obecnie zwraca ona jednak tylko kolekcję jednoelementową składającą się tylko z operacji „MakeUppercaseAsync”.
Operacja ta zmieni małe litery w napisie tekstowym identyfikatora na duże litery.
Co się stanie, jeśli będziesz chciał zdebugować ten projekt. No bo jak. To nie jest aplikacja przecież. Otóż po naciśnięciu Start Debbuging dzieje się prawdziwa magia.
Uruchamia mi się kolejne Visual Studio i na nim mogę zobaczyć, czy moja nowa funkcjonalność działa poprawnie. W swoim projekcie z rozszerzeniem naturalnie mogę postawić breakpoint.
To rozszerzenie będzie zaznaczać kod na zielono, gdy w tym wypadku klasa zawiera w sobie małe litery. Po najechaniu kursorem wyświetla mi się możliwość powiększenia wszystkich liter.
Po zatrzymaniu debugowania Visual Studio, które służyło mi do testowania, znika.
Właśnie napisaliśmy swoje pierwsze rozszerzenie do kompilatora Roslyn.
Rozszerzenie to można później zainstalować. W końcu ten projekt stworzył Instalkę VSIX.
W następnym wpisie przeanalizujemy wyrażenia Syntax Trees w kompilatorze Roslyn.