İlkay İlknur

just a developer...

Roslyn Scripting APIs

Bu makaleye Github üzerinden katkıda bulunabilirsiniz.

Bir önceki yazımızda Visual Studio 2015 Update 1 ile gelen Interactive Window'u incelemiştik. Interactive Window ile C# kodlarını Visual Studio içerisinde hızlı bir şekilde çalıştırıp anında sonuçlarını görebiliyoruz. Nasıl browserda console içerisinde script çalıştırıp sonucunu anında görebiliyorsak aynısını artık C# için de yapabiliyoruz. Peki bu nasıl mümkün oluyor ?

Aslında tüm sihir Roslyn'in scripting API'larında. Yani Interactive Window içerisinde bir kod yazdığınızda bu kod scripting API'ları aracılığıyla Roslyn'e gönderiliyor ve arkada derlenip, çalıştırılıp sonucu bize geri veriliyor. Şimdi isterseniz gelin bu scripting API'larına biraz gözatalım.

Scripting API'larına ulaşabilmemiz için projemize ilk olarak Microsoft.CodeAnalysis.Scripting nuget paketini yüklememiz gerekiyor.

PM> Install-Package Microsoft.CodeAnalysis.Scripting 

Nuget paketini yüklediğimiz proje eğer .NET Framework 4.6'yı target etmiyorsa aşağıdaki hatayı alıyoruz. Bu hatayı almamak için projemizi .NET Framework 4.6'ya yükseltmemiz gerekiyor.

Could not install package 'System.Runtime 4.0.20'. You are trying to install this package into a project that targets '.NETFramework,Version=v4.5.2', but the package does not contain any assembly references or content files that are compatible with that framework

Nuget paketini başarılı bir şekilde yükledikten sonra artık scripting API'larını test etmeye hazırız. Genel olarak C# kodlarını çalıştırmak için kullanacağımız tipler Microsoft.CodeAnalysis.CSharp.Scripting namespace'i içerisinde bulunuyor.

İlk olarak en basit olandan başlıyoruz. CSharpScript.EvaluateScript metodu bizim basit C# ifadelerini çalıştırıp sonuçlarını almamızı sağlayan metot.

class Program
{
    static void Main(string[] args)
    {
        RunAsync().Wait();
    }

    static async Task RunAsync()
    {
        var result = await CSharpScript.EvaluateAsync("1+1");
        var result2 = await CSharpScript.EvaluateAsync<int>("1+1");
        Console.WriteLine($"result:{result}, result2:{result2}");
    }
}

Eğer EvaluateAsync'in generic olmayan metodunu kullanırsak bize sonuç object tipinden dönüyor. Ancak generic metodu kullanırsak strongly-typed olarak script'in sonucunu alabiliyoruz.

class Program
{
    static void Main(string[] args)
    {
        RunAsync().Wait();
    }

    static async Task RunAsync()
    {
        var result = await CSharpScript.EvaluateAsync<int>("int x=10;int y=12; int z=x+y; z");
        Console.WriteLine($"result:{result}");
    }
}

Yukarıda yazdığımız koda tekrar bakmamızda fayda var. "int x=10;int y=12; int z=x+y; z" aslında derlenebilir bir C# kodu değil. Ancak scripting API'larında bir değişkenin o anki değerini alabilmek için doğrudan adını yazdığımızda değerini alabiliyoruz. Aynı diğer scripting ortamlarında olduğu gibi.

EvaluateAsync metodu opsiyonel ikinci parametre olarak bizden ScriptOptions tipinde bir object bekliyor. Bu parametre ile istediğimiz namespace'i veya kütüphaneyi referans olarak ekleyebiliyoruz. Böylece EvaluateAsync içerisinde çalıştıracağımız kodlarda eklediğimiz kütüphanelerden ve namespacelerden tipleri kullanabiliyoruz. Eğer bu namespaceleri veya kütüphaneleri eklemezsek runtimeda scriptin derlenmesi esnasında compilation error alırız.

