Narzędzie Autogen jako generator kodu dla programisty

Autogen jest systemem szablonów, który pozwala generować kod na podstawie szablonów i plików z definicjami zmiennych. Powody, dla którego wybrałem go spośród wielu istniejących silników generacji kodu to łatwa dostępność i możliwość wywołania z linii poleceń.

Pierwszy trywialny przykład.

Jak już napisałem, Autogen opiera się na dwóch rodzajach plików: plikach z szablonami i plikach definicji.

Poniżej zamieściłem bardzo prosty plik szablonowy (niech nazywa się First.tpl).

[+ AutoGen5 template out +]
This is [+name+] [+surname+]'s work.

Pierwsza linijka tego pliku definiuje format znaczników który otaczać będzie wstawki dla Autogena, oraz listę rozszerzeń (tu tylko jedno: ".out"). Natomiast druga to tekst wykorzystujący dwie zmienne: name i surname, które zdefiniujemy w pliku definicji. Plik ten będzie bardzo prosty i w zasadzie nie trzeba go tłumaczyć (niech nazywa się Definitions.def):

autogen definitions First;

name=Lolek;
surname=Lolokimono;

Pierwsza linijka to standardowy nagłówek zawierający też nazwę pliku szablonowego, którego on dotyczy, natomiast dalej mamy ciąg przypisań typu 'zmienna = wartość'. Kiedy wywołamy Autogena, ten przetworzy plik szablonowy i dokona podstawień.

Autogena można wywołać w następujący sposób:

autogen Definitions.def

Autogen wie, który plik szablonowy ma przetworzyć, poniewać powiedzieliśmy mu o tym w nagłówku pliku z definicjami.

Zgodnie z tym co napisaliśmy, powinniśmy dostać teraz plik First.out, w którym będzie następujący tekst:

This is Lolek Lolokimono's work.

Proste i przejrzyste.

Pytanie nr 1: Czy naprawdę format znaczników okalających można sobie przedefiniować?

Tak. Powyższy plik szablonu można zapisać, korzystając np. ze znaczników RHTML:

<% AutoGen5 template out %>
This is <%name%> <%surname%>'s work.

Należy jednak zauważyć, że brak spacji między znacznikami a nazwami zmiennych jest konieczny.

Pytanie nr 2: A co jeśli chcę wygenerować pliki o różnych rozszerzeniach?

Rozszerzenia można dopisywać do nagłówka po spacji, np.

[+ AutoGen5 template out1 out2 out3 out4 +]
This is [+name+] [+surname+]'s work.

Wygeneruje dla nas pliki: First.out1 First.out2 First.out3 First.out4. Potem okaże się, że Autogen pozwala na generowanie warunkowe w zależności od tego, dla którego z podanych rozszerzeń aktualnie generujemy.

Drugie podejście: wykorzystanie list.

W tym przykładzie wykorzystam Autogena do generacji struktur języka C. Na początek utwórzmy sobie pierwszą parę plików, która będzie nam generowała tylko pustą strukturę o danej nazwie (w zasadzie nie jest to żaden krok do przodu w stodunku do poprzedniego przykładu, także daruję sobie komentarze, z wyjątkiem tego jednego, że znowu zmieniłem format znaków okalających na '${' i '}' ):

Plik Struct.tpl

${ AutoGen5 template h }

struct ${structName}
{

};

Plik Definitions.def

autogen definitions Struct;

structName=MyStructure;

Po wykonaniu powinno nam to dać następujący wynik:

struct MyStructure
{

};

Teraz powinniśmy zastanowić się, jak dodać pola do naszej struktury. Każde pole składa się z typu i nazwy. Oczywistym jest, że szablon powinien być niezależny od liczby pól, które chcemy wstawić, dlatego zakładamy, że pól jest tyle, ile precyzuje plik definicji.

Ale jak plik definicji to precyzuje?

Odpowiedzią są listy. Jest to po prostu kilka razy powtórzona w pliku definicji deklaracja pojedynczego bloku. Blok taki, w naszy przypadku, może wyglądać tak:

