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
|