Örneğin C# 6.0 ile beraber static tiplerin isimlerini using ile eklediğimizde kod içerisinde artık doğrudan tipin ismini kullanmadan metodun adıyla çağrım yapabiliyorduk. Math sınıfı içerisindeki Tan metodunu çağırdığımız aşağıdaki kodu düşünelim.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    try
    {
        var result = await CSharpScript.EvaluateAsync<int>("Tan(20);");
        Console.WriteLine($"result:{result}");
    }
    catch (CompilationErrorException ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

Bu kodu çalıştırdığımızda compilation error alırız. Çünkü using ifadesi ile Math tipini script içerisinde referans almadık.

Eğer aşağıdaki gibi System.Math namespace'ini script içerisine referans olarak eklersek Tan(20) kodu başarılı olarak çalışacak ve sonucunu alabileceğiz.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    try
    {
        var result = await CSharpScript.EvaluateAsync<double>("Tan(90);"ScriptOptions.Default.WithImports("System.Math"));
        Console.WriteLine($"result:{result}");
    }
    catch (CompilationErrorException ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

Şimdiye kadar yaptığımız örneklerden de gördüğünüz üzere EvaluateAsync metodu context bağımsız olarak çalışıyor. Yani bir kod veriyorsunuz, derleyip, kodu çalıştırıyor ancak sonrasında içerisindeki tüm değişkenler ve değerleri kaybolup gidiyor. Peki ya aynı interactive window senaryosunda olduğu gibi çalıştıracağımız kodu dışarıdan alıyorsak ve bu kodu da çalıştırdığımız contexti korumak istiyorsak ne yapacağız ? İşte tam burada devreye EvaluateAsync metodunun biraz daha gelişmiş versiyonu olan RunAsync metodu devreye giriyor.

RunAsync metodu EvaluateAsync metodunun aksine bize ScriptState tipinde bir object döndürüyor. Bu object de kodu çalıştırdığımız contexti içerisinde barındırıyor.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    var state = await CSharpScript.RunAsync<int>("1+2");
}

ScriptState içerisindeki property ve metotlara kısaca bakarsak.

  • ContinueWithAsync metotları tahmin edeceğiniz üzere mevcut context üzerinden yeni kodlar çalıştırmamızı sağlar.
  • GetVariable metodu script içerisinde o anda tanımlı olan belirli bir değişkenle ilgili bilgileri alabilmemizi sağlar.
  • ReturnValue ise çalıştırdığımız kodun geri dönüş değerini içerir. EvaluteAsync metodundan doğrudan dönen değer burada ReturnValue propertysinde bulunuyor.
  • Script propertysi son çalıştırdığımız script ile ilgili bilgileri saklar.
  • Variables propertysi ise script içerisinde o anda tanımlı olan tüm değişkenlerler ile ilgili bilgileri içerisinde barındırır.

Şimdi ContinueWithAsync ile ufak bir örnek yapalım.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    var state = await CSharpScript.RunAsync<int>("1+2");
    Console.WriteLine(state.ReturnValue);
    var state2 = await state.ContinueWithAsync("int i=1;");
    var state3 = await state2.ContinueWithAsync<int>("i+5");
    Console.WriteLine(state3.ReturnValue);
}

Yukarıda görüldüğü gibi öncelikle basit bir toplama işlemi çalıştırdık. Sonrasında aynı context içerisinden devam edip bir değişken tanımladık. Bir sonraki adımda ise bu değişkeni kullanarak bir toplama işlemi daha yaptık. Burada en çok dikkat çekmek istediğim nokta buradaki işlemlerden dönen tiplerin immutable olması. Yani her yeni script çalıştırışımızda bize yeni bir ScriptState nesnesi geliyor. Aslında baktığımızda bu state nesnelerini uygun bir şekilde saklarsak ihtiyacımıza göre ilgili kodları hiç çalıştırmamış gibi bir önceki state üzerinden devam etme imkanına da sahip olabiliriz.

Yukarıdaki kodun çıktısını aşağıda görebilirsiniz.

