Genel olarak tasarlanmış bir yazılımın, farklı kullanıcıların tüm
ihtiyaçlarını karşılaması çok zordur. Daha geniş bir kullanıcı grubuna hitab
eden yazılım geliştirmek için izlenebilecek ilk yol tasarım sırasında olabildiğince
farklı durumu incelemek ve tasarımı bu durumlara göre yapmak, diğer bir yol ise
yazılıma esneklik katmaktır. Esnek bir yazılım, üretim ortamına taşındıktan
sonra da kullanıcılar tarafından geliştirilmeye açık olan yazılımdır. Bir
anlamda, programcıların bıraktıkları noktadan, yazılımı geliştirme işini kullanıcılar
devralmalıdır. Kullanıcılara bu imkanı sağlamak için ise, tasarım araçları
(form, iş akışı) ve programlama araçlarından (api’ler, scripting dilleri,
script motorları) faydalanılır.
Bu yazıda C# ile programlarımıza nasıl esneklik katabileceğimizi,
programımızı nasıl bir kod derleyiciye dönüştürebileceğimizi göreceğiz.
CSharpCodeProvider Sınıfı
Programımıza bir kod derleyicinin sahip olduğu yetenekleri vermek aslında
düşünüldüğü kadar zor değil. Framework zaten bu görevi yerine getiren bir sınıf
barındırıyor. Bizim yapacağımız şey ise bu sınıfı kullanmak ve birkaç ekstra
özellik ekleyerek tek bir metod çağrısı ile istenen kodun derlenip çalıştırılmasını
sağlamak. Sözünü ettiğimiz sınıf olan CSharpCodeProvider sınıfı, C# kodlarını
derleyen ve CodeDOM kod modellerini kaynak koduna dönüştüren nesnelere ulaşmamızı
sağlar.
Yukarıda saydığımız büyük işleri küçük bir metodla gerçekleştiren sınıfımızı
yazmaya başlayalım. Sınıfımıza CodeRunner adını verdim.
Öncelikle bize gerekli olan isim uzaylarını belirtiyoruz.
using System;
using System.IO;
using System.Text;
using System.Reflection;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Collections.Specialized;
Bir sonraki adım sınıf tanımını oluşturmak;
namespace CodeRunnerLib
{
public class CodeRunner
{
public static StringCollection ReferencedAssemblies = new
StringCollection();
public static StringCollection NameSpaces = new StringCollection();
static CodeRunner()
{
ReferencedAssemblies.Add("System.dll");
NameSpaces.Add("System");
}
Yukarıdaki kod parçası içerisinde iki adet değişken tanımladık.
ReferencedAssemblies adındaki StringCollection nesnesi, derlenecek olan kod
için gerekli harici assembly’leri referanslara eklemek için kullandığımız değişken.
NameSpaces adındaki diğer StringCollection nesnesi ise derlenecek olan
kodun başına “using ...” ifadesi ile eklenecek isim uzaylarının listesini
saklayan değişken.
CodeRunner sınıfının yapılandırıcısını (constructor) statik olarak tanımladım
çünkü CodeRunner sınıfı tek bir statik metoda sahip. Böylece CodeRunner sınıfını
kullanabilmek için bir CodeRunner nesnesi oluşturmamıza da gerek kalmıyor.
Statik yapılandırıcı içerisinde “System.dll” dosyasını referanslara ekliyoruz.
Ayrıca “using” ifadesi ile derlenecek kodun başında yer alması için NameSpaces
koleksiyonuna “System” isim uzayını ekliyoruz.
public static CompilerErrorCollection RunCode(string Code) {
CodeRunner sınıfının tek metodu olan RunCode metodunun tanımını yukarıda
görüyorsunuz. Metod, parametre olarak bir string alıyor ve derleme işleminin
sonuçlarını içerisinde bulunduran bir CompilerErrorCollection nesnesi
döndürüyor.
CSharpCodeProvider cp = new CSharpCodeProvider();
Kodları derleme ve hataları döndürme işlemlerinde kullanacağımız
ICodeCompiler arayüzüne ulaşmamızı sağlayan CSharpCodeProvider nesnesini tanımladık.
Şimdi bahsettiğimiz işlemleri yapabilmek için CodeProvider nesnesinin bir kod
derleyici üretmesini ve bize döndürmesini sağlayalım;
ICodeCompiler derleyici = cp.CreateCompiler();
CSharpCodeProvider sınıfının CreateCompiler metodunu kullanarak
ICodeCompiler arayüzünü uygulayan bir derleyici sınıf elde ettik. RunCode
metoduna parametre olarak geçilen kodları derlemek için artık bu arayüzü
kullanabiliriz.
CompilerParameters derleyiciParams = new CompilerParameters();
Derleme işleminin konfigurasyonunu yapabilmek için bir
CompilerParameters nesnesi tanımladık. Bu nesnenin birkaç özelliğini değiştirip
’’derleyici nesnesine, kodları nasıl derleyeceğini bildireceğiz.
foreach (string refAs in ReferencedAssemblies)
{
derleyiciParams.ReferencedAssemblies.Add(refAs);
}
Daha önce CodeRunner sınıfına eklediğimiz ReferencedAssemblies
koleksiyonu içerisinde bulunan tüm assembly isimlerini ’’derleyiciParams adındaki
CompilerParameters nesnesi aktardık. Böylece ’derleyici arayüzü, kodları
derlerken hangi assembly’leri kullanması gerektiğini bilecek.
derleyiciParams.GenerateInMemory = true;
Yukarıdaki ifade ile derleme işlemi bittikten sonra, çıktının (assembly)
hafızada tutulacağını belirtmiş oluyoruz. Eğer derleme işlemi sonucunda elde
edilen assembly’nin hafızada değil de diskte saklanmasını istiyorsanız
CompilerParameters sınıfının ’’GenerateInMemory özelliğini ’’false yapıp,
’’OutputAssembly özelliğinde bir dosya adı belirtmelisiniz.
StringBuilder sbKod = new StringBuilder();
IndentedTextWriter kodYazici = new IndentedTextWriter(new
StringWriter(sbKod));
foreach (string nSpace in NameSpaces)
{
kodYazici.WriteLine("using {0};", nSpace);
}
kodYazici.WriteLine("public class DinamikSinif {");
kodYazici.Indent += 3;
kodYazici.WriteLine("public void DinamikMetod() {");
kodYazici.Indent += 3;
kodYazici.Write(Code);
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Close();
Yukarıdaki satırlarda tam bir kod dosyası hazırlayıp bir StringBuilder
nesnesi içerisinde saklıyoruz. Kodları yazdırmak için kullandığım nesne bir
IndentedTextWriter nesnesi. Bu nesneyi CodeDOM ile kod oluştururken ya da bizim
yaptığımız gibi dinamik kod geliştirirken kullanabilirsiniz. IndentedTextWriter
nesnesinin ilginç olan tek özelliği ’’Indent adındaki özellik. Bu özellik
’’WriteLine ya da ’’Write ile yazdığımız kod satırlarının ’’Indent kadar
girintili olarak yazılmasını sağlıyor.
Yukarıdaki kodda önemli olan tek nokta şu:
CodeRunner.RunCode metoduna parametre olarak geçilen tüm kodları tam bir
sınıf tanımı içerisinde saklıyoruz. Önce NameSpaces koleksiyonuna eklenen tüm
isim uzaylarını ’’using ifadesi ile üretilen kodun başına ekliyoruz. Daha sonra
dinamik bir sınıf tanımlıyor, sınıf içerisinde geçici bir metod oluşturuyor ve
parametreden gelen kodları bu dinamik metod içerisine yerleştiriyoruz.
Örneklemek gerekirse;
RunCode metoduna parametre olarak “’MessageBox.Show(“Test”);” geçilmiş
olsun. Bu durumda oluşturulan dinamik sınıf tanımı şu şekilde:
using System;
public class
DinamikSinif
{
public void
DinamikMetod()
{
MessageBox.Show("Test");
}
}
Tabi yukarıdaki kodun çalışabilmesi için CodeRunner.References
koleksiyonuna “System.Windows.Forms.dll” ve NameSpaces koleksiyonuna
“System.Windows.Forms” eklenmeli...
CompilerResults derlemeSonuclari =
derleyici.CompileAssemblyFromSource(derleyiciParams, sbKod.ToString());
Bir sonraki adımda ’’derleyici nesnesini kullanarak oluşturduğumuz
dinamik sınıf kodunu, belirlediğimiz ayarlarla (’’derleyiciParams) derliyoruz.
Derleme işleminin sonucu ’’derlemeSonuclari adında bir CompilerResults nesnesi
içerisinde saklanıyor. Eğer derleme sırasında bir hata varsa, hata ile ilgili
bilgiler –hata olan satırın numarası ve hata mesajı- bu nesnenin Errors
koleksiyonunda barındırılıyor. Ayrıca derleme işlemi başarılı ise, derlenen
assembly’ye CompilerResults.CompiledAssembly özelliğinden ulaşıyoruz.
if (derlemeSonuclari.Errors.HasErrors)
{
return derlemeSonuclari.Errors;
}
Eğer derleme işlemi sırasında bir hata ile karşılaşılırsa hatalar
RunCode metodunun dönüş değeri olarak belirleyip metoddan çıkıyoruz. Böylece
RunCode metodunu çağıran tüketiciler işler yolunda gitmezse hatalarla ilgili
bilgilere ulaşabiliyorlar.
Assembly derlenmis = derlemeSonuclari.CompiledAssembly;
Eğer hatasız bir derleme işlemi gerçekleşmişse derlenen assembly’yi
’’derlenmis adındaki değişkende saklıyoruz. Şimdi biraz Reflection kullanarak
dinamik olarak derlediğimiz assembly içerisindeki dinamik sınıfımızın bir örneğini
oluşturabiliriz;
object dinamikSinif =
derlenmis.CreateInstance("DinamikSinif");
’’derlenmis ile sakladığımız assembly içerisindeki DinamikSinif
nesnesinin bir örneğini assembly’nin ’’CreateInstance metodunu kullanarak oluşturduk.
Bir sonraki adım yine dinamik olarak oluşturduğumuz metodu çağırmak;
Type dinamikSinifTipi = dinamikSinif.GetType();
dinamikSinifTipi.InvokeMember("DinamikMetod",
BindingFlags.InvokeMethod, null, dinamikSinif, null);
return null;
Metodu çağırabilmek için önce Reflection kullanarak ’’dinamikSinif
nesnesinin tip tanımına ulaşıyoruz. Daha sonra tip tanımı üzerinden ’’InvokeMember
metodu ile ’’DinamikMetod adındaki metodu çağırıyoruz. Böylece RunCode metoduna
parametre olarak geçilen C# kodu sanal bir sınıf içerisindeki sanal bir metod
içine gömülerek çalıştırılmış oluyor.
Aşağıda CodeRunner sınıfının tam kodunu bulabilirsiniz. Ayrıca yazının
sonunda CodeRunner sınıfı ve test için kullanabileceğiniz bir winforms
uygulamasının kaynak kodları da mevcut...
using System;
using System.IO;
using System.Text;
using System.Reflection;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Collections.Specialized;
namespace CodeRunnerLib {
public class CodeRunner {
public static StringCollection ReferencedAssemblies = new
StringCollection();
public static StringCollection NameSpaces = new StringCollection();
static CodeRunner() {
// Statik yapılandırıcı fonksiyon içerisinde
// System isim uzayına referans gösteriyoruz
ReferencedAssemblies.Add("System.dll");
NameSpaces.Add("System");
}
public static CompilerErrorCollection RunCode(string Code) {
/* Metin kutusuna yazılan kodu derlemek için
* bir CSharpCodeProvider nesnesi oluşturuyoruz.
*/
CSharpCodeProvider cp = new CSharpCodeProvider();
ICodeCompiler derleyici = cp.CreateCompiler();
/* Kod derleyiciyinin birkaç özelliğini ayarlayabilmek
* için bir CompilerParameters nesnesi oluşturuyoruz.
*/
CompilerParameters derleyiciParams = new CompilerParameters();
// Derleyiciye gerekli olan assembly’leri referansla
gösteriyoruz
foreach (string refAs in ReferencedAssemblies) {
derleyiciParams.ReferencedAssemblies.Add(refAs);
}
// Derlenen assembly’nin hafızada tutulmasını
istiyoruz
derleyiciParams.GenerateInMemory = true;
/* Derlenecek olan kodu oluşturuyoruz.
* Metin kutusuna yazılan kodu, dinamik olarak oluşturduğumuz
* bir sınıf içerisine gömeceğiz
*/
StringBuilder sbKod = new StringBuilder();
IndentedTextWriter kodYazici = new IndentedTextWriter(new
StringWriter(sbKod));
// Derlenecek kod için gerekli olan isimuzaylarını
aktarıyoruz
foreach (string nSpace in NameSpaces) {
kodYazici.WriteLine("using {0};", nSpace);
}
// Sınıf tanımını oluşturalım
kodYazici.WriteLine("public class DinamikSinif {");
kodYazici.Indent += 3;
kodYazici.WriteLine("public void DinamikMetod() {");
kodYazici.Indent += 3;
// Parametrede gelen kodu, geçici sınıf metodu
içerisine yerleştiriyoruz
kodYazici.Write(Code);
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Indent -= 3;
kodYazici.WriteLine("}");
kodYazici.Close();
// Kodu derliyoruz, derleme işleminin sonuçlarını
saklamak için bir CompilerResult nesnesi oluşturuyoruz
CompilerResults derlemeSonuclari =
derleyici.CompileAssemblyFromSource(derleyiciParams, sbKod.ToString());
// Derleme işlemi sonucunda bir problem varsa bunları
gösteriyoruz
if (derlemeSonuclari.Errors.HasErrors) {
return derlemeSonuclari.Errors;
}
// Derlenmiş olan assembly’yi alıyoruz
Assembly derlenmis = derlemeSonuclari.CompiledAssembly;
// Assembly içerisinden kendi eklediğimiz sınıfın bir
örneğini oluşturuyoruz
object dinamikSinif =
derlenmis.CreateInstance("DinamikSinif");
// DinamikSinif’a ait DinamikMetod adındaki metodunu
çağırıyoruz
Type dinamikSinifTipi = dinamikSinif.GetType();
dinamikSinifTipi.InvokeMember("DinamikMetod",
BindingFlags.InvokeMethod, null, dinamikSinif, null);
return null;
}
}
}
Hiç yorum yok:
Yorum Gönder