1. Sta je vTable?
vTable je u najosnovnijem obliku niz "funkcijskih" pointera. Sa tom razlikom sto svaki od tih pointera ne pokazuje na neki podatak vec na kod odredjenog metoda. Prva greska koju ljudi prave je da vTable direktno asociraju sa OOP-om. Iako OOP ne moze funkcionisati bez vTable-a, nista vas ne sprecava da u C-u napravite niz function pointera, popunite tu listu i onda radite "indirektne" pozive preko tog niza (http://www.codeproject.com/cpp/PolyC.asp), mada se to naravno ne radi u praksi

2. Sta omogucava vTable?
Omogucava dve veoma vazne OOP tehnologije, polimorfizam i interfejse.
3. Kako to ustvari radi?
Ok, uzmimo ovo parce koda (C#):
public class Deda
{
public int polje1;
public static void Staticki() {}
public virtual void Metod() {}
public virtual void Metod(int par) {}
public void Obicni() {}
}
class Class1
{
[STAThread]
static void Main(string[] args)
{
Deda d = new Deda();
Deda.Staticki();
d.polje1 = 123;
d.Metod();
d.Metod(1234);
d.Obicni();
}
}
Prilicno jednostavna klasa, jedno polje, jedan staticki metod, dva virtuelna overloaded metoda i jedan obicni metod. U Class1 instanciramo tu klasu i pozivamo sve sto treba.
C# za razliku od C++-e ne pravi native code, pa se necu zadrzavati mnogo na MSIL-u vec cemo tretirati kao da je sve to vec stiglo do native koda.
Kompajler, kad bilduje klasu, ima nekoliko aspekata.
Sva polja idu direktno u memorijski potpis klase. Staticke metode i obicne metode se linkuju staticki na ulaz u odgovarajuci kod metoda (jedina razlika izmedju statickih i obicnih metoda je da se statickom ne prosledjuje skrivena referenca na objekat ciji metod se poziva), vTable nema nikakav uticaj ovde.
E sad, za svaki virtual metod (i virtuelne property accessore), alocira se slot u vTable i u tu poziciju se "upuca" pointer na kod metoda. vTable se alocira van objekta na pocetku aplikcije i svaka klasa koja ima bar jedan virtuelni metod ima svoj vTable koji koriste svi objekti te klase. Na kraju, memorijski potpis ove klase izgleda ovako (bar u skolskom primeru, vise o tome posle):
Instanca klase (iliti objekat) vTable za Deda (samo jedan za celu aplikaciju)
+------------------------------+ +--------------------------------+
| Pointer na vTable | ---------> | [0] pointer na Deda.Metod() |
| polje1 | | [1] pointer na Deda.Metod(int) |
+------------------------------+ +--------------------------------+
Sta se desava pri pozivanju? Ovako izgleda MSIL kod u Class1
.locals init (
[0] ConsoleApplication1.Deda deda1)
// Deda d = new Deda();
L_0000: newobj instance void ConsoleApplication1.Deda::.ctor()
L_0005: stloc.0
// Deda.Staticki()
L_0006: call void ConsoleApplication1.Deda::Staticki()
// d.polje1 = 123
L_000b: ldloc.0
L_000c: ldc.i4.s 123
L_000e: stfld int32 ConsoleApplication1.Deda::polje1
// d.Metod()
L_0013: ldloc.0
L_0014: callvirt instance void ConsoleApplication1.Deda::Metod()
// d.Metod(1234);
L_0019: ldloc.0
L_001a: ldc.i4 1234
L_001f: callvirt instance void ConsoleApplication1.Deda::Metod(int32)
// d.Obicni();
L_0024: ldloc.0
L_0025: callvirt instance void ConsoleApplication1.Deda::Obicni()
Prva stvar koja bode oci je d.Obicni(). Ako to nije virtuelni metod, zasto se koristi callvirt? Razlog je u tome sto sam C# kompajler uopste i ni na koji nacin ne radi sa vTable

class Class1
{
[STAThread]
static void Main(string[] args)
{
Deda d = new Deda();
00000000 push ebp
00000001 mov ebp,esp
00000003 sub esp,8
00000006 push edi
00000007 push esi
00000008 mov dword ptr [ebp-4],ecx
0000000b xor esi,esi
0000000d mov ecx,9D5140h
00000012 call F9B21FC0
00000017 mov edi,eax
00000019 mov ecx,edi
0000001b call dword ptr ds:[009D5188h]
00000021 mov esi,edi
Deda.Staticki();
00000023 call dword ptr ds:[009D5180h]
d.polje1 = 123;
00000029 mov dword ptr [esi+4],7Bh
d.Metod();
00000030 mov ecx,esi
00000032 mov eax,dword ptr [ecx]
00000034 call dword ptr [eax+38h]
d.Metod(1234);
00000037 mov ecx,esi
00000039 mov edx,4D2h
0000003e mov eax,dword ptr [ecx]
00000040 call dword ptr [eax+3Ch]
d.Obicni();
00000043 mov ecx,esi
00000045 cmp dword ptr [ecx],ecx
00000047 call dword ptr ds:[009D5184h]
}
0000004d nop
0000004e pop esi
0000004f pop edi
00000050 mov esp,ebp
00000052 pop ebp
00000053 ret
Pa ajd da krenemo redom. Prva linija je pozivanje konstruktora i smestanje reference u lokaciju 0, na asmeblerskom delu se vide dva poziva, prvi je nevazan za pricu (alokacija), drugi je poziv konstruktora i smestanje reference u ESI registar (koji u stvari predstavlja MSILovu lokaciju 0).
Deda.Staticki();
MSIL koristi call konstrukciju, JIT koristi indirektan poziv (za one slabije sa asemblerom, call je direktan poziv na fisknu adresu unutar istog code segmenta, call dword ptr je indirektni poziv, tj, idi na adresu u operandu, uzmi adresu metoda i pozovi tu adresu)
Sada iskace pitanje, ako smo rekli da je u pitanju staticki poziv i ako znamo da JIT definitivno zna adresu na koju je smestio Staticki(), zasto se u asembleru koristi indirektni poziv (call dword ptr) a ne staticki poziv?
To pitanje je na mestu i odgovor malo kasnije, ali posledica trenutne implementacije JIT-a je da nikad (ali nikad) ne koristi staticke pozive za MSIL kod. Iako delimicno smanjuje performanse programa ovo nema toliko znacaja u funkcionisanju koliko ima u kasnijoj raspravi o early/late binding, al otom potom.
d.polje1 = 123;
MSIL ucitava lokaciju 0 (referenca na d), ucitava kosntantu 123 i koristi stfld da upuca vrednost u polje1. JIT to prevodi u indirektni mov (mov dword ptr). ESI nosi adresu reference, na 4 bajta udaljenosti od pocetka nalazi se memorijska lokacija polje1 i u nju upucava konstantu 7Bh (123). Ako pogeldate dijagram sa pocetka, na adresu ESI se nalazi objekat, na lokaciji ESI+0 je pointer na vTable, te je na lokaciji ESI+4 locirano polje1.
d.Metod();
ok, poziv virtuelnog metoda. MSIL ucitava lokaciju 0 (referencu kao skriveni parametar za metod) i poziva Metod() sa callvirt. JIT na osnovu metadata zakljucuje da je poziv virtuelnog metoda i koristi vTable.
Ovako, na adresi ESI+0 je pointer na vTable, pa je proces sledeci:
- mov ecx, esi - kopira referencu na objekat u exc (skriveni parametar)
- mov eax, eax,dword ptr [ecx] - kopira u eax cetiri bajta sa lokacije ecx+0 (tj esi+0), tj pointer na pocetak vTable. U ovom trenutku eax (navodno) pokazuje na vTable.
- call dword ptr [eax+38h] - ovo je pravi indirektni vTable-driven polimorficki poziv.

d.Metod(1234);
Slicno kao gore, samo sto EDX nosi neskriveni parametar 1234. Takodje, vidi se da je lokacija u vTable sada +3Ch (sledeca lokacija u vTable)
d.Obicni();
Slicno kao Staticki(), poziva se indirektni metod, samo sto se pre poziva u ecx ubacije referenca (skriveni parametar), sto je jedina razlika izmedju pozivanja statickog i objektnog metoda.
4. Nasledjivanje...
ako dodamo sledecu klasu
public class Unuk: Deda
{
public override void Metod() {}
}
class Class1
{
[STAThread]
static void Main(string[] args)
{
Deda d = new Deda();
Deda u = new Unuk();
d.Metod();
u.Metod();
}
}
Preskocicemo MSIL i dati samo asm:
Deda d = new Deda();
00000010 mov ecx,9D5140h
00000015 call F9B21FC0
0000001a mov esi,eax
0000001c mov ecx,esi
0000001e call dword ptr ds:[009D5188h]
00000024 mov edi,esi
Deda u = new Unuk();
00000026 mov ecx,9D51C8h
0000002b call F9B21FC0
00000030 mov esi,eax
00000032 mov ecx,esi
00000034 call dword ptr ds:[009D5208h]
0000003a mov ebx,esi
d.Metod();
0000003c mov ecx,edi
0000003e mov eax,dword ptr [ecx]
00000040 call dword ptr [eax+38h]
u.Metod();
00000043 mov ecx,ebx
00000045 mov eax,dword ptr [ecx]
00000047 call dword ptr [eax+38h]
Iz ovog se vidi da su dva poziva identicna (call dword ptr [eax+38h]), ali opet d.Metod() poziva Deda.Metod, a u.Metod poziva Unuk.Metod() iako su i u i d istog tipa (Deda).
"Tajna" je u vTable-u. U ovom primeru edi nosi referencu na d, a ebx nosi referencu na u. Jedina razlika je ta sto se na adresama edi+0 nalazi vTable za Deda klasu (u ovom primeru 9D5140h), a na ebx+0 adresa za vTable klase Unuk (9D51C8h), u te dve tabele na lokaciji 38h nalaze se dva razlicita funkcijska pointera, i to je nacin na koji vTable omogucava polimorfizam, bez obzira na to koji je tip reference, odluku o tome koji metod se poziva donosi odgovarajuca lokacija u vTable.
5. Kako to .NET radi za razliku od C++-a...
Ono sto mozda zapadne za oko je pitanje: ako Klasa Deda ima samo dva virtuelna metoda, zasto su funkcijski pointeri na lokacijama 34h i 38h a ne 0h i 4h?
Odgovor lezi u nacinu na koji JIT prevodi MSIL kod. Klasicni (C++) kompajler i linker, prevo prevede kod u prvom prolazu usput pamteci staticke lokacije metoda i lokacije sa kojih se poziva, onda linker na osnovu te tabele izbilda sve vTable-ove i upuca "staticke" lokacije u pozive.
JIT sa druge strane (verovatno zbog brzine prevodjenja) ima samo jedan prolaz kroz kompajler/linker. U takvom rezimu JIT kad prevodi poziv metoda ne zna gde se kod tog metoda nalazi pa da bi izbegao dvoprolaznost, ima mali korak na pocetku: za svaku klasu (iz metadata) JIT rezervise prostor za vTable koji sadrzi SVE metode, i staticke i obicne i virtuelne. Tako da iako u trenutku prevodjenja mozda ne zna gde ce se nalaziti npr. Deda.Staticki() ali ZNA da ce pointer na taj metod biti na lokaciji u vTable ciju adresu zna. I tako kako prolazi kroz metod po metod, generise native code i indirektne pozive drugih metoda i onda na kraju upuca entry-point u odgovarajucu lokaciju u vTable, bez potrebe da u drugom prolazu tu adresu stavi u sve pozive tog metoda, tako da vTabele za nase klase ustvari izgledaju ovako:
Instanca klase Deda vTable za Deda
+------------------------------+ +--------------------------------+
| Pointer na vTable | ---------> | <rezervisano ili nevazno> |
| | | Deda.Staticki() |
| polje1 | | Deda.Metod() |
+------------------------------+ | Deda.Metod(int) |
| Deda.Obicni() |
+--------------------------------+
Instanca klase Unuk vTable za Unuk
+------------------------------+ +--------------------------------+
| Pointer na vTable | ---------> | <rezervisano ili nevazno> |
| | | Deda.Staticki() |
| polje1 | | Unuk.Metod() |
| polje2 | | Deda.Metod(int) |
+------------------------------+ | Deda.Obicni() |
+--------------------------------+
C++ programeri se u startu mrste na ovo

6. Interfejsi
Interface je funkcionalno nista drugo do "ogoljena" referenca koja sadrzi samo pointer na vTable koji se nalazi unutar vTable klase koja ga implementira. Recimo da postoji Interface IDeda koji ima jedan metod (void Obicni()) i da klasa Deda implementira taj interface. vTable za IDeda ne pocinje na istoj lokaciji kao vTable za Deda klasu nego na lokaciji koja definise pointer na metod Obicni(). Naravno, ovo implicira da se svi metodi nekog interface-a u vTable klase koja ih sadrzi nalaze jedan iza drugog i tako je bilo bar do sada

Implementacija Interface-a na .NET JIT je komplikovana (konsultuju se tri tabele umesto jedne), pa cu tu pricu ostavitri za neki drugi put.
7. Early/Late binding
Kompjuterski naucnici definisu late binding kao poziv "necega" cija se adresa na zna do trenutka pozivanja. Po toj definiciji svi virtuelni metodi su late-bound (zato sto se adresa indirektno odredjuje iz vTable a pri kompajliranju se ne zna koji ce tacno vtable biti u referenci), dok su svi staticki i objektni metodi early-bound, i tako je bilo do skoro

Terenci (kao sto sam ja) uglavnom smatraju ovu definiciju zastarelom, narocito sa uvodjenjem .NET-a
Na osnovu prethodne diskusije vidi se da po toj definiciji uopste ne postoji early binding u .NETu posto su svi pozivi implementirani sa [call dword ptr]. Zabunu su pokusali da rese tvrdnjom da se definicija odnosi samo na sors code, ali je to uvelo jos vecu zabunu i otezalo odredjivanje sta je u stvari late a sta early binding, narocito za manje iskusne korisnike, a i efektivno cemu definicija ako nema neko utemeljenje u realnoj praksi, mozemo da se lazemo tri dana da je to early-binding kad je implementacija i early i late bound poziva ista

U celoj toj prici postoji i koncept dinamickog bindovanja kakav koristi COM gde se klasa dinamicki "ucitava" i onda konsultuje type-library da se locira ID metode i zatim pozivanje metoda obavlja kroz IDispatch interfejs (slican mehanizam za .NET postoji u refleksiji), i to je tehnika koja sa sobom vuce pad performansi, i koji se u VB terminlogiji tretira kao late binding, i sa kojim se i ja licno slazem.
Definicija koju ja prihvatam je da je early binding poziv nekog metoda cija adresa je poznata u trenutku prevodjenja ILI je prisutna u adresnom prostoru programa u trenutku poziva. Late bound su svi pozivi cija odredisna adresa mora da se "sracuna" prilikom pozivanja.
[Ovu poruku je menjao mmix dana 25.07.2006. u 12:31 GMT+1]
naroda koji je bio veseo, pomalo površan, od jednog naroda koji je bio znatiželjan, koji
je voleo da vidi, da putuje, da upozna,
od naroda koji je bio kosmopolitski napravio narod koji je namršten, mrzovoljan,
sumnjicav, zaplašen, narod koji se stalno nešto žali, kome je stalno neko kriv - Z.Đinđić