Yukarıda ScriptState tipi içerisinde script içerisinde tanımladığımız değişkenlerle ilgili bilgileri içerisinde saklar demiştik. Şimdi bununla ilgili de ufak bir örnek yapalım.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    var state = await CSharpScript.RunAsync("int x=1;");
    PrintVariables(state);
    var state2 = await state.ContinueWithAsync("x=4;");
    PrintVariables(state2);
    var state3 = await state2.ContinueWithAsync("x=10;");
    PrintVariables(state3);
}

private static void PrintVariables(ScriptState state)
{
    foreach (var variable in state.Variables)
    {
        Console.WriteLine($"Variable Name: {variable.Name},Type: {variable.Type.Name}, Value: {variable.Value}");
    }
}

ScriptState nesnesi o anda script içerisinde tanımlı olan değişkenlerle ilgili isim,tip ve o anki değeri gibi bilgileri içerisinde barındırıyor. Dolayısıyla bizde bu nesne üzerinden değişkenlerle ilgili bilgilere ulaşabiliyoruz.

Yukarıdaki kodun çıktısına bakarsak.

Yukarıdaki çıktıyla beraber kodu beraber incelersek her bir state nesnesi içerisinde değişkenin değerinin değiştiğini görüyoruz.

CSharpScript tipi içerisinde son olarak Create statik metodunu kullanarak da script çalıştırabiliyoruz. Ancak bu metot diğerlerinden biraz farklı. Öncelikle bu metodu çağırdığımızda parametre olarak verdiğimiz kod anında çalıştırılmıyor. Çalıştırma operasyonunu scripti yarattıktan sonra bizim tetiklememiz gerekiyor.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    var script = CSharpScript.Create("1+2");
    var state = await script.RunAsync();
}

Ayrıca burada diğer metotlardan farklı olarak script olarak verdiğimiz kodun derlenmesi RunAsync metodunu ilk çağırışımızda yapılıyor. Eğer istersek biz de Compile metodunu kullanarak derleme işlemini önceden tetikleyebiliyoruz.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    var script = CSharpScript.Create("1+2");
    script.Compile();
    var state = await script.RunAsync();
}

Script tipi üzerinden script çalıştırdığımızda bu tip içerisinde kodu derlemek için gerekli olan tüm yapılar(syntax tree vs...) saklanıyor. Bu nedenle eğer hep aynı kodu çalıştıracaksak bu yapılar bizim için ekstra yük demek. Bunun için CreateDelegate metodunu kullanarak içerisinde bu ekstra yapıları içermeyen daha basit bir tip elde edebiliriz ve bu tip üzerinde kodu daha hızlı ve efektif şekilde çalıştırabiliriz.

static void Main(string[] args)
{
    RunAsync().Wait();
}

static async Task RunAsync()
{
    var script = CSharpScript.Create("1+2");
    var runner = script.CreateDelegate();
    var result = await runner();
    Console.WriteLine(result);
}

Yukarıdaki kodu bilgisayarınızda çalıştırırsanız delegate yaratma aşamasının biraz vakit aldığını ancak sonrasında delegate'i çalıştırma işleminin çok hızlı olduğunu göreceksiniz.

Roslyn içerisinde scripting API'larının kullanımları bu şekilde. Gördüğünüz gibi API'ların hepsi oldukça kuvvetli ve çok farklı senaryolara da cevap verebilecek şekilde tasarlanmış. Bu API'lar kullanılarak Interactive Window gibi daha pek çok farklı uygulama da yapılabilir. Örneğin web sayfası üzerinden aldığınız kodları serverda çalıştırıp sonuçlarını yine web sayfası üzerinde gösterebilirsiniz. Böylece kullanıcılar için ufak bir playground yapmış olabilirsiniz.

Roslyn'in Scripting API'ları da aynı compilerlar gibi open source. Eğer bakmak isterseniz buradan ulaşabilirsiniz. Aynı şekilde Interactive Window Scripting API'ları kullanılarak nasıl yazılmış merak ediyorsanız Github üzerinden kodlarına ulaşabilirsiniz.

Bir sonraki yazıda görüşmek üzere



Yorum Gönder