Objecten in Visual Objects: van Code to Garbage
In dit artikel wil ik jullie wat meer vertellen over de manier waarop Visual Objects met objecten omgaat. Ik wil daarbij aandacht besteden aan de rol van:
- de Compiler
- de Linker
- de Runtime
Als voorbeeld neem ik de volgende code:
CLASS Person
PROTECT _cName AS STRING
PROTECT _nAge AS DWORD
PROTECT _cGender AS STRING
METHOD Init(cName, nAge, cGender) CLASS Person
_cName := cName
_nAge := nAge
_cGender := cGender
ACCESS Name CLASS Person
RETURN _cName
ASSIGN Name(cName) CLASS Person
_cName := cName
return cName
De Compiler en klassen
Allereerst wil ik het hebben over de rol van de Compiler.
Compileren van de klasse definities
Wanneer deze een klassedefinitie als hierboven tegenkomt, doet de compiler een heleboel zaken:
- Er wordt assembly code gegenereerd waarmee de klasse door de Runtime kan worden aangemaakt. Deze sourcecode maakt gebruik van de (niet gedocumenteerde) functie DeclareClass() . De parameters voor deze functie zijn o.a. de klassenaam (als Symbol), de Parent klasse (ook een symbol), de grootte van elk object en een lijst van de typen en namen van de geexporteerde IVars. De grootte van de objecten is in dit geval 20 bytes: 12 bytes voor de eigenlijke data (strings vragen 4 bytes voor een pointer in het object ) en 8 bytes voor een pointer naar de VTable en een pointer naar de Class Structure.
- De VTable is een structure van pointers naar de methoden en accesses/assigns en zal worden opgebouwd door de linker indien een klasse gebruik maakt van strong typing. De Class Structure wordt door de Runtime opgebouwd.
Compileren van de (access & assign) methoden
Bij het compileren van een methode zal de Compiler vanzelfsprekend controleren of de betreffende klasse aanwezig is.
Daarnaast zal de compiler de benodigde assembly code genereren. Omdat de argumenten van de methode niet getypeerd zijn, zal de compiler conversies van Usual argumenten naar het juiste type genereren. Vervolgens wordt de daadwerkelijke code uitgevoerd, en tenslotte wordt het resultaat weer vertaalt naar een Usual, zodat het in de vorm van een usual kan worden teruggegeven. Als het argument per Reference werd doorgegeven wordt die referentie opgeheven.
Zo ziet de assembly voor de Name assign er gedeeltelijk als volgt uit (zonder optimalisaties)
STARTUP Code
LOAD Parameter in Registers // Usual (8 byte)
CALL Usual2String // Kan error geven !
STORE Register to Ivar // 4 bytes
DEREFERENCE Parameter //
STORE RESULT // 8 bytes IN EAX/EDX CLEANUP CODE
RETURN
Tevens genereert de compiler code waarmee de methode bij het runtime systeem wordt geregistreerd, gebruik makend van de niet gedocumenteerde DeclareMethod() functie:
PUSH 1
PUSH Address method
PUSH Name
PUSH Person
CALL DeclareMethod
Het converteren van de argumenten van usual naar het juiste type en terug is natuurlijk een stuk minder efficiënt dan wanneer je de argumenten van de methoden zou typeren. Dat kun je bereiken door de methode te typeren. Daarop komen we verderop terug.
Compileren van de aanroep van de methoden.
Stel we gaan de klasse als volgt gebruiken.
FUNCTION Start
Local oPerson as Person
oPerson := Person{“Mr Data”,40,”M”}
? oPerson:Name
wait
De compiler zal dan de volgende pseudo:code genereren om het object te maken:
Push “M” // as USUAL, 8 bytes
Push 40 // idem
Push “Mr Data” // idem
Push #Person // idem
Push 4 // # of Usual arguments on the stack
CALL CreateInstance()
Voor het tonen van de Person:Name Access genereert de compiler de volgende pseudo code:
Push #Name // as SYMBOL, 4 bytes
Push oPerson // as Object, 4 bytes
Call IVarGet() // Lees property
Store Result in temp var. // 8 bytes, result = USUAL
Push temp var // as USUAL so 8 bytes
Push 1 // # of arguments for Qout
Call QOUT
Je ziet dat de compiler niet direct de access methode aanroept, maar gebruik maakt van IVarGet().
Het effect op de code van typeren van methoden
Om betere performance te krijgen en tevens het voordeel dat de compiler betere controle over je code geeft, kun je methoden en accesses/assigns typeren.
Dat zou er als volgt uit kunnen zien
ACCESS Name AS STRING PASCAL CLASS Person
.
.
ASSIGN Name(cName as STRING) AS STRING PASCAL CLASS Person
.
.
Tevens moet je dan tevens de volgende regels toevoegen aan de klasse definitie:
DECLARE ASSIGN Name
DECLARE ACCESS Name
Als je dat doet ziet de gegenereerde pseudo code voor de Name Assign er als volgt uit
STARTUPCODE
STORE Parameter to IVAR // Schrijf IVar
MOVE EAX Parameter // Return waarde
CLEANUPCODE
RETURN
Je ziet dat de code een stuk efficiënter is dan hiervoor, waarbij ook nog de conversie en de dereferentie wordt uitgevoerd.
Bij het aanroepen van de Name Access ziet de code in de start functie er dan als volgt uit en wordt er geen gebruik meer gemaakt van de IvarGet().
CALL PERSON:NAME:ACCESS
Store Result in temp var. // 8 bytes, result = USUAL
Push temp var // as USUAL dus 8 bytes
Push 1 // Aantal argumenten voor Qout
Call QOUT
Bij het compileren van getypeerde methoden wordt echter ook nog de volgende code gegenereerd (een methode zonder naam, dit wordt de stub-methode genoemd):
LOAD Parameter in Registers // USUAL (8 byte)
CALL Usual2String // Kan error geven !
PUSH Result // STRING PTR
CALL PERSON:NAME:ASSIGN // Resultaat in EAX
STORE STRING TYPE // in EDX,
RETURN
Deze code wordt door de VO runtime aangeroepen als je de Assign Late-bound aanroept, zoals bijv in onderstaande code.
LOCAL oPerson as OBJECT
oPerson := FunctionThatReturnsAPerson()
oPerson:Name := “Robert”
De compiler maakt dus per getypeerde methode 2 methoden:
- De Getypeerde methode zelf.
- Een niet getypeerde stub methode.
De eerste methode wordt aangeroepen wanneer de compiler tijdens het compileren het type van een methode kan vaststellen
De tweede methode wordt aangeroepen vanuit de IvarGet()/IVarPut() en Send() functies.
Je kunt je voorstellen dat er VO gebruikers waren die dit een beetje teveel van het goede vonden.
Daarom is de ONLYEARLY compiler pragma bedacht.
Die heeft twee betekenissen:
1. Als je hem in je klasse definitie opneemt, zorg hij er voor dat er geen stubs worden gegenereerd. Tevens worden er geen DeclareMethod()meer gegenereerd. Dat kan dus aanzienlijk in grootte schelen voor grote projecten.
Je gerbuikt het pragma als volgt:
CLASS Person
PROTECT _cName AS STRING
PROTECT _nAge AS DWORD
PROTECT _cGender AS STRING
~”ONLYEARLY+”
DECLARE ASSIGN Name
DECLARE ACCESS Name
~”ONLYEARLY-”
2. Als je het ONLYEARLY pragma in je code opneemt, zorgt hij ervoor dat er geen late-bound calls (Ivar..() en Send() ) worden gegenereerd. Als je dat dan toch probeert, zal de compiler een waarschuwing geven. In je code ziet het er dan als volgt uit:
FUNCTION Start
~”ONLYEARLY+”
Local oPerson as Person
oPerson := Person{“Mr Data”,40,”M”}
? oPerson:Name
wait
~”ONLYEARLY+”
De Linker en klassen
In dit onderdeel zullen we kort stilstaan bij wat de rol van de Linker is m.b.t. de klassen.
Bouwen van de initialisatie code
Een van de rollen die de linker in VO heeft, is het verzamelen van alle code die door de compiler is gemaakt waarin de DeclareClass, DeclareMethod calls enz. staan. De linker maakt een speciale functie aan in elke EXE en DLL, waarin al deze code wordt aangeroepen. Deze functie heet __VoDllClassInit().
Bouwen van de VTable
Wanneer er getypeerde methoden in een klasse voorkomen, zal de Linker een tabel maken waarin pointers naar deze methoden komen te staan. Dit wordt de VTable genoemd. Een verwijzing naar deze tabel wordt in de code opgenomen die vanuit de __VoDllClassInit() wordt aangeroepen. Het adres van de VTable is nl. één van de argumenten voor de DeclareClass() functie.
De Runtime en klassen
Tenslotte moeten de klassen natuurlijk door de Runtime worden aangemaakt en beheerd.
We hebben al gezien dat de compiler en de linker de informatie over de klassen hebben klaargezet in de __VoDllClassInit() functie. Deze functie wordt automatisch aangeroepen door de Runtime wanneer een VO EXE of DLL wordt gestart.
Onderstaande figuur laat zien hoe de klassen (en de objecten) er in het geheugen ongeveer uitzien:

-
De blauwe blokjes zijn structures die door de runtime in statisch geheugen worden gemaakt, naar aanleiding van de informatie die door de DeclareClass en DeclareMethod functies wordt doorgegeven.
- De oranje blokjes zijn code die zich in de EXE/DLL bevindt.
- De gele blokjes zijn daadwerkelijke objecten die zich in het dynamisch geheugen bevinden.
Het aanmaken van objecten door de Runtime
Op basis van de informatie die de Runtime in de klasse structuur vindt worden objecten aangemaakt in dynamisch geheugen. Zoals hierboven al vermeld hebben alle objecten een verwijzing naar de klasse structuur en naar de VTable. Runtime functies als ClassName(), IsInstanceOf() , IVarGet(), Send() enz. maken gebruik van de link naar de klasse structuur.
De runtime plaats pointers naar deze twee zaken in elk object.
Het opruimen van objecten door de Runtime
Dit is de verantwoordelijkheid van de Garbage Collector. In principe gaat dit heel eenvoudig. Het dynamisch geheugen van VO bestaat uit twee banken. Elke keer als de Garbage Collector actief wordt kopieert hij de gebruikte blokken geheugen (strings, objecten, arrays & floats) van de ene bank naar de andere en past de verwijzingen naar die blokken geheugen aan.
Wanneer er geen verwijzingen naar een object meer gevonden worden, wordt het niet meer gekopieerd en houdt het op te bestaan.
Als je zelf code wilt laten uitvoeren op het moment dat een object ophoud te bestaan, kun je gebruik maken van een zg. Axit() methode. Je dient het object dan te registreren bij de Garbage Collector via de RegisterAxit() functie. Dat zorgt voor de volgende twee zaken:
- Er wordt een bit in de header van het object gezet, die aangeeft dat het object een Axit() behoeft
- In een tabel wordt een verwijzing naar het object opgenomen.
De runtime functie UnRegisterAxit() doet precies het omgekeerde!
Nadat de Garbage Collector alle objecten in het dynamisch geheugen heeft verplaatst naar de nieuwe bank, wordt een functie aangeroepen die alle geregistreerde objecten langsloopt.
Als nu blijkt dat het object verplaatst is naar de andere bank, wordt de verwijzing in de tabel bijgewerkt met het nieuwe adres. Anders wordt er een bit in de header van het object gezet, zodat duidelijk is dat het object opgeruimd is, en worden alle verwijzingen naar dynamisch geheugen in het object ook opgeruimd en wordt de tabel bijgewerkt.
Om problemen te voorkomen (als je bv. iets zou doen in de Axit() methode dat een nieuwe run van de Garbage Collector zou activeren) wordt het dynamisch geheugen tijdens dit hele proces Gelockt middels een DynLock()/
Het aantal objecten dat je van een Axit() methode kunt voorzien is beperkt. Het aantal staat standaard op 16000. Je kunt het ook via het register instellen in de sleutel: HKEY_CURRENT_USER\Software\ComputerAssociates\CA-Visual Objects Applications\Runtime, de setting MaxRegisteredAxitMethods. Je kunt de setting ook kwijt in je HKEY_LOCAL_MACHINE, wat handig kan zijn als je Services programmeert of een DCOM component dat anoniem wordt gestart.
Helaas is deze instelling globaal, d.w.z. dat hij geldt voor alle VO applicaties op het betreffende werkstation.
In VO 2.7 wordt het mogelijk deze instelling vanuit je applicatie dynamisch in te stellen (via de nieuwe functie SetMaxRegisteredAxitMethods), maar daar zul je nog even op moeten wachten.
Epe,
Juni 2003
Robert van der Hulst