Hotový projekt

Už je to tak, právě začínáte číst poslední lekci kurzu o programování skriptovacího jazyka. Kompiler už máte jistě úspěšně za sebou :) Nebylo to zase tak těžké ne? No, zřejmě několik nejasností stále máte, ale to se časem vyjasní. Dneska budeme potřebovat znalosti o virtuálním stroji, takže pokud si v tom nejste jistí, zopkujte si teorii k virtuálnímu stroji. Hlavně ty věci okolo zásobníků (zásobníku pro výpočty a zásobníku pro volání funkcí). Je důležité, abychom si uvědomili co od virtuálního stroje vyžadujeme. Budeme-li náš skriptovací jazyk využívat v nějakém našem programu, napíšeme si zřejmě skript s nějakými užitečnými funkcemi které budeme chtít používat. Jak ale implementovat virtuální stroj. Chci říct, není moc těžké napsat ho tak, aby pokaždé když budeme chtít provést nějakou funkci, virtuální stroj tuto funkci spustil a celou ji vykonal. Co když ale budeme potřebovat spustit stejnou funkci víckrát ve stejný okamžik, třeba pokud budeme programovat vícevláknovou aplikaci a každé vlákno spustí danou funkci s různými parametry. Jedno řešení je, aby každé vlákno mělo svůj virtuální stroj, ale to se moc nepoužívá. Je to také trochu plýtvání pamětí, pač každý virtuální stroj pro svůj běh potřebuje nějakou paměť. Většinou je možné spustit více funkcí najednou na stejném virtuálním stroji. Je to stejné jako když spustíte na počítači více programů, tu posloucháte muziku, tu píšete kurz o něčem, všechno ve stejný okamžik. Pokud víte jak to operační systém zhruba dělá, že umí obsloužit více aplikací najednou, už máte jistě také nápad, jak tuto vychytávku zahrnout do virtuálního stroje. Nějaký kus vykonávaného kódu je jednoznačný v tom s jakými pracuje daty, obsahem proměnných a různých programových čítačů. Vytvoříme si proto ve virtuálním stroji třídu, která bude reprezentovat běžící funkci. Co taková třída musí všechno obsahovat? Bude muset mít vlastní zásobník pro výpočty (execution stack), vlastní zásobník pro volání funkcí (call stack) a vlastní programové čítače (PC, SP, BSP). O všem jsme si pověděli v teorii o virtuálním stroji. Každá spuštěná funkce si tak bude pracovat se svými zásobníky a čítači a o tom, že běží vedle s jinými daty nebude vůbec vědět. Trik ve společném běhu dvou a více funkcí v jeden okamžik je v rychlosti počítače, což určitě také víte. Samozřejmě že nemůže doslova dělat více věcí najednou, ale můžeme to simulovat tím, že vždycky vykonáme jen daný počet instrukcí z dané funkce a pak se přepneme na jinou funkci, další která se vykonává. A to budeme dělat pořád dokola, dokud funkce neskončí. Prostě se budeme pořád přepínat mezi funkcemi a vykonávat část jejich kódu. Uf, snad je to aspoň trochu jasné.
Tím že jsme udělali jazyk "beztypový" nám zbylo trochu víc práce pro virtuální stroj. Kompiler totiž nemohl provést typovou kontrolu a tak musí virtuální stroj provádět všechny konverze sám. On musí umožňovat všechno převést na všechno ostatní, všechno mezi sebou. Náš jazyk v praxi podporuje jen 3 datové typy - integer, double a string, ale není nutné ve skriptu specifikovat která proměnná jaký typ hodnoty obsahuje. Při provádění nějakých operací s proměnnými se tedy musí vždy provést konverze, pokud je to třeba. Byl by to hřích nevyžít k tomu přetížené operátory :) Proto jsem pro položku ve výpočetním zásobníku (execution stack) nadefinoval třídu, která bude reprezentovat proměnnou jednoho ze tří jmenovaných typů. Třída CStack má přetížené všechny potřebné operátory a nachází se v souboru FunctionInstance.h

    class StackItem
    {
    public:
       int kind; //je zde ulozen integer, double nebo retezec
       union //misto pro ulozeni hodnoty
       {
          int intval;
          char* strval;
          double doubleval;
       };

    public:

       //funkce pro pretypovani hodnoty, nazvy rikaji vse
       inline void Cast_inttodouble();
       inline void Cast_inttostring();
       
       inline void Cast_doubletoint();
       inline void Cast_doubletostring();
       
       inline void Cast_stringtodouble();
       inline void Cast_stringtoint();
       
       //operatory, pro snazsi kod ve tride VirtualMachine
       bool operator == (StackItem &right);
       bool operator < (StackItem &right);
       bool operator > (StackItem &right);
       bool operator <= (StackItem &right);
       bool operator >= (StackItem &right);
       bool operator != (StackItem &right);
       
       StackItem operator + (StackItem &right);
       StackItem operator - (StackItem &right);
       StackItem operator * (StackItem &right);
       StackItem operator / (StackItem &right);
       StackItem operator % (StackItem &right);
       StackItem operator << (StackItem &right);
       StackItem operator >> (StackItem &right);
       StackItem& operator -- (); //prefix --
       StackItem operator -- (int); //postfix --
       StackItem& operator ++ (); //prefix ++
       StackItem operator ++ (int); //postfix ++
       StackItem operator & (StackItem &right);
       StackItem operator | (StackItem &right);
       void operator = (StackItem &right);
    };

Další je struktura ActivationStackItem. Je o hodně jednodušší než StackItem :) Jek možná název napodívá, reprezentuje jednu položku v zásobníku pro volání funkcí (call stack) a obsahuje členské proměnné pro uložení hodnot, které je nutno si při skoku na jinou funkci zapamatovat. Ale o tom už jsem také psal v teorii o virtuálním stroji. Nachází se ve stejném hlavičkovém souboru jako StackItem :)

    //****************************************************************************************************************
    //reprezentuje jakysi task state segment pro funkci
    //****************************************************************************************************************

    struct ActivationStackItem
    {
       unsigned int endCode;
       int PC,BSP,SP;
       unsigned int numberOfParams;
    };

Jednoduchá, ne? Třídu která představuje zásobník tady nebudu ukazovat. Její implementace až tak nesouvisí s principem práce virtuálního stroje. Nyní je na čase třída, kterou by jste čekali v souboru pojmenovaném FunctionInstance.h a totiž třída FunctionInstance. Jejím úkolem je celý první odstavec této poslední lekce :) Díky ní můžeme pustit víckrát stejnou funkci s různými daty a samozřejmě také více funkcí najednou :)

    //****************************************************************************************************************
    //trida reprezentujici instanci funkce se vsemi potrebnymi zdroji a obsluznymi funkcemi
    //****************************************************************************************************************

    class CFunctionInstance
    {
    private:
       CStack<StackItem> Stack; //zasobnik pro provadeni vypoctu
       CStack<ActivationStackItem> ActivationStack; //zasobnik pro ukladani volani fci
       int iCurrActivation; //ukazatel na vrchol zasobniku definovaneho o lajnu vejs

    public:
       FUNCTION_HEADER* function; //ukazatel na hlavicku funkce
    public:
       CFunctionInstance(void);
       ~CFunctionInstance(void);
       
       BYTE status;
       //vrati ukazatel na zasobnik pro vypocty
       CStack<StackItem>* GetStack() {return &Stack;}
       //vrati ukazatel na zasobnik pro volani funkci
       CStack<ActivationStackItem>* GetActivationStack() {return &ActivationStack;}
       //pro ulozeni nove polozky na zasobnik pro volani funkci pri volani funkce.
       HRESULT PushActivation();
       //pri navrati z funkce
       void PopActivation();
       //vrati obsah citace PC - Program Counter - citatc instrukci
       unsigned int GetPC() {return ActivationStack[iCurrActivation].PC;}
       //vrati obsah citace BSP - Base Stack Pointer - ukazatel dna zasobniku
       unsigned int GetBSP() {return ActivationStack[iCurrActivation].BSP;}
       //vrati obsah citace SP - Stack pointer - ukazatel vrcholu zasobniku
       unsigned int GetSP() {return ActivationStack[iCurrActivation].SP;}
       //vrati pocet parametru funkce
       unsigned int GetNumberOfParams() {return ActivationStack[iCurrActivation].numberOfParams;}
       //vrati adresu (offset) v kodovem segmentu kde konci telo funkce
       unsigned int GetEndCode() {return ActivationStack[iCurrActivation].endCode;}
       //Funkce pro praci s polozkami struktury ActivationStackItem, ktera predstavuje jednu polozku v zasobniku pro volani funkci
       //a udrzuje v sobe informace o stavu provadene funkce a informace nutne k provadeni funkce

       void AddPC(int offset) {ActivationStack[iCurrActivation].PC += offset;}
       void SetPC(unsigned int adress) {ActivationStack[iCurrActivation].PC = adress;}
       void AddSP(int offset) {ActivationStack[iCurrActivation].SP += offset;}
       void SetSP(unsigned int adress) {ActivationStack[iCurrActivation].SP = adress;}
       void SetEndCode(unsigned int adress) {ActivationStack[iCurrActivation].endCode = adress;}
       void SetNumberOfParams(unsigned int params) {ActivationStack[iCurrActivation].numberOfParams = params;}
       void SetBSP(unsigned int adress) {ActivationStack[iCurrActivation].BSP = adress;}
       //Funkce pro praci se zasobnikem pro volani funkci
       //zvyseni vrcholu zasobniku o 1

       void Push() {ActivationStack[iCurrActivation].SP++;}
       //snizeni vrcholu zasobniku o dany pocet
       void Pop(int number = 1);
       //Funkce pro praci se zasobnikem pro vypocty
       //Funkce pro ukladani hodnot na vrchol zasobniku

       void PushInt(int i);
       void PushString(char* str);
       void PushDouble(double i);
       void PushStackItem(StackItem s);
       //Funkce pro vyber hodnot z vrcholu zasobniku
       int PopInt();
       char* PopString();
       double PopDouble();
       StackItem PopStackItem();
       //Funkce pro vraceni typu promenne v zasobniku nebo lokalni promenne
       //Rozdil je jen v adresaci promennych v zasobniku

       int GetStackVariableKind(int offset = 0);
       int GetLocalVariableKind(int offset = 0);
       //Funkce pro nastaveni hodnoty promenne na urcite pozici v zasobniku
       void SetInt(int i, int offset = 0);
       void SetString(char* s, int offset = 0);
       void SetDouble(double i, int offset = 0);
       void SetStackItem(StackItem s,int offset = 0);
       //Funkce pro vraceni hodnoty promenne na urcite pozici v zasobniku
       int GetInt(int offset = 0);
       char* GetString(int offset = 0);
       double GetDouble(int offset = 0);
       StackItem GetStackItem(int offset = 0);
       //Funkce pro vraceni lokalni promenne
       int GetLocalInt(int offset = 0);
       char* GetLocalString(int offset = 0);
       double GetLocalDouble(int offset = 0);
       //Funkce pro nastaveni lokalni promenne
       void SetLocalInt(int i, int offset = 0);
       void SetLocalString(char* s, int offset = 0);
       void SetLocalDouble(double i,int offset = 0);
    };

Víte vysvětlit jak funguje celý projekt virtuálního stroje není také nejjednodušší a myslím, že nejlepší bude jít na to přes příklad. Aneb, vysvětlím to celé na tom jak virtální stroj vykoná skript helloworld. Z toho důvodu zde ještě nejdřív vypíši třídy, které jsou ve virtuálním stroji důležité a pak si projekt projdeme od toho co se děje po spuštění až do konce. Aspoň si budete moci trochu prohlédnout, s čím pracujeme. Nezastavujte se zatím nad tím, že nevíte co která třída dělá, zjistíte to časem :) Navíc teď už zbývají jen dvě pro nás životně důležité. Vím, v projektu je jich sice víc, ale ty nás nemusejí teď zajímat. Pokud už jste se koukali do projektu, víte o třídách CStack, což je vlastně šablona pro zásobník a pak je tam ještě asi šest tříd, které souvisejí s implementací obecné hashovací tabulky. Mohla by být specifická pro tento projekt, ale znáte to, když už něco děláte, tak aspoň tak ať to můžete později zase použít. Jde o třídy, vlastně šablony CHashTable, CHashTableItem, dvě verze CDefaultCompare a dvě verze CDefaultHashFunction. Podívejme se na třídu, která reprezentuje skript CSript. Její definice je v souboru Script.h:

    //*******************************************************************************************************************
    //Struktura reprezentujici zdroje potrebne k provedeni skriptu
    //*******************************************************************************************************************

    struct ScriptDependencies
    {
       unsigned char* m_CodeSegment; //ukazatel na kodovy segment
       unsigned int m_CodeSegmentSize; //velikost kodoveho segmentu
       unsigned int m_NumConstants; //pocet konstant v tabulce konstant
       CStack<StackItem> m_ConstantTable; //tabulka konstant
       CHashTable<char*,FUNCTION_HEADER*>* m_FunctionList; //tabulka funkci
       CHashTable<char*,ExternFunction*>* m_Imports; //tabulka importovanych funkci
       
       //konstruktor
       ScriptDependencies()
       {
       m_CodeSegment = NULL;
       m_CodeSegmentSize = 0;
       }
    };

    //*******************************************************************************************************************
    //trida reprezentujici skript
    //*******************************************************************************************************************

    class CScript : public IScript
    {
    private:
       CList<FUNCTION_HEADER*> m_FunctionTemplates; //seznam se sablonami vsech funkci ve skriptu
       CList<CFunctionInstance*> m_runningFunctions; //seznam funkci ktere se prave provadeji
       CList<ExternFunction*> m_importedFunctions;
       FILE* m_File; //soubor se skriptem
       
       IVirtualMachine* m_VM; //virtualni stroj
       
       DWORD m_dwRef;
       
       ScriptDependencies m_ScriptDependencies;
       
       CFunctionInstance* m_CurrFunction;
    private:
       //pomocne fce pro cteni dat
       char ReadChar();
       CBuffer* ReadZString();
       int ReadInt32();
       unsigned int ReadUInt32();
       short ReadInt16();
       unsigned short ReadUInt16();
       unsigned char ReadByte();
       double ReadDouble();
       
       //funkce pro vytvoreni instance funkce :)
       CFunctionInstance* MakeInstance(FUNCTION_HEADER* function);
    public:
       CScript(void);
       ~CScript(void);
       //funkce pro nacteni skriptu
       HRESULT LoadScript(const char* file);
       //funkce pro vykonani iMaxInstructions instrukci z kazde bezici funkce
       void HandleScripts(int iMaxInstructions = MAX_INSTRUCTIONS,bool* isnext = NULL);
       //funkce spusti funkci "main", pokud ve skriptu je
       HRESULT StartMain();
       //zastavi vsechny bezici funkce tim ze vyprazdni seznam bezicich funkci
       void Stop();
       //spusti funkci s urcitym jmenem
       HRESULT RunFunction(char* function);    //Funkce pro ziskani ruznych informaci o skriptu
       unsigned int GetCodeSegmentSize() {return m_ScriptDependencies.m_CodeSegmentSize;}
       unsigned char* GetCodeSegment() {return m_ScriptDependencies.m_CodeSegment;}
       unsigned int GetNumConstants() {return m_ScriptDependencies.m_NumConstants;}
       CStack<StackItem>* GetConstantTable() {return &(m_ScriptDependencies.m_ConstantTable);}
       CList<FUNCTION_HEADER*>* GetFunctionsList() {return &m_FunctionTemplates;}
       CFunctionInstance* GetCurrFunction() {return m_CurrFunction;}
       
       HRESULT AddRef();
       HRESULT Release();
    };

A jako poslední, srdce projektu - samotný virtuální stroj. No, hlavní třídou projektu je spíš vypsaná CScript. Jistě jste si všimli že dědí po rozhraní IScript, tedy se používá jako rozhraní. Pro každý načítaný skript se tak musí vytvořit nová třída, která je přístupná přes rozhraní IScript. Každá instance třídy CScript používá k vykonávání skriptu vlastní třídu CVirtualMachine, která je přístupná přes rozhraní IVirtualMachine. Vím, sice jsem před několika odstavci psal, že by nebylo dobré používat pro každou funkci vlastní virtuální stroj a může to vypadat, že není moc velký rozdíl v používání jednoho virtuálního stroje pro jeden soubor skriptu. Ale, zas tak často snad nebude spouštět více než jeden soubor skriptu najednou. A pokud ano, můžete si do jazyka, kompileru přidat něco jako příkaz #include v C nebo C++ a spojit všechny zdrojové soubory do jednoho :) CVirtualMachine je definovaná v souboru VirtualMachine.h:

    //definice ukazatele na funkci pro zpracovani instrukce
    typedef int (CALLBACK *EXEC_FUNC) (CFunctionInstance*,ScriptDependencies*);

    //*******************************************************************************************************************
    //Trida ktera reprezentuje virtualni stroj ktery je schopen provadet instrukce skriptovaciho jazyka
    //Obsahuje clenske funkce pomoci kterych jsou vykonavany jednotlive instrukce jazyka. Adresy techto funkci jsou
    //ulozeny v tabulce na pozici ktera prislusi danemu bajtovemu kodu instrukce. Ziskani ukazatele na funkci se pak
    //provadi prostym zadanim nactneho bajtoveho kodu instrukce jako indexu do teto tabulky
    //*******************************************************************************************************************

    class CVirtualMachine : public IVirtualMachine
    {
    private:
       ScriptDependencies* m_ScriptDependencies; //ukazatel na zdroje skriptu
       
       EXEC_FUNC* m_Functions; //pole ukazatelu na funkce ktere budou vykonavat kod
       DWORD m_dwRef; //pocet instanci tridy, souvisi s rozhranim IVirtualMachine
    private:
        //prototypy funkci pro zpracovani instrukci, nazvy jasne rikaji jakou instrukci dana funkce
       //vykonava, emuluje

       static int CALLBACK Exec_nop(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_mul(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_neg(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_mod(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_sub(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_div(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_add(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_goto(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_ifeq(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_ifne(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_if_cmpeq(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_if_cmpgt(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_if_cmplt(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_if_cmple(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_if_cmpge(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_if_cmpne(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_return(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_vreturn(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_load(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_store(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_ldc_int(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_ldc_string(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_ldc_double(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_dup(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_pop(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_lcall(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_ecall(CFunctionInstance* func,ScriptDependencies* sd);
       
       static int CALLBACK Exec_shl(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_shr(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_inc(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_dec(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_and(CFunctionInstance* func,ScriptDependencies* sd);
       static int CALLBACK Exec_or(CFunctionInstance* func,ScriptDependencies* sd);
    public:
       //konstruktor a destruktor
       CVirtualMachine(void);
       ~CVirtualMachine(void);
       
       //inicializace virtualniho stroje, je nutne volat prvni
       void Init(ScriptDependencies* scriptdependencies);
       //provedeni instrukci
       void Run(CFunctionInstance* function,int iMaxInstructions);
       //souvisi s rozhranim
       HRESULT AddRef();
       HRESULT Release();
    };

Začneme tedy od začátku. Vlastně, skoro od začátku, protože úplně na začátku bychom si v nějaké hlavní funkci, třeba main vytvořili nový objekt CScript a přistupovali k němu přes rozhraní IScript. Potom můžeme používat všechny funkce rozhraní IScript :) Přesně tohle je ve funkci main projektu usl_vm, pač to je program pro souštění skriptů, jak jste možná zjistili pokud jste se již koukali na projekt. Sám o sobě nic neumí, musí k němu být samozřejmě připojena knihovna VM, což je druhý projekt v celém řešení a který implementuje náš virtuální stroj a obsahuje všechno o čem tady píšu :) Spouštění skriptu začneme tím, že ho načteme pomocí funkce LoadScript třídy CScript.

    //***********************************************************************************************************************
    //fce pro nacteni skriptu do pameti
    //***********************************************************************************************************************

    HRESULT CScript::LoadScript(const char* file)
    {
       //vytvoreni a inicializace tabulky funkci a importu
       m_ScriptDependencies.m_FunctionList = new CHashTable<char*, FUNCTION_HEADER*>(); //tabulka funkci
       m_ScriptDependencies.m_Imports = new CHashTable<char*,ExternFunction*>();
       
       //docasna promenna pro nacitani znaku
       unsigned char Chr;
       bool bErr = false;
       
       //otevreni skriptu
       m_File = fopen(file,"rb");
       if (!m_File) //info o chybe
       {
          Log("Failed to open binary script file %s<br>",file);
          return ERR_BADSTREAM;
       }
       
       //****************nacteni a kontrola podisu skriptu
       //kazdy skript zkompilovany kompilerem musi mit tneto podpis
       //bezprostredne na zacatku souboru

       char signature[4];
       fread(signature,3*sizeof(char),1,m_File);
       signature[3] = 0;
       if (strcmp(signature,"USL") != 0)
       {
          //pokud podpis skriptu nesouhlasi, nepovazujeme soubor za platny soubor skriptu
          //a koncime s nacitanim. Vratime prislusnou chybu

          Log("Invalid file signature! Cannot continue loading script!<br>");
          return ERR_BADSIGNATURE;
       }
       //podpis je vporadku, vypiseme do logu ze nacitame skript
       Log("Loading script '%s'<br>",file);
       
       //nacteni skriptu
       do
       {
          Chr = ReadChar(); //precteni cisla sekce
          
          if (Chr == FUNCTION_ID) //sekce s definici funkce
          {
             //nic sloziteho, nacitani hodnot do prislusnych vlastnosti
             FUNCTION_HEADER* function = new FUNCTION_HEADER;
             CBuffer* functionName = ReadZString();
             function->name = functionName->GetBuffer();
             function->inputs = ReadUInt16();
             function->ouputs = ReadUInt16();
             function->localsLimit = ReadUInt16();
             function->stackLimit = ReadUInt16();
             function->startCode = ReadUInt32();
             function->endCode = ReadUInt32();
             
             m_ScriptDependencies.m_FunctionList->Put(functionName->GetBuffer(),function);
             m_FunctionTemplates.Add(function); //pridani sablony do seznamu
             Log("Loaded function '%s'<br>",functionName->GetBuffer());
          }
       else if (Chr == IMPORT_ID)
       {
          ExternFunction* pItem = new ExternFunction;
          
          char* func_name = ReadZString()->GetBuffer(); //precteni jmena funkce
          char* func_signature = ReadZString()->GetBuffer(); //precteni podpisu funkce
          char* lib_name = ReadZString()->GetBuffer(); //precteni jmena knihovny
          pItem->type = ReadChar(); //precteni typu
          
          //ziskani handle na knihovnu
          pItem->hDll = GetModuleHandle(lib_name);
          if (!(pItem->hDll)) //pokud se to nepovede, knihovna neni nactena v pametovem prostoru aplikace
          { //pokusime se handle ziskat tak ze knihovnu nactem
             pItem->hDll = LoadLibrary(lib_name);
             if (!(pItem->hDll)) //pokud se nam nepodari ziskat handle knihovny ani jejim nactenim
             { //tak informujeme o chybe, ale nebudeme to brat jako chybu kvuli ktere
                Log("Failed to get handle of module '%s'.<br>",lib_name); //by skript nemohl byt spusten
                bErr = true;
             }
          }
          //pokud je handle knihovny platne
          if (pItem->hDll)
          {
             //ziskani adresy funkce
             pItem->address = GetProcAddress(pItem->hDll,func_name);
             if (!(pItem->address))
                Log("Failed to retrieve adress of function '%s'<br>",func_name);
          }
          
          //pridani funkce do hashovaci tabulky
          m_ScriptDependencies.m_Imports->Put(func_signature,pItem);
          m_importedFunctions.Add(pItem);
          
          //vypsani informace do logu
          Log("Loaded external function '%s' (Address: %Xh), Library: '%s' (Handle: %Xh).<br>",func_name,pItem->address,lib_name,pItem->hDll);
       }
       else if (Chr == CONSTANTS_ID) //sekce s tabulkou konstant
       {
          m_ScriptDependencies.m_NumConstants = ReadUInt32();
          Log("Loading Constant Table. Total constants: %i<br>",m_ScriptDependencies.m_NumConstants);
          
          m_ScriptDependencies.m_ConstantTable.Init(m_ScriptDependencies.m_NumConstants); //tabulka konstant
          
          //v cyklu nacteme konstanty
          for (unsigned int i = 0; i < m_ScriptDependencies.m_NumConstants; i++)
          {
             char type = ReadChar(); //precti typ konstanty
             
             switch (type)
             {
             case INT_CONST:
                m_ScriptDependencies.m_ConstantTable[i].kind = INT_CONST;
                m_ScriptDependencies.m_ConstantTable[i].intval = ReadInt32();
                Log("Read constant integer: %i<br>",m_ScriptDependencies.m_ConstantTable[i].intval);
                break;
             case ZSTRING_CONST:
                m_ScriptDependencies.m_ConstantTable[i].kind = ZSTRING_CONST;
                m_ScriptDependencies.m_ConstantTable[i].strval = ReadZString()->GetBuffer();
                Log("Read constant string: %s<br>",m_ScriptDependencies.m_ConstantTable[i].strval);
                break;
             case DOUBLE_CONST:
                m_ScriptDependencies.m_ConstantTable[i].kind = DOUBLE_CONST;
                m_ScriptDependencies.m_ConstantTable[i].doubleval = ReadDouble();
                Log("Read constant double: %f<br>",m_ScriptDependencies.m_ConstantTable[i].doubleval);
                break;
             default:
                Log("ERROR - Unknown constant type '%i'<br>",type);
                break;
             }
          }
       }
       else if (Chr == CODE_ID) //kodovy segment
       {
          m_ScriptDependencies.m_CodeSegmentSize = ReadUInt32(); //precteni velikosti kodoveho segmentu
          
          //alokace pameti pro nacteni kodoveho segmentu
          m_ScriptDependencies.m_CodeSegment = new unsigned char[m_ScriptDependencies.m_CodeSegmentSize];
          Log("Loading code. Code size: %i bytes<br>",m_ScriptDependencies.m_CodeSegmentSize);
          
          fread(m_ScriptDependencies.m_CodeSegment,sizeof(unsigned char),m_ScriptDependencies.m_CodeSegmentSize,m_File);
          }
       }
       while (m_ScriptDependencies.m_CodeSegment == NULL); //opakuj dokud neni platny ukazatel na kodovy segment, tj dokud neni kodovy segment nacten
       
       //zavreni souboru
       fclose(m_File);
       
       //skript je nacten, ted ho musime na necem spustit, ziskej proto ukazatel na rozhrani virtualniho stroje
       //abychom mohli skript spustit

       CreateVMObject(IID_IVirtualMachine,(void**)&m_VM);
       if (!m_VM)
       {
          //pokud se to nepovede, je to blby
          Log("Failed to obtain pointer to Virtual machine interface! Null Pointer!<br>");
          return E_NOINTERFACE;
       }
       
       //inicializuje virtualni stroj
       m_VM->Init(&m_ScriptDependencies);
       
       //spust funkci main pokud ve skriptu je
       StartMain();
       
       //vrat Ok
       return S_OK;
    }

Už zase dlouhá funkce, říkáte si? :) Ale jednoduchá, říkám opět já :) No, někdo musí výplody assembly modulu v kompileru umět načíst :) Vždyť hodně kódu tvoří zase jen kontroly chyb. Můžete si všimnout, že na prvních dvou řádcích se vytvoří ve struktuře, která uchovává informace o skriptu, dvě hashovací tabulky. Jedna slouží k uložení informací o lokálních funkcích a druhá k uložení informací o importovaných funkcích. Funkce se volají podle jména, tedy podle nějakého řetězce. Jak už jsem mnohokrát psal, s tím se těžko pracuje a hlavně porovnávání trvá dlouho. Proto asi nejrychlejší způsob jak při volání funkce zjistit informace o volané funkci je použítí hash tabulky. Pořád budeme schopni volat funkci pomocí jejího názvu. Položku v hash tabulce najdeme spočtením indexu položky v tabulce pomocí hash funkce a nemusíme provádět žádná dlouhá porovnávání několika, a někdy ještě více řetězců :) Pokračujme dále v kódu. Porovnáme jestli podpis skriptu souhlasí. To je jen taková první rychlá kontrola :) Potom už začánáme s čtením jednotlivých sekcí. To je ten největší cyklus. Myslím že není třeba ho dopodrobna vysvětlovat. Jde přece jen o čtení dat ze souboru. Pozor na to aby jste četli data z jednotlivých sekcí v tom pořadí v jakém jste je zapsali, jinak načtete nějaké nesmysly a hned tak se to nepozná. Jediné co možná neznáte, je načtení nějaké knihovny a funkce za běhu programu. To potřebujeme kvůli externím funkcím. Když chceme zavolat nějakou externí funkci jako například MessageBoxA z ukázkového skriptu helloworld, musíme mít knihovnu, ve které se funkce nachází, načtenou v paměťovém prostoru naší aplikace, tedy v paměťovém prostoru běžícího virtuálního stroje. Funkce MessageBoxA se nachází v knihovně user32.dll. V sekci kde načítáme externí funkce nejdřív pomocí funkce GetModuleHandle zjistíme handle knihovny user32.dll. Handle se špatně překládá, ale je to jakési držadlo, číslo které jednoznačně identifikuje instanci běžícího procesu (to může být aplikace, knihovna). Handle nesmí být nulové. Funkce GetModuleHandle zjistí handle knihovny jen pokud je knihovna už načtena v pamětovém prostoru aplikace. Pokud tedy vrátí nulové handle, víme, že knihovna ještě není načtena v paměti a načteme ji pomocí funkce LoadLibrary. Ta načte knihovnu do paměťového prostoru naší aplikace a vrátí nám její handle, které potřebujeme ke zjištění adresy funkce. Načtení knihovny do paměťového prostoru naší aplikace vlastně znamená že bude rozšířen kódový segment programu o kód načtené knihovny a my potřebujeme zjistit adresu funkce v rámci adresového prostoru naší aplikace, abychom ji mohli zavolat. Pokud se nám podaří zjistit handle, můžeme zjistit adresu funkce pomocí funkce GetProcAdress, která potřebuje znát jen handle knihovny a název funkce. Pokud víte něco o fungování procesoru, operačního systému a spouštění programů, to co jsem se teď pokoušel vysvětlit by vám mělo být taky jasné. Pokud o tom nic nevíte, není v mých možnostech to tady vysvětlit a berte to tak, že tento úsek o kterém jsem tu teď mluvil prostě zjišťuje adresu funkce abychom ji mohli zavolat a můžete to brát jako normální poštovní adresu. Taky ji musíte znát, aby jste mohli úspěšně poslat dopis. Na načítání lokálních funkcí nic složitého není, jenom se načtou dané položky a celé se to uloží v jedné struktuře do hashovací tabulky. Stejně tak konstanty se ukládají do tabulky, ale do obyčejné. Konstanty indexujeme pomocí čísel, indexů a nemusíme pro ně mít hashovací tabulku, která se indexuje pomocí klíče. Pokud se skript načte, tedy v něm nebyly neplatné sekce nebo porušené sekce a hlavně tam byl kódový segment, vytvoříme objekt CVirtualMachine, pomocí funkce CreateVMObject (definice v Manager.h, implementace v Manager.cpp, tak se na to podívejte), která vrátí jen rozhraní IVirtualMachine, což nám stačí. Vytvoření virtuální stroj inicializujeme zavoláním jeho funkce Init a předáme mu strukturu s informacemi, se zdroji načteného skriptu jako parametr. Funkce Init má jen jeden řádek a vypadá následovně:

    //*******************************************************************************************************************
    //inicializace
    //*******************************************************************************************************************

    void CVirtualMachine::Init(ScriptDependencies* scriptdependencies)
    {
       //jen ulozeni ukazatele na zdroje, ktery je nezbytny pro funkci virtualniho stroje
       m_ScriptDependencies = scriptdependencies;
    }

Jen se uloží ukazatel na strukturu informací o skriptu, zdrojů. Virtální stroj ho potřebuje znát, aby mohl přistupovat k tabulce lokálních funkcí, tabulce externích funkcí, tabulce konstant a kódovému segmentu. Když máme skript načtení a virtuální stroj vytvořený a inicializovaný, můžeme spustit funkci main, tedy pokud ve skriptu je. Pokud tam není, nic se nestane, pokud nějakým jiným způsobem nespustíme nějakou jinou funkci. Třída CSript sice obsahuje funkci RunFunction pro spuštění určité funkce ve skriptu, ale neumožňuje zadat parametry funkce, čili se hodí jen pro spouštění funkcí bez parametrů. Funkci, která by toto umožňovala třída CScript zatím neobsahuje a to je myslím dobrý úkol pro vás v rámci vylepšování :) Funkce StartMain, která spouští funkci main je takováto:

    //***********************************************************************************************************************
    //Fce pro spusteni funkce main
    //***********************************************************************************************************************

    HRESULT CScript::StartMain()
    {
       for (unsigned int i = 0; i < m_FunctionTemplates.Size(); i++)
       {
          FUNCTION_HEADER* tmp = m_FunctionTemplates[i];
          if (strncmp(tmp->name,"main",4) == 0) //projdi v cyklu vsechny nactene funkce a pokud
          { //prvni ctyri pismena nayvu funkce souhlasi
             CFunctionInstance* instance = MakeInstance(tmp); //s main, tak ji spust a skonci
             m_runningFunctions.Add(instance);
             m_CurrFunction = instance;
             return S_OK;
          }
       }
       
       return S_FALSE;
    }

Funkce prochází tabulku lokálních funkcí a porovnává první čtyři znaky jména každé funkce s řetězcem main. Porovnáváme jen čtyři znaky, protože kvůli možnosti přetěžování funkcí, je jméno každé funkce sestaveno ještě ze závorek a čísla udávajícího počet argumentů funkce (funkce main celá měla název main(0)). Zřejmě by bylo dobré hlídat zde i nulový počet argumentů funkce, ale tato verze nám pro začátek stačí. Můžete tak mít hlavní funkci pojmenovanou i jinak, hlavně pokud bude začínat main :) Taky radši ohlídejte ten nulový počet argumentů. Já jsem to nezkoušel, ale pokud spustíte funkci která má nějaké parametry jako první, nebude mít na zásobníku tyto parametry uložené, může v lepším případě dojít jen k nesprávnému adresování zásobníku a vracení špatných výsledků funkcí, v horším případě pak virtuální stroj může spadnout s chybou. Proto třída CSript zatím neobsahuje funkci pro spuštění funkce s parametry. Jde přeci jen o vyučovací projekt a každý by si ho měl před nějakým pořádným použitím pořádně zabezpečit. V takhle velkých věcech se navíc chyby špatně hledají a mzslím, že už někde v úvodu jsem psal, že projekt není bez chyb. Pokud ne, tak to píšu aspoň teď :) Pokud je funkce main nalezena, vytvoříme její novu instanci (bude mít vlastní zásobníky a čítače) a přidáme ji do seznamu běžícících funkcí. Až bude zavolána metoda HandleScripts, bude funkce automaticky vykonána. Mohla by vás ještě zajímat funkce MakeInstance, která vytváří instanci funkce:

    //***********************************************************************************************************************
    //vytvori instanci funkce
    //***********************************************************************************************************************

    CFunctionInstance* CScript::MakeInstance(FUNCTION_HEADER* function)
    {
       CFunctionInstance* newInstance = new CFunctionInstance;
       
       newInstance->function = function;
       newInstance->SetEndCode(function->endCode);
       newInstance->SetPC(function->startCode);
       newInstance->SetSP(function->localsLimit - 1);
       newInstance->SetBSP(0);
       newInstance->SetNumberOfParams(function->inputs);
       //nastav fci ktera se provede pri zmene zasobniku
       
       return newInstance;
    }

Jde zase o přiřazování proměnných z jedné struktury do druhé. Důležité je správně nastavit konec kódu funkce, začátek funkce (čítač PC) a limit zásobníku (čítač SP). Od localsLimit odečítáme -1 aby ukazatel vrcholu zásobníku ukazoval na první proměnnou. BSP nastavujeme na 0, protože to je ukazatel dna zásobníku a čekáme, že funkce je spouštěna jako první (v zásobníku není nic uloženo), prostě, že se začíná od začátku, s čistým trikem :) Samotné vykonávání běžících funkcí se ale spouští až funkcí HandleScripts:

    //***********************************************************************************************************************
    //Fce pro obslouzeni skriptu
    //provede se maximalne iMaxInstructions instrukci.
    //Bude obslouzena vzdy jedna funkce ze seznamu spustenych funkci
    //do promenne na kterou ukazuje isnext se ulozi true pokud je v seznamu dalsi funkce k obslouzeni
    //***********************************************************************************************************************

    void CScript::HandleScripts(int iMaxInstructions,bool* isnext)
    {
       static unsigned int i = 0;
       
       //pokud je v seznamu bezicich funkci nejaka funkce k obslouzeni
       if (i < m_runningFunctions.Size())
       {
          m_CurrFunction = m_runningFunctions[i];
          
          if (m_CurrFunction->status == STATUS_RUNNING) //proved zadany pocet instrukci na funkci
             m_VM->Run(m_CurrFunction,iMaxInstructions);
          
          if (m_CurrFunction->status == STATUS_DEAD) //pokud byla funkce ukoncena, tj ma status dead
          { //tak ji odstran ze seznamu bezicich funkci.
             m_runningFunctions.Remove(m_CurrFunction);
             m_CurrFunction = NULL;
          }
          
          if (isnext) //pokud je ukzatel platny
             *isnext = true; //uloz true, v seznamu je dalsi funkce ke zpracovani
          i++; //inkrementuj i, tj. posun se na dalsi funkci
       }
       else
       {
          if (isnext)
             *isnext = false; //uloz false, v senamu uz neni dalsi funkce k provedeni
          i = 0; //i bude ukazovat opet na prvni funkci
       }
    }

I když název této funkce je v množném čísle, je na ní zvláštní to, že při jednom volání obslouží právě jednu funkci ze seznamu běžících funkcí. Vždycky vezme jednu položku z tabulky běžících funkcí a pokud funkce běží, přesnějí řečeno její stav říká že by měla běžet, je spuštěna na virtuálním stroji. Vždy je ale provedeno nejvíce iMaxInstructions instrukcí, aby virtuální stroj nezatěžoval systém na moc dlouho. Do proměnné isnext se potom uloží, zda je v seznamu běžících funkcí ještě nějaká další funkce k vykonání. Program který funci volá, pozná, zda má pokračovat :) Pro použití v nějakém projektu by bylo ale lepší obsluhovat provádění skriptů v samostaném vlákně. Toto je takové jednoduché řešení jak nestrávit příliš mnoho času při obsluhování mnoha běžících funkcí v jednom programovém cyklu a mít možnost obsloužit jednu funkci v jednom programovém cyklu. Pokud zadáte iMaxInstructions = 0, funkce se vykoná celá dokud neskončí. Pokud funkce nabývá stavu STATUS_DEAD, znamená to že už byla vykonána a můžeme ji odstranit ze seznamu běžících funkcí, kde už nemá co dělat.

Myslím, že už se můžeme podívat na třídu CVirtualMachine, tedy na virtuální stroj a jeho metodu Run, která vykonává všechny příkazy Universal Scriptu:

    //*******************************************************************************************************************
    //funkce pro provedeni instrukce
    //*******************************************************************************************************************

    void CVirtualMachine::Run(CFunctionInstance* function,int iMaxInstructions)
    {
       int iExecutedInstructions = 0;
       
       //kontrola uakazatele na zdroje
       if (!m_ScriptDependencies)
          return;
       
       //ukazatel na zdroje muze byt platny, ale co dilci ukazatele na jednotlive dalsi zdroje
       if (!m_ScriptDependencies->m_CodeSegment || !m_ScriptDependencies->m_FunctionList)
          return;
       
       //pokud maximalni pocet instrukci byl zadan 0, coz znaci konej dokud neskonci funkce, nastav mnoztvi provedenych
       //instrukci na -1, cimz vlastne zajisti ze nikdy nedojde k nesplneni podminky cyklu ktera hlida pocet instrukci
       if (iMaxInstructions == 0)
          iExecutedInstructions = -1;
       
       //dekodovaci smycka
       while ((function->GetPC() < function->GetEndCode()) && (function->status != STATUS_DEAD) && (iExecutedInstructions < iMaxInstructions))
       {
          //precti aktualni bytovy kod instrukce
          unsigned char opCode = m_ScriptDependencies->m_CodeSegment[function->GetPC()];
          //zvys citac instrukci o bajt cimz se posuneme na dalsi bytovy kod
          function->AddPC(sizeof(char));
          
          //koukni se jestli se nejedna o neznamy kod instrukce a pokud jo, nahlas to a kod jednoduse preskoc
          //tim ze budes pokracovat dal

          if (opCode < 0 || opCode > (NUM_OPCODES - 1))
          {
             Log("Error. Invalid opcode '%i' found in code segment
    ",opCode);
          }
          else
          {
             //kod instrukce je platny, ziskej ukazatel na funkci ktera instrukci emuluje
             //bytovy kod je indexem do pole ukazatelu na obsluzne funkce. Tento zpusob je
             //zrejme nejrychlejsi zpusob dekodovani instrukci, zadne rozhodovaci bloky ktere
             //hrozne zdrzuji. Pracuje to jako preruseni

             EXEC_FUNC funcPointer = (EXEC_FUNC)m_Functions[opCode];
             iExecutedInstructions += funcPointer(function,m_ScriptDependencies);
          }
          
          //opet hlidej pripad ze se ma pracovat dokud funkce neskonci
          if (iMaxInstructions == 0)
             iExecutedInstructions = -1;
       }
       
       //pokud funkce skoncila, vypis o tom ypravu do logu
       if (function->GetPC() >= function->GetEndCode())
       {
          Log("Killing function '%s'. PC is: '%i'. End of code is: '%i'
    ",function->function->name,(unsigned int)function->GetPC(),(unsigned int)function->GetEndCode());
          //nastav status funkce na dead, tj. uz byla provedena a bude diky tomu odstranena ze seznamu funkci
          //ktere cekaji na zpracovani

          function->status = STATUS_DEAD;
       }
    }

Co že to dělá? Na začátku ověčujeme platnost zdrojů, tj. jestli jsou platné ukazatele na tabulku lokálních a globálních funkcí a hlavně ukazatel na kódový segement. Kdyby nebyl kódový segment, nebylo by co dělat :) Potom se podíváme jestli se má funkce provádět dokud neskončí a pokud ano, nastavíme proměnnou iExecutedInstructions na -1, podle čehož potom poznáme že máme pořád pokračovat. Následuje hlavní cyklus, který se opakuje tak dlouho, dokud není dosaženo konce kódu funkce nebo maximálního počtu instrukcí co se má provést (pokud iMaxInstructions = 0, nastaví se iExecutedInstructions na -1 aby tato podmínka nikdy neplatila před vkročením do cyklu a potom pokaždé na konci cyklu). Také se kontroluje jestli funkce nenabývá stavu STATUS_DEAD, čili jestli nebyla náhodou zavolána k provedení ještě před automatickým odstraněním ze seznamu běžících funkcí třídou CScript. Stavu STATUS_DEAD funkce nabývá pokud je vykonána do konce. V těle cyklu se pak čte kód instrukce a kontroluje se, jestli jde o platnou instrukci Universal Scriptu. Pokud ano, zjistí se ukazatel na funkci která instrukci emuluje. K emulaci každé instrukce slouží jedna funkce, která obsahuje více či méně kódu v C++ :) Aby bylo volání funkcí co nejrychlejší, použil jsem nejrychlejší možný způsob a to je právě volání funkcí přes ukazatele. Ukazatele na všechny funkce jsou uloženy v tabulce ukazatelů. Ukazatel na funkci je uložen vždy na pozici, která odpovídá číslu, kódu instrukce, kterou funkce emuluje. Toto je daleko rychlejší a efektivnější způsob než použití rozhodovací konstrukce if nebo konstrukce case, které jsou náročnější na zpracování než prosté zjištění ukazatele na funkci pomocí přečteného kódu instrukce a jejího zavolání :) Potom už cyklus končí. Na konci se jen kontroluje jestli funkce skončila a pokud ano, je jí nastaven STATUS_DEAD, aby třída CSript poznala, že ji může odstranit ze seznamu běžících funkcí.

Nyní se podíváme na některé emulační funkce, ne na všechny, jen na některé. Nejdřív na ty jednodušší a pak na ty složitější :) Všechny funkce pro emulaci instrukcí začínají na Exec_ a pak následuje jméno instrukce, takže snadno poznáte co která funkce emuluje. Každá emulační funkce také vrací počet instrukcí a myslím že to je pokaždé 1 :) Jako první se podíváme na funkci Exec_add, která emuluje sčítání dvou operandů. Funkce pro další operace jako odčítání, násobení, dělení a další, vypadají stejně.

    //*******************************************************************************************************************
    //instrukce add
    //*******************************************************************************************************************

    int CALLBACK CVirtualMachine::Exec_add(CFunctionInstance* func,ScriptDependencies* sd)
    {
       //vezme ze zasobniku dva operandy a na vrchol ulozi jejich soucet
       StackItem var_left,var_right;
       
       var_right = func->PopStackItem();
       var_left = func->PopStackItem();
       
       func->PushStackItem(var_left + var_right);
       
       return 1;
    }

Vidíte, že funkce vybere ze zásobníku dvě proměnné (jako třídy StackItem) a využije jejich přetíženého operátoru pro sčítání pro uložení součtu proměnných na vrchol zásobníku. Podívejte se do třídy StackItem na operátor pro sčítání a uvidíte, že je tam trochu víc kódu :), který ale jen zjišťuje typ obou proměnných a podle toho provádí nutné datové konverze a následně proměnné sečte a vrátí výsledek. Máme tři typy proměnných (float, int a string) a přetížený operátor musí provádět kontrolu pro každý typ s každým, tedy celkem 9, což se potom rozleze na víc řádků, ale nebojte se toho :) Každá instrukce která provádí nějakou matematickou operaci je úplně stejná jako tato, jen se liší v operátoru, samozřejmě :) Další funkce na kterou se podíváme bude Exec_ldc_int.

    //*******************************************************************************************************************
    //instrukce ldc_int
    //*******************************************************************************************************************

    int CALLBACK CVirtualMachine::Exec_ldc_int(CFunctionInstance* func,ScriptDependencies* sd)
    {
       //nacte integer z tabulky konstant na vrchol zasobniku
       unsigned int index = *((unsigned int*)&(sd->m_CodeSegment[func->GetPC()]));
       func->AddPC(sizeof(unsigned int));
       
       func->PushInt(sd->m_ConstantTable[index].intval);
       return 1;
    }

Funkce jen načtš celočíselnou (integer) konstantu na vrchol zásobníku. Je to také jedna z funkcí, která emuluje instrukce, která má i parametr. Nesmíme proto zapomenout připočítat do čítače PC počet bajtů, které zabíral parametr instrukce, který bylo třeba přečíst. U instrukce ldc_int je to 16 bitový index do tabulky konstant. V posledním řádku funkce je vidět uložení konstanty na zásobník i její čtení z tabulky konstant. Další funkcí je Exec_load:

    //*******************************************************************************************************************
    //instrukce load
    //*******************************************************************************************************************

    int CALLBACK CVirtualMachine::Exec_load(CFunctionInstance* func,ScriptDependencies* sd)
    {
       //nacte promennou na vrchol zasobniku
       unsigned short adress = *((unsigned short*)&(sd->m_CodeSegment[func->GetPC()]));
       func->AddPC(sizeof(unsigned short));
       
       //zeptame se na typ promenne co je v zasobniku na adrese adress
       int var_type = func->GetLocalVariableKind(adress);
       
       switch (var_type)
       {
       case INT_CONST:
          func->PushInt(func->GetLocalInt(adress));
          break;
       case DOUBLE_CONST:
          func->PushDouble(func->GetLocalDouble(adress));
          break;
       case ZSTRING_CONST:
          func->PushString(func->GetLocalString(adress));
          break;
       }
       
       return 1;
    }

Instrukce load načítá na vrchol zásobníku nějakou proměnnou, lépe řečeno její obsah. Opět přičítáme k PC 2 bajtíky, velikost parametru instrukce load a totiž index proměnné. Nejdřív zjistíme typ proměnné a podle toho uložíme na vrchol zásobníku odpovídající hodnotu. Přejdeme na složitější funkce. Tedy pokud složitější znamená delší :) Jednou takovou je funkce Exec_lcall, která emuluje instrukci lcall pro volání lokálních funkcí:

    //*******************************************************************************************************************
    //instrukce lcall
    //*******************************************************************************************************************

    int CALLBACK CVirtualMachine::Exec_lcall(CFunctionInstance* func,ScriptDependencies* sd)
    {
       //provede skok na lokalni funkci, funkci ktera je napsana ve skriptu
       unsigned int index = *((unsigned int*)&(sd->m_CodeSegment[func->GetPC()]));
       func->AddPC(sizeof(unsigned int));
       
       FUNCTION_HEADER* function = NULL; //ukazatel na strukturu s informacemi o funkci
       //precteni jmena funkce z tabulky konstant
       bool found = sd->m_FunctionList->Get(sd->m_ConstantTable[index].strval, &function);
       
       //pokud bylo jmeno funkce nalezeno v tabulce konstant, musi platit promenna found a
       //ukazatel na funkci musi byt tez platny

       if (found && function != NULL)
       {
          //musime ulozit aktualni stack-pointer, ukazatel vrcholu zasobniku
          int iCurSP = func->GetSP();
          
          //zkontrolujeme jestli vrchol zasobniku + limit volane funkce neprekroci velikost zasobniku, pokud ano,
          //nahlasime do logu chybu a funkci volat nebudeme. Navic nastavime ukazatel kodu na konec programu, protoze
          //by nasledujici operace bez provedeni volane funkce nejspis nebyly spravne

          int stackHeight = (iCurSP - func->function->inputs) + func->function->localsLimit + func->function->stackLimit;
          if (stackHeight > MAX_STACK_SIZE)
          {
             Log("Error: Cannot call function '%s'. Not enought stack space to execute it! Terminating
    ",func->function->name);
             //ted opatreni aby fce skoncila
             func->SetPC(func->GetEndCode());
             return 0;
          }
          
          //ulozime na zasobnik volani funcki hodnoty nove funkce, tim vznikne novy vrchol zasobniku a stary sav zustane
          //ulozen. Budeme ho moci obnovit a pokracovat v provadeni kodu tam kde jsme skoncili. Jen nejdriv zkontrolujeme
          //jestli zasobnik volani funkci taky neni preplnen

          if (func->PushActivation() == ERR_STACKOVERFLOW)
          {
             Log("Error - Stack Overflow. Terminating execution...
    ");
             func->SetPC(func->GetEndCode());
             return 0;
          }
          
          //nastavim program counter-ukazatel kodu na novou adresu
          func->SetPC(function->startCode);
          //stejne tak nastavime konec kodu
          func->SetEndCode(function->endCode);
          
          //Tyto dva rdaky souvisi s nastavenim ukazatelu do zasobniku pro novou funkci. Pokud je vam jasne jak
          //pracuje zasobnik u naseho virtualniho stroje, rozlustite i tyto vypocty. Promenne funkci jsou pocitany
          //od argumentu funkce. Promenne jsou ulozeny v zasobniku od adresy kam ukazuje BSP. Hned po posledni promenne
          //je v zasobniku misto pro provadeni vypoctu a ukladani vysledku. Na vrchol tohoto zasobniku ukazuje SP.
          //Pri volani nove funkce musi tedy BSP ukazovat na misto kde je ulozena prvni promenna teto funkce a SP bude
          //ukazovat na zacatek zasobniku pro provadeni vypoctu. Bude tedy ukazovat na posledni promennou funkce. Protoze
          //pri prvnim pouziti nejake instrukce pro nacteni operandu do zasobniku bude nejprve SP inkrementovano, cimz se ukazatel
          //dostane na prvni misto za posledni promennou funkce

          func->SetBSP(iCurSP - (function->inputs - 1));
          func->SetSP(iCurSP - function->inputs + function->localsLimit);
          
          func->SetNumberOfParams(function->inputs);
       }
       else
          //tohle by se prakticky nemelo nikdy stat
          Log("ERROR - Function '%s' was not found in VM
    ",sd->m_ConstantTable[index].strval);
       
       return 1;
    }

Skládá se to opět převážně z kontrol chyb. Z všeho nejdřív je nutné zjistit jméno funkce kterou máme volat. Jména funkcí jsou uložena jako konstanty v tabulce konstant. Přečteme tedy z kódového segmentu index do tabulky konstant a podle něj získáme jméno funkce. Potom šáhneme do hash tabulky lokálních funkcí pro informace o funkci (uloží se do struktury function). Pokud informace o funkci najdeme, můžeme začít s pokusem funkci zavolat. Než funkci zavoláme, je nutné provést několik kroků. Nejdřív zkontrolujeme stav zásobníku a zda je v něm dost místa na vykonání volané funkce. Pokud dost místa není, je to dost špatné a funkci nezavoláme. Navíc ještě nastavíme ukazatel PC na konec aktuálně prováděné funkce, protože vykonávat ji dále když volání jiné funkce selhalo by asi nemělo moc velký smysl. Pokud v této části problém mít nebudeme, můžeme se pokusit uložit na zásobník volání funkcí novou položku. Zásobník pro volání funkcí je vlastně pole, které na každém místě obsahuje položku ActivationStackItem, která obsahuje všechny čítače. Voláním funkce PushActivation se vlastně jen přesuneme na novou položku a přes ukazatel func už budeme pracovat s novými čítači PC, SP i BSP. Ty staré zůstanou v zásobníku uloženy, aby se mohlo pokračovat ve vykonávání funkce až nově volaná funkce skončí. Je nutné nastavit PC, konec kódu nové funcke, ukaztele zásobníku PC a BSP a počet parametrů funkce. Tyto příkazy by vám měly být jasné z teorie k virtuálnímu stroji. Tím máme všechno přenastavené a hotové. Funkce skončí a virtuální stroj bude dále číst a vykonávat instrukce nové funkce dokud nenarazí na instrukci návratu a nevrátí se ke staré funkci. Zásobník pro výpočty ani zásobník pro volání funkcí se nemění, ten zůstává stejný. Zásobníky jsou uloženy ve tříde CFunctionInstance, která reprezentuje instanci funkce. Instance funkce se ale vytvořila pro funkci main, kterou jsme spustili jako první a nyní se nové instance nevytvářejí. Všechny funkce, které voláme z funkce main a následně i funkcí volaných z funkce main, využívají zásobník pro výpočty i zásobník pro volání funkcí funkce main. Možná to zní trochu složitě :( Instance funkce se vytváří jen pokud spouštíme funkci třídou CScript, tedy její metodou StartMain nebo RunFunction, ne jindy! Zásobníky mají v našem virtuálním stroji pevnou velikost, které jsou určeny konstantami MAX_STACK_SIZE (zásobník pro výpočty) a MAX_RECURSION_DEPTH (zásobník pro volání funkcí). Jou definovány v souboru FunctionInstance.h a obě jsou rovny 1024. Můžete si tuto hodnotu změnit, ale nejlepší řešení je implementace zásobníků, které se budou zvětšovat a zmenšovat podle potřeby.

Další funkcí je Exec_ecall pro vykonání externí funkce:

    //*******************************************************************************************************************
    //instrukce ecall
    //*******************************************************************************************************************

    int CALLBACK CVirtualMachine::Exec_ecall(CFunctionInstance* func,ScriptDependencies* sd)
    {
       //zavola externi funkci
       unsigned int index = *((unsigned int*)&(sd->m_CodeSegment[func->GetPC()]));
       func->AddPC(sizeof(unsigned int));
       unsigned short params = *((unsigned short*)&(sd->m_CodeSegment[func->GetPC()]));
       func->AddPC(sizeof(unsigned short));
       
       ExternFunction* pItem = NULL; //ukazatel na strukturu s informacemi o funkci
       bool found = sd->m_Imports->Get(sd->m_ConstantTable[index].strval,&pItem);
       
       //stejne jako u lokalni funkce, funkce musi byt nalezena v tabulce a ukzatel na strukturu musi byt platny
       if (found && pItem)
       {
          //zkontrolujeme handle knihovny a adresu, aby nahodou nebyly neplatne, pokud se tak stane,
          //nemuzeme volani provest

          if ((!pItem->hDll) || (!pItem->address))
          {
             Log("Cannot call function '%s'. Address or module handle is invalid. Adress is: %Xh, Module Handle is: %Xh
    ",sd->m_ConstantTable[index].strval,pItem->address,pItem->hDll);
             return 0;
          }
          
          //ukladani parametru do zasobniku
          for (unsigned int i = 0; i < params; i++)
          {
             //jak jiste vime, pri standartnim volani funkce pres assembler se musi argumenty funkce
             //ukladat v opacnem poradi nez jsou zapsany. Protoze ale nas skriptovaci jazyk je uklada do
             //zasobniku poporade, v tom poradi jak je zapiseme, staci argumenty jen ze zasobniku vybirat a ukladat
             //do zasobniku pocitace. A mame i zajisteno ze budou ulozeny od posledniho po prvy

             StackItem va_arg = func->PopStackItem();
             switch (va_arg.kind)
             {
             case INT_CONST:
                __asm push va_arg.intval;
                break;
             case ZSTRING_CONST:
                __asm push va_arg.strval;
                break;
             case DOUBLE_CONST:
                //__asm push va_arg.doubleval;
                break;
             }
          }
          
          //ziskame adresu funkce
          FARPROC address = pItem->address;
          //ted volani fce
          __asm call address;
          
          StackItem va_return;
          //vraci hodnotu?
          switch (pItem->type)
          {
          case INT_CONST:
             __asm mov va_return.intval, eax;
             va_return.kind = INT_CONST;
             func->PushStackItem(va_return);
             break;
          case ZSTRING_CONST:
             __asm mov va_return.strval, eax;
             va_return.kind = ZSTRING_CONST;
             func->PushStackItem(va_return);
             break;
          case DOUBLE_CONST:
             /*__asm mov va_return.doubleval, eax;
             va_return.kind = DOUBLE_CONST;
             func->PushStackItem(va_return);*/

             break;
          default:
             /*va_return.kind = INT_CONST;
             va_return.intval = 0;
             func->PushStackItem(va_return);*/

             break;
          }
       }
       
       return 1;
    }

Jako v předdchozím případě je nejdřív nutné zjistit jméno funkce z tabulky konstant a následně informace o funkci z hash tabulky importovaných funkcí. Provede se kontrola handle knihovny ve které se funkce nachází a adresy funkce. Pokud je něco nullového :), znamená to, že buď knihovna není načtena v paměti nebo daná funkce v knihovně není. A v tom případě nemůžeme pokračovat. Následuje uložení parametrů funkce do zásobníku. Tentokrát nejde o naše zásobníky, ale o zásobník počítače. To se snad dá poznat z toho cyklu a z toho že ukládáme pomocí assembleru. Určitě znáte aspoň trochu assembler a víte že pokud voláte funkci z assembleru, musíte její parametry uložit od posledního po první. Mám pocit že už jsem to v tomhle kurzu tajké někde vysvětloval. Pokud jste to nevěděli, tak už to víte :) Volání funcke provedem pomocí instrukce call adress. To je instrukce assembleru pro volání funkce, což je v podstatě skok na danou adresu. Ale nemůžete to nahradit instrukcí pro nepodmíněný skok, protože ta neukládá obsahy registrů aby bylo možné se z funkce vrátit a pokračovat tam kde se skončilo! V posledním kroku se podíváme, jestli měla funkce vracet hodnotu a pokud ano, tak ji přečteme a uložíme na zásobník. Pokud funkce vrací hodnotu, je to vždy přes registr eax. Můžete si také všimnout, že virtuální stroj zatím neumí předat parametry typu double. Je to jednoduše tím, že jsem nepřišel na to jak to udělat. Klasickým způsobem jako u osatních proměnných to nejde, protože v assembleru se používají maximálně 32bitová celá čísla. Double má za prvé víc než 4 bajty a za druhé je to desetinné číslo a o ty se stará koprocesor. Procesor a koprocesor si data vyměňují přes opeační paměť, ale nevím jak je to v případě, že jde o parametr funkce. Nejspíš se to dělá přes ukazatele, ale nejsem si jistý. Pokud to budete chtít zprovoznit a používat externí funkce s double parametry, napište si vlastní knihovnu s funkcemi, které budou požadovat vždy ukazatel na double! Tady ve virtuálním stroji budete vždy ukládat na zásobník ukazatel na double (jako v případě řetězce, ukládáte také vlastně jen ukazatel a ne celý řetězec!). Můžete to brát jako další domácí úkol této lekce :) Stejné to bude s vracením výsledku, také přes ukazatel! Budete tedy double předávat odkazem a ne hodnotou jakou integer! Pokud někdo máte nápad, jak předávat double hodnotou, budu rád když mi o tom napíšete!

Když už jsme u toho volání funkcí, poslední dvě funkce na které se podíváme budou Exec_return a Exec_vreturn:

    //*******************************************************************************************************************
    //instrukce return
    //*******************************************************************************************************************

    int CALLBACK CVirtualMachine::Exec_return(CFunctionInstance* func,ScriptDependencies* sd)
    {
       //provede navrat z funkce a nevraci se hodnota
       int arg = func->GetNumberOfParams();
       
       func->PopActivation();
       
       func->Pop(arg);
       return 1;
    }
    //*******************************************************************************************************************
    //instrukce vreturn
    //*******************************************************************************************************************

    int CVirtualMachine::Exec_vreturn(CFunctionInstance* func,ScriptDependencies* sd)
    {
       //uloz si posledni hodnotu, tj tu co se vraci
       StackItem var_return = func->PopStackItem();
       
       //kolik mela funkce parametru
       int args = func->GetNumberOfParams();
       
       //vrat se do puvodni fce, tj tady se obnovi obsah PC, BSP a SP
       func->PopActivation();
       
       //odloz argumenty funkce
       func->Pop(args);
       
       //uloz na vrchol zasobniku vracenou hodnotu
       func->PushStackItem(var_return);
       
       return 1;
    }

Jsou to skoro stejné funkce :) První emuluje instrucki return, která provádí návrat z funkce, která nevrací žádnou hodnotu, tedy návrat z metody. V tom případě stačí jen snížit vrchol zásobníku pro výpočty o počet argumentů funkce a vrátit se v zásobníku pro volání funkcí na původní položku, tedy snížit jeho vrchol o 1. Ukazatel func bude ukazovat zase na původní čítače PC, SP, BSP a můžeme vesele pokračovat v tom co jsme dělali před voláním funkce :) Druhá funkce emuluje instrukci vreturn, která se používá ve funkcích které vracejí hodnotu. Vrácená hodnota musí být po návratu z funkce na vrcholu zásobníku. Proto je nutné si ji zapamatovat, vrátit vše do původního stavu stejně jako ve funkci Exec_return a potom uložit vrácenou hodnotu na vrchol zásobníku.

Závěr:

To podstatné z virtuálního stroje jsme probrali. Když věnujete ještě pár hodin samostudiju kódu celého projektu, určitě ho pochopíte :) Doufám, že tento kurz byl pro vás zajímavý a že vám něco dal :) Za případné návrhy na vylepšení zaslané na moji adresu budu velmi rád, dotazy také zodpovím, pokud to bude v mé moci a kritiku hned smažu :) Gratuluji, máte svůj první skriptovací jazyk a virtuální stroj! A pokud jste dostatečně "šílení", můžete začít pracovat na nějaké konkurenci pro Visual Basic Script, Java Script či Javu, i když to jsou celkem fajn jazyky se silným postavením, což by mohl být trošku problém :)

Radek Henyš

Část 9