Virtuální stroj - Část První a poslední:) |  |
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 `null`ové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