fieds = { 
    type=int; 
    name=integerField; 
};

Powtarzając ten fragment kodu kilka razy i modyfikując nieco, otrzymujemy następujący plik definicji:

autogen definitions Struct;

structName=MyStructure;

fields = { 
    type=int; 
    name=integerField; 
};
fields = {
    type=char;
    name=charField;    
};
fields = {
    type=long;
    name=longField;    
};
fields = {
    type=double;
    name=doubleField;    
};

Mamy zatem listę o czterech rekordach. Teraz warto by z niej skorzystać w pliku szablonowym. Do tego służy konstrukcja:

${ FOR nazwa_listy "separator" }
        cośtam ${nazwa_elementu}
${ ENDFOR nazwa_listy}

Prześledźmy to na przykładzie naszej listy, która nazywa się fields i posiada pola: type oraz name:

${ AutoGen5 template h }

struct ${structName}
{
    ${ FOR fields "\n\t"} ${type} ${name}; ${ ENDFOR fields }
};

Jak widać, tutaj iterujemy po liście fields, dla której drukujemy pola type, name oraz średnik, a następnie wstawiamy znak nowej linii oraz tabulator. Dlaczego akurat te znaki? Spróbuj je usunąć i zobacz, co z tego wyjdzie.

Wykonanie dwóch posiadanych plików powinno dać nam w efekcie następujący plik Struct.h:

struct MyStructure
{
         int integerField; 
         char charField; 
         long longField; 
         double doubleField; 
};

Warto zauważyć, że separator nie jest wstawiany po ostatnim rekordzie (co jest bardzo sensowne).

Okazuje się, że to nie koniec - można zagnieżdżać w sobie listy. Powiedzmy np., że chcielibyśmy zamiast jednej struktury generować całą ich serię. W tym celu wystarczy wziąć to, co już mamy w pliku Definitions.def i opakować następną listą, np.:

autogen definitions Struct;

structs = {
    name=MyStructure;
    fields = { 
        type=int; 
        name=integerField; 
    };
    fields = {
        type=char;
        name=charField;    
    };
    fields = {
        type=long;
        name=longField;    
    };
    fields = {
        type=double;
        name=doubleField;
    };
};
structs = {
    name=MySecondStructure;
    fields = { 
        type=int*; 
        name=integerPointerField; 
    };
    fields = {
        type=char*;
        name=charPointerField;    
    };
    fields = {
        type=long;
        name=longPointerField;    
    };
    fields = {
        type=double*;
        name=doublePointerField; 
    };
};

Szablon do generacji kodu przybierze natomiast następującą formę:

${ AutoGen5 template h }

${ FOR structs } 
struct ${name}
{
    ${ FOR fields "\n\t"} ${type} ${name}; ${ ENDFOR fields }
};
${ ENDFOR structs }

Da to w wyniku następujący kod:

struct MyStructure
{
         int integerField; 
         char charField; 
         long longField; 
         double doubleField; 
};

struct MySecondStructure
{
         int* integerPointerField; 
         char* charPointerField; 
         long longPointerField; 
         double* doublePointerField; 
};

Manipulacja wartościami.

Autogen umożliwia nie tylko podstawianie przygotowanych wartości, ale też przerabianie ich w zależności od naszych potrzeb. W celu pokazania tego mechanizmu napiszmy prosty szablon do generowanie gettera i settera dla zmiennej w języku zbliżonym do C++.

To co chcemy wiedzieć o zmiennej, to jej nazwa i typ, zatem plik definicji może wyglądać tak:

autogen definitions GetSet;

type=std::string;
name=johnnysFriendName;

Napiszmy teraz plik szablonowy, jednak na razie bez manipulacji na wartościach. Dołączymy ją w następnym kroku, a na razie plik szablonowy będzie mieć następującą postać:

${ AutoGen5 template h }

${type} get${name}() const
{
    return ${name};
}

void set${name}(const ${type}& new${name})
{
    ${name} = new${name};
}

Po przetworzeniu tych plików przez Autogen na wyjściu dostaniemy:

std::string getjohhnysFriendName() const
{
        return johnnysFriendName;
}

void setjohhnysFriendName(const std::string& newjohhnysFriendName)
{
        johnnysFriendName = newjohhnysFriendName;
}

Załóżmy, że chcemy, by wszystkie nazwy przestrzegały konwencji żeKażdyWyrazZaczynaSięZWielkiejLitery. natomiast widać, że w nazwie 'getjohnnysFriendName' ta konwencja nie jest zachowana.

Żeby zabrać się za przetwarzanie napisów, będziemy musieli użyć wbudowanego interpretera języka Scheme Lisp. Niestety, wygląda na to, że Autogen implementuje tylko niektóre funkcje Scheme'a, zatem zamiast od razu użyć funkcji, która pozwoli nam skonwetrować pierwszy znak do wielkiej litery (bo niestety w Autogenie nie ma tej funkcji), musimy zrobić obejście. Wyglądać będzie ono mniej więcej tak:

${ define nameWithPrefix }${  
        (define name (get "name") )
        (string-capitalize! (substring name 0 1) )}${ 
        (substring name 1 (string-length name)) }${
enddef}

Ten kod definiuje makro użytkownika. Tego typu makra zawarte są między ${define nazwa}, a ${enddef}. Następnie w bloku między nimi wykonywane są trzy instrukcje Scheme:

(define name (get "name") )

..definiuje nam zmienną w aktualnym zakresie. w Scheme wartość zmiennej name z pliku definicji musimy pobrać za pomocą konstrukcji:
(get "nazwa")

zatem w tej linijce po prostu przypisujemy zmiennej Scheme'a o nazwie name wartość zmiennej pliku definicji o nazwie name.

następna linijka:

(string-capitalize! (substring name 0 1) )}${

zawiera wywołanie funkcji string-capitalize (która zamienia pierwszą literę każdego słowa w napisie na wielką, a resztę liter na małe) na wynikach funkcji substring 0 1, która wybiera po prostu podnapis składający się z pierwszej litery. Podsumowując - konwertujemy pierwszą literę do wielkiej litery. Dalej jest zamknięcie aktualnego bloku poleceń za pomocą '}' i otwarcie następnego. Ten następny blok otwierany jest w tej samej linijce, żeby napis, który składamy, nie rozjechał się (pamiętajmy, że autogen nie pomija znaku nowej linii).

Następna linijka:

(substring name 1 (string-length name)) }${

oznacza: "podnapis od drugiego znaku (indeks 1) do końca napisu name (string-length name)". Jak widać, składamy napis z dwóch członów - przerobionej pierwszej litery i całej reszty.

Tak zdefiniowane makro można potem wywołać, wykorzystując instrukcję INVOKE:

${ INVOKE nameWithPrefix }

Zobaczmy, jak to wygląda w właściwym kodzie:

${ AutoGen5 template h }

${ define nameWithPrefix }${  
        (define name (get "name") )
        (string-capitalize! (substring name 0 1) )}${ 
        (substring name 1 (string-length name)) }${
enddef}

${type} get${ INVOKE nameWithPrefix }() const
{
    return ${name};
}

void set${ INVOKE nameWithPrefix }(const ${type}& new${ INVOKE nameWithPrefix })
{
    ${name} = new${ INVOKE nameWithPrefix }; 
}

Wygenerujmy teraz plik GetSet.h. Jego zawartość powinna być już zadowalająca:

std::string getJohhnysFriendName() const
{
    return johhnysFriendName;
}

void setJohhnysFriendName(const std::string& newJohhnysFriendName)
{
    johhnysFriendName = newJohhnysFriendName; 
}

Generacja zależna od rozszerzenia.

Czasami chcemy, by część wygenerowanych informacji różniła się dla różnych formatów pliku. Pierwszy z brzegu przykład to kiedy mamy plik nagłówkowy oraz plik z implementacją w języku C. Autogen zawiera mechanizm odpowiadający za generowanie warunkowe, który w tym punkcie wykorzystamy.

Dla przykładu zaimplementujemy generator funkcji języka C. Będzie on generować deklaracje do pliku nagłówkowego oraz definicję do pliku implementacji.

Na początek zacznijmy od przygotowania szablonu i definicji bez użycia jakichkolwiek konstrukcji warunkowych:

Plik Function.tpl:

${ AutoGen5 template c h }

${type} ${name}(${FOR arguments ", "}${type} ${name}${ENDFOR arguments})
{

}

Plik Definitions.def:

autogen definitions Function;

type=int;
name=foo;
arguments={
    type=int;
    name=intArgument;
};
arguments={
    type=double;
    name=doubleArgument;
};
arguments={
    type=char;
    name=charArgument;
};

Jak na razie wykonanie tych plików da nam dwa bliźniacze pliki: FunctionGenerator.c i FunctionGenerator.h. Oba będą zawierały następującą treść:

int foo(int intArgument, double doubleArgument, char charArgument)
{

}

Teraz chcemy zmienić ten szablon tak, by do pliku .c trafiała definicja, a do pliku .h deklaracja.

Na początku warto wiedzieć, że w Autogenie do pobrania rozszerzenia aktualnie generowanego pliku służy funkcja:

(suffix)

Korzystając z niej i z dwóch wbudowanych makr Autogena: IF oraz CASE, wykonamy przetwarzanie warunkowe. Na początku skorzystamy z rozszerzenia, by sprawdzić, czy załączyć nagłówek (jeśli jesteśmy w pliku .c), czy też nie (jeśli jesteśmy w pliku .h):

${ IF (== (suffix) "c") }
#include "Function.h"
${ENDIF}

Jak widać, makro IF zaczyna się od słowa IF, a kończy na ENDIF. konstrukcja:

(== (suffix) "c")

to zwykłe porównanie tego, co zwraca funkcja suffix oraz napisu "c".

Instrukcja CASE przybierze formę:

CASE (suffix) }
${ == h};
${ == c}
{

}
${ ESAC }

Wynika stąd, że zaczyna się ona słówkiem CASE, a kończy słówkiem ESAC. w międzyczasie są sprawdzane za pomocą ${== h} oraz ${== c} dwie możliwości. W przypadku pliku .h generowany jest średnik, natomiast w przypadku pliku .c generowane są klamry.

Pełny plik szablonu ma postać:

${ AutoGen5 template c h }
${ IF (== (suffix) "c") }
#include "Function.h"
${ENDIF}
${type} ${name}(${FOR arguments ", "}${type} ${name}${ENDFOR arguments})${ 
CASE (suffix) }
${ == h};
${ == c}
{

}
${ ESAC }

Jego wypełnienie danymi z pliku Definitions.def da nam wynik jakiego się spodziewamy:

Plik Function.c:

#include "Function.h"

int foo(int intArgument, double doubleArgument, char charArgument)
{

}

Plik Function.h:

int foo(int intArgument, double doubleArgument, char charArgument);

Coś jeszcze? W sumie brakuje kilku rzeczy, m. in. ochrony przed powtórnym włączeniem w nagłówku, ale pamiętajmy, że to tylko przykład.

Podsumowanie

W tym artykule przedstawiłem bardzo pobieżnie funkcjonalność wszechstronnego narzędzia, jakim jest Autogen. Jeśli ktoś jest zainteresowany dalszym zgłębianiem wiedzy na ten temat, to polecam poczytać dokumentację na stronie domowej projektu.

Natomiast jeśli ktoś chce dalej kontynuować poznawanie Autogena przez wykorzystanie go w praktyce, to polecam napisać sobie prosty generator klas dla języka C++.

astralastral

O ile nie zaznaczono inaczej, treść tej strony objęta jest licencją Creative Commons Attribution-Share Alike 2.5 License.