BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Skrypty we własnej aplikacji – część 2

30-08-2005 22:46 | User 79341
Druga część artykułu o rozszerzaniu aplikacji o język skryptowy. Tym razem pokażę jak wykorzystać możliwości CodeDom do stworzenia systemu skryptowego opartego na kompilatorze C#, J#, VB.NET lub dowolnego innego.

Wstęp

 

Nazwę technologii CodeDOM można rozszyfrować jako Code Document Object Model. DOM kojarzy nam się od razu z XHTML, i bardzo słusznie gdyż tam po raz pierwszy spotykamy się z tym terminem. Czym jest sam DOM – wyjaśnienia możemy szukać u źródła : interfejs niezależny od platformy oraz języka, który pozwala programom i skryptom na dynamiczny dostęp i aktualizację zawartości, struktury i stylu dokumentu. Oczywiście źródło o którym wspomniałem dotyczy XHTMLa, więc proszę nie zwracać uwagi na przypisy które się znajdują na stronie, gdyż nie mają tu zastosowania. Chodzi o fakt, iż jak wskazuje pierwszy człon nazwy tej technologii - „Code” – dokument który poddajemy obróbce to kod źródłowy. CodeDOM to w głównej części generator kodu – tworzy graf – drzewo kodu (hierarchia przestrzeni nazw, klas, metod…), z którego potrafi wygenerować kod zgodny z .NET Framework, skompilować go i uruchomić, przy czym może się to odbywać tylko w pamięci operacyjnej. Generatory CodeDOM można spotkać na każdym niemalże kroku – w ASP.NET generuje kod ze stron aspx, Wsdl.exe tworzy kod dla Web Services, czy Xsd.exe generuje klasy odczytujące XML zgodnie ze schematem XSD.

 

 

(graficzna reprezentacja struktury CodeDom - od CodeCompileUnit po CodeStatement)

 

Artykuł ten niestety nie będzie się zajmował generowaniem kodu (który jest bardzo ciekawy – wystarczy chociażby pomyśleć o aplikacji która sama będzie sobie dopisywała kod w miarę potrzeb ;) ). Skupię się tylko na etapie kompilacji kodu dla potrzeb języka skryptowego.

 

Dostępne generatory kodu i kompilatory

.NET Framework udostępnia gotowe parsery m.in. C++ (managed), JScript, C#, VB.NET, oraz J#. Oczywiście można dodać inne języki – implementując generator i kompilator kodu. Nie jest to szczególnie skomplikowane – zwłaszcza, że kompilator kodu z którym mamy do czynienia uruchamia odpowiednie narzędzie z linii poleceń – mimo iż tego nie widzimy, to na tym polega cały proces kompilacji kodu. Poniższej opisany projekt powstał nie uwględniając wielu ważnych aspektów, więc proszę potraktować go jako wskazówkę jak coś takiego zrealizować, a nie przenosić w takiej postaci do własnej aplikacji.

 

Kompilacja i uruchomienie skryptu

W celu kompilacji kodu potrzebujemy odpowiedniego providera – dostawcy usług generowania kodu, kompilacji kodu oraz paru innych przydatnych rozwiązań (jak parser kodu).

Lista providerów dostępnych w .NET Framework 1.1 :

 

język

namespace

provider

C#

Microsoft.CSharp

CSharpCodeProvider

VB.NET

Microsoft.VisualBasic

VBCodeProvider

J#

Microsoft.VJSharp

VJSharpCodeProvider

JScript

Microsoft.JScript

JScriptCodeProvider

C++ (managed ext.)

Microsoft.MCpp

MCppCodeProvider

 

Możemy również skorzystać z refleksji aby załadować odpowiedniego dostawcę w czasie działania aplikacji.

 

[Kod C#]

Assembly langAssembly = Assembly.LoadFrom("vjsharpcodeprovider.dll");

CodeDomProvider codeProvider = (CodeDomProvider)langAssembly.CreateInstance("Microsoft.VJSharp.VJSharpCodeProvider");

 

Gdy już mamy dostawcę, oraz kod źródłowy (w postaci tekstowej, bądź też DOM) to możemy już kompilować. Do procesu kompilacji używamy również zestawu parametrów – ustawień kompilatora mówiacych o tym, co ma być wynikiem kompilacji oraz czego użyć podczas kompilacji.

 

[Kod C#]

CSharpCodeProvider csProvider = new CSharpCodeProvider();

CompilerParameters csParams = new CompilerParameters();

csParams.GenerateInMemory = true;

csParams.GenerateExecutable = false;

csParams.CompilerOptions = "/optimize";

csParams.IncludeDebugInformation = false;

csParams.ReferencedAssemblies.Add("system.dll");

csParams.ReferencedAssemblies.Add("ScrClassLibrary.dll");

 

Proces kompilacji jest niczym innym jak wywołaniem pliku *.exe (csc.exe/vbc.exe/vjc.exe…) kompilatora, toteż mamy możliwość ustawienia opcji kompilatora które normalnie podajemy w linii poleceń – służy do tego właściwość CompilerOptions - w powyższym przykładzie ustawiłem opcję optymalizacji kodu („/optimize”). Opcja generacji w pamięci jest trochę myląca - podczas każdego procesu kompilacji powstają pliki tymczasowe, które są usuwane w przypadku wybrania tej opcji. Ważnym działaniem jest dodanie referencji do bibliotek zewnętrznych. Działa to w pewien sposób jako ogranicznik uruchamianego kodu – bez załadowania odpowiedniej biblioteki powołanie obiektów, których nie chcemy aby użytkownik dotykał, się nie powiedzie. Jednakże bibliotekę można załadować również w runtime, więc jest to kolejny punkt na który trzeba zwrócić uwagę przy zabezpieczaniu skryptów i samej aplikacji.

 

[Kod C#]

ICodeCompiler csCompiler = csProvider.CreateCompiler();

CompilerResults csResult = csCompiler.CompileAssemblyFromSource(csParams, codeString);

 

Dostępnych mamy parę metod kompilacji – między innymi z kodu w postaci łańcucha tekstowego, jak też z kodu w postaci Dom – utworzonego przez generator kodu.

 

Obiekty aplikacji

Jednym z głównym założeń języka skryptowego w aplikacji jest współpraca z jej obiektami czy też metodami. Pamiętamy z części pierwszej iż tam było to proste – wystarczyło skorzystać z metody AddObject().

W opisywanym przypadku musimy zastosować inne podejście, nieco skompilowane. Ponieważ proces kompilacji skryptu nie różni się praktycznie od kompilacji zwykłego pliku *.cs, musimy jakoś przenieść referencje do naszych obiektów do skryptu. W przykładowej aplikacji zastosowałem rozwiązanie, które opiszę poniżej (jest to tylko jedno z wielu możliwych rozwiązań, a cechuje je stosunkowo łatwa realizacja).

 

Piszemy interfejs opisujący wszystko co udostępniamy skryptom, umiesczamy go w osobnej bibliotece, aby było możliwe jego dołączenie podczas procesu kompilacji.

Przykład użytego przez mnie interfejsu :

 

[Kod C#]

public interface IAppInterface

{

            RichTextBox TextEditor

            {

                        get;

            }

            void SetFont(string fontName, int fontSize);

void RandomFonts();

}

 

Interfejs taki należałoby bardziej rozbudować – między innymi aby nie udostępniać bezpośrednio kontrolek WinForms, a jedynie odpowiednio obudowane meotdy. Wystarczy spojrzeć jak obudowany jest interfejs rozszerzeń Visual Studio – schemat przedstawiłem w jednym z wcześniejszych artykułów.

 

Następnym krokiem jest właściwa implementacja interfejsów. Umieszczamy ją w aplikacji, powołujemy do życia przy starcie aplikacji, po czym wypełniamy dynamicznie referencjami podczas działania aplikacji, oraz przekazujemy do skryptów gdy użytkownik z nich skorzysta. Wydaje się proste – i takie jest. Przykładowa implementacja znajduje się w załączniku – warto zadbać o właściwe akcesory dla właściwości. W przedstawionym przykładzie interfejs ma tylko akcesor get, natomiast implementacja pozwala na set, dzięki czemu użytkownik nie będzie mógł sam w skrypcie ustawić tej właściwości na referencję do innego obiektu.

 

Główna metoda skryptu jest statyczną, czyli nie tworzymy obiektu skryptu, a jedynie wykonuję metodę main(). Z tego powodu musimy jej przekazać obiekt reprezentujący dostęp do aplikacji. Ponieważ jest on implementacją interfejsu IAppInterface to oczywiście nie muszę wiązać go z implementacją w postaci klasy AppInterface. Następnie przy pomocy mechanizmów refleksji wykonuję metodę przekazując parametr.

 

Uruchomienie skryptu

Pod tytułem tego akapitu należy rozumieć nic innego jak wywołanie metody main skompilowanego modułu. I tu z pomocą przychodzi, po raz kolejny, mechanizm refleksji typów.

 

[Kod C#]

Assembly compiledAssembly = csResult.CompiledAssembly;

Object[] myparam = { this.appHandlers };

Type[] mtypes = compiledAssembly.GetTypes();

foreach (Type t in mtypes)

{

MethodInfo mi = t.GetMethod("main", BindingFlags.Public | BindingFlags.Static);

            if (mi != null)

            {

                        try

                        {

                                   mi.Invoke(null, myparam);

                        }

                        catch (Exception e)

                        {

                                   ...

                        }

            }

}

 

Przeglądamy typy biblioteki – czyli klasy (ewentualnie gdy używamy przestrzeni nazw musimy przejrzeć moduły (metoda GetModules()) w poszukiwaniu klas), i próbujemy pobrać metodę main danej klasy. Jak się uda to ją wywołujemy metodą Invoke, w której parametrach zaszywamy obiekt klasy AppInterface (którym przekazujemy referencje do obiektów naszej aplikacji).

 

Przykład

Dołączony do artykułu przykład to pseudo edytor tekstu. Udostępnia on skryptom obiekt samego edytora w postaci kontrolki RichTextBox, oraz dwie metody zaszyte w klasie AppInterface – SetFont() oraz RandomFonts(). Druga z metod służy do ustawienia losowo wybranych ustawień czcionek w zaznaczonym obszarze. Dołączony jest również skrypt który korzysta z tej metody, oraz drugi skrypt który robi dokładnie to samo, ale operując bezpośrednio na obiekcie TextEditor.

 

 

 

Generowanie kodu

Mimo iż na początku artykułu wspomniałem iż nie będzie on na temat generowania kodu, to jest to temat na tyle ciekawy iż nie mogłem się powstrzymać przed pokazaniem chociaż prostego przykładu.

Pisząc aplikację w której chcemy użyć języka skryptowego możemy również rozważyć podejscie bardziej „user friendly” – sprawić aby pisanie skryptu było jak układanie klocków (w postaci graficznego interfejsu), a kod właściwy pozostawić generatorom kodu. Zobaczmy na koniec przykład takiego generatora – posłużymy się wcześniej stworzonym obiektem pośredniczącym, jednak cały kod prostego przykładowego skryptu zostanie wygenerowany przez program.

 

[Kod C#]

CSharpCodeProvider csProvider = new CSharpCodeProvider();

ICodeGenerator csGenerator = csProvider.CreateGenerator();

 

// najwyzszy poziom drzewa

CodeCompileUnit codeUnit = new CodeCompileUnit();

// dodamy przestrzen nazw

CodeNamespace codeNSpace = new CodeNamespace("SampleNamespace");

// przykladowa klasa

CodeTypeDeclaration codeClass = new CodeTypeDeclaration("SampleClass");

// metoda main, statyczna i publiczna

CodeMemberMethod codeMethod = new CodeMemberMethod();

codeMethod.Name = "main";

codeMethod.Attributes = MemberAttributes.Public | MemberAttributes.Static;

// parametr metody main (przeznaczenie – patrz treść artykułu)

CodeParameterDeclarationExpression codeParams = new CodeParameterDeclarationExpression(typeof(IAppInterface), "aplikacja");

// wywołanie metody RandomFonts() obiektu aplikacja

CodeMethodInvokeExpression codeMInvoke = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("aplikacja"), "RandomFonts");

// dodajemy wywołanie metody do wnętrzna metody main

codeMethod.Statements.Add(new CodeExpressionStatement(codeMInvoke));

 

// i na koniec składamy drzewo

codeMethod.Parameters.Add(codeParams);

codeClass.Members.Add(codeMethod);

codeNSpace.Types.Add(codeClass);

codeUnit.Namespaces.Add(codeNSpace);

 

Po czym możemy kompilować metodą CompileAssemblyFromDom() obiektu klasy CodeGenerator i wykonać w ten sam sposób co wcześniej kompilowane z kodu podanego tekstem.

 

Słowo podsumowania

Przedstawiony system języka skryptowego, mimo iż nie należy do łatwego w implementacji i zapewnieniu bezpieczeństwa, posiada praktycznie nieograniczone możliwości – wszystko zależy od tego jak zaplanujemy implementację takiego systemu we własnej aplikacji. W połączeniu z generowaniem kodu można osiągnąć bardzo ciekawe i, co najważniejsze, przyjazne dla użytkownika systemy skryptowe. Możemy nawet zastanowić się nad pomysłem udostępnienia użytkownikowi samego szkieletu aplikacji, a cała funkcjonalność byłaby zaszyta i rozwijana w postaci kodu źródłowego na serwerze bazodanowym. Warto zgłębić tajniki CodeDom, do czego zachęcam - korzystając chociażby z odnośników podanych poniżej. Cykl artykułów na temat języków skryptowych powoli zbliża się ku końcowi – jego celem jest przekazanie czytelnikowi informacji na temat „jakie mam możliwości, jakie warianty i co z tym da się zrobić”. Zapraszam do kolejnej części, której tematem będzie Visual Studio for Applications (VSA).

 

Odnośniki (aktywne na dzień publikacji tego artykułu) :

Microsoft .NET CodeDom Technology

DotNetScript

CodeDom Grammar

CodeDom w MSDN

Generowanie kodu w trakcie działania programu (codeguru.pl)

Załączniki:

Komentarze 1

integromk
integromk
1 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Chcę zbudowac podobną apl;ikację wykorzystującą język c#.
Potrzebuję parsera html, czy wiesz może gdzie można znajśc taki parser i jaką ma nazwę? Chcę zroobic wyszukiwarke do książek w sklepach internetowych i potrzebuje parsera. Wyszukiwarka będzie zrobiona w technologii asp.net w języku c#:) bo vb nie znam:))
Pozdrawiam
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.