Generovanie kódu

Generovanie zdrojového kódu

Podľa knihy Programátor pragmatik môžeme nástroje generujúce zdrojový kód rozdeliť do dvoch skupín:

  1. pasívne generátory, pri ktorých kód vygenerujeme raz a ďalej ho upravujeme ručne;
  2. aktívne generátory, ktoré používame vždy, keď je potrebné zmeniť kód.

Príkladom pasívnych generátorov sú nástroje generujúce kostru projektu. Po tom, čo je kód vygenerovaný, upravujeme ho ručne a generátor už ďalej nepoužívame. Ďalším použitím pasívnych generátorov sú nástroje na jednorazovú konverziu medzi jazykmi alebo formátmi, napríklad konvertor z Javy do Kotlinu v IntelliJ IDEA.

V minulosti sa pasívne generátory často používali na generovanie rozsiahlych častí kódu. Napríklad súčasťou Visual C++ boli sprievodcovia pre vytvorenie projektu, ktoré dokázali vygenerovať aj tisíc riadkov kódu. Výhodou bolo to, že ste tento kód nemuseli písať. Museli ste ho však rozširovať a udržiavať. Pritom išlo o kód, ktorý ste nepísali a vôbec ste mu nemuseli rozumieť. Preto sa takéto riešenie ďalej ukázalo ako nevýhodné. Dnes sa pomocou pasívnych generátorov zvykne generovať iba úplne triviálny kód, s pochopením ktorého by nemal nastať žiaden problém. Problém nutnosti veľkého množstva kódu hneď na začiatku projektu je dnes riešený skôr pomocou vyššej úrovne abstrakcie poskytovanej knižnicami a rámcami.

Príkladom aktívnych generátorov sú prekladače z jedného jazyka do iného. Napríklad prekladač zo SASS do CSS, alebo z TypeScriptu do JavaScriptu. Tieto prekladače spúšťame po každej zmene vstupného kódu.

Pri použití aktívneho generátora platí, že vygenerovaný kód nemá byť upravovaný ručne. Všetky zmeny sa robia iba v zdrojovom kóde a generátor sa spúšťa znovu. Pritom, samozrejme, všetky ručné zmeny výsledného kódu budú prepísané.

Príkladom aktívneho generátora je Google Protocol Buffers (skrátete Protobuf). Protobuf definuje jazyk pre opis údajových štruktúr, ktoré sa budú posielať v správach medzi procesmi. Na základe tohto opisu dokáže vygenerovať kód pre serializáciu a deserializáciu v niekoľkých podporovaných jazykoch.

Klasickým príkladom sú tiež generátory syntaktických analyzátorov ako YACC alebo Antlr, ktoré na základe formálne zapísanej gramatiky jazyka generujú kód v programovacom jazyku, ktorý dokáže text v tomto jazyku spracovať.

Vstupom prekladača je teda program v inom jazyku, ktorý poskytuje bohatšie vyjadrovacie schopnosti ako cieľový jazyk, alebo model opisujúci požadovaný výsledok na vyššej úrovni abstrakcie. Pričom model môže byť vyjadrený rôznymi spôsobmi. Môže to byť nejaký textový doménovo-špecifický jazyk, alebo dokonca grafický jazyk (napríklad diagramy).

Vzťah medzi modelom a generovaným textom
Obr. 1: Vzťah medzi modelom a generovaným textom

Model však môže byť zadaný pomocou používateľského rozhrania, napríklad formulára, či sprievodcu, alebo môže byť odvodený z rôznych vstupov. V každom prípade v generátore bude musieť existovať aspoň vnútorná reprezentácia modelu ako nejakej údajovej štruktúry.

Techniky generovania

Rôzne techniky generovania kódu sú veľmi detailne opísané v knihe Martina Fowlera Domain Specific Languages. Rozlišuje sa predovšetkým

  • transformačné generovanie a
  • generovanie pomocou šablón.

Transformačné generovanie predstavuje kód, ktorý prechádza vstupný model a na základe neho konštruuje výsledný text. Transformačné generovanie môže byť riadené vstupom alebo výstupom. Rozdiel spočíva v tom, či poradie generovania jednotlivých prvkov je dané ich poradím vo vstupnom modeli, alebo tým poradím, ktoré je vyžadované výstupným formátom.

Napríklad majme jazyk pre špecifikáciu konečno-stavových automatov, ktorý definuje stavy a prechody. Ak generátor prechádza elementy definované na vstupe bez ohľadu na ich typ a generuje príslušný kód v tom istom poradí, potom ide o generovanie riadené vstupom:

for (var element: stm.getElements()) {
    if (element.isState())
        printState(element);
    else if (element.isTransition())
        printTransition(element);
}

Ak však generátor najprv vyberie z modelu všetky stavy a následne vyberie všetky prechody, lebo také usporiadanie je potrebné na výstupe, potom ide o generovanie riadené výstupom:

printStatesHeader();
for (var state: stm.getStates())
    printState(state);

printTransitionsHeader();
for (var trans: stm.getTransitions())
    printTransition(trans);

Generovanie pomocou šablón využíva pripravenú statickú časť výsledného textu, v ktorej sú špeciálne označené dynamické elementy — šablónu. Pritom sa používa šablónový systém, ktorý skombinuje šablónu s premenlivými dátami — kontextom do výsledného textu.

Existuje množstvo hotových šablónových systémov, ktoré môžeme použiť. Hlavným rozdielom medzi nimi je šablónový jazyk — formát ako v texte označujeme premenlivé časti, ktoré budú vyplnené na základe kontextu. Medzi populárne šablónové systémy patria napríklad:

Väčšinou sa transformačné a šablónové generovanie používajú spolu. Napríklad kostra výstupného súboru je generovaná pomocou šablón, ale niektoré zložitejšie časti pomocou transformačného kódu. Alebo naopak — kostra výstupu je generovaná transformačne na základe vstupu, ale jednotlivé elementy sú opísané šablónami.

Na čo si dať pozor

Exploits of a Mom
Obr. 2: Exploits of a Mom

Útok typu SQL Injection je iba konkrétnym príkladom všeobecnejšieho problému, ktorý vzniká pri generovaní kódu.

Pri generovaní kombinujeme dopredu pripravené časti kódu so vstupom, ktorý prichádza z vonku. Zatiaľ čo časti kódu, ktoré sú súčasťou generátora, môžeme pripraviť tak, aby presne zodpovedali syntaxi cieľového jazyka, časti preberané zo vstupu môžu potenciálne obsahovať čokoľvek. Napríklad pri generovaní dopytu v jazyku SQL môžeme použiť jednoduchú šablónu:

String.format("INSERT INTO Students(name) VALUES ('%s');", studentName);

Pritom predpokladáme, že obsah, ktorý vložíme bude celý súčasťou reťazca. Ak však premenná studentName obsahuje reťazec „Robert'); DROP TABLE Students;--“, jej obsah naruší nami zamýšľanú štruktúru a namiesto jedného príkazu INSERT dostaneme INSERT nasledovaný príkazom DROP TABLES:

INSERT INTO Students(name) VALUES ('Robert'); DROP TABLE Students;--');

Takýto problém môže vzniknúť aj neúmyselne. Napríklad ak niekto má priezvisko „O'Reilly“, v našom prípade pridanie jeho mena zlyhá, lebo vygenerovaný kód nebude syntakticky správny. Program by však mal vedieť správne spracovať všetky mená (napríklad aj Jennifer Null, alebo Rachel True).

Dynamickými prvkami, ktoré generujeme na základe vstupu sú zvyčajne reťazce. Pre zápis reťazcov vo väčšine jazykov platí pravidlo, že musia byť obklopené úvodzovkami alebo apostrofmi a vo vnútri nesmú obsahovať samotné úvodzovky a niektoré ďalšie znaky. Takéto pravidlá sú nevyhnutné kvôli tomu, aby bolo možné neskôr daný kód jednoznačne analyzovať. Špeciálne znaky však môžeme reprezentovať pomocou zápisu zvaného escaping. Napríklad priezvisko „O'Reilly“ musíme v rôznych dialektoch SQL reprezentovať ako 'O''Reilly' alebo 'O\'Reilly'.

Podobné problémy vznikajú ak dynamicky generujeme identifikátory. Identifikátory môžu obsahovať iba obmedzenú množinu znakov a nesmú sa zhodovať s kľúčovými slovami jazyka. Napríklad môžeme vygenerovať databázovú tabuľku reprezentujúcu premenovanie, ktorá sa bude volať „RENAME“ a bude mať stĺpce „FROM“ a „TO“. Tento príkaz však zjavne nie je validným v SQL:

SELECT FROM, TO FROM RENAME;

Jedným z riešení je escaping, podobne ako pri reťazcoch. Avšak málo ktoré jazyky ho podporujú pri identifikátoroch. Ďalším riešením je quoting — obklopenie špeciálnym druhom úvodzoviek, ktoré sú určené pre identifikátory. Túto možnosť podporuje SQL, kde identifikátory môžeme uvádzať v úvodzovkách:

SELECT "FROM", "TO" FROM "RENAME";

Ďalšou možnosťou je prepis mien, pri ktorom identifikátory nespĺňajúce podmienky jazyka modifikujeme:

SELECT FROM_, TO_ FROM RENAME_;

Musíme však dávať pozor na to, aby modifikované identifikátory neboli v konflikte s inými identifikátormi.

Literatúra