Da-mi taticule si mie o lopata si-o mistrie

Cei ce trec prin viata doar in plimbare
Uita ca exista si penitenciare
Unde nu-i soare si nu creste-o floare
Si doare cind musti din covrig

Nenumarati contabili au flatulat de-a lungul timpului perspective numerologice asupra traiului de programator, de la regula 80/20, care a fost aplicata sistematic si contradictoriu tuturor perechilor posibile de cantitati numarabile (dar nu neaparat masurabile) din aceasta ingrata activitate, pina la transpiratul 10/50/30/10. Eu propun o simplificare a acestor dihotomii, triotomii si cvadruplotomii ciomuistice: programatorii isi petrec majoritatea timpului facind cacaturi care n-au absolut nici o legatura cu lucrul pe care vor sa-l programeze.

Cea mai de cacat situatie in care te poti gasi ca programator este atunci cind trebuie sa cooperezi cu niste cod facut de altcineva, adica intotdeauna. In general tu vrei sa faci o chestie clara, dar pina sa ajungi sa scrii alea 2 linii care te intereseaza pe tine trebuie sa te bati cu sute de muisme care iti stau in drum: compilatoare, IDE-uri, SDK-uri, framework-uri, biblioteci, OS-uri si asa mai departe. In loc sa te preocupi de ce face codul tau, te chinui sa intelegi care din mizeriile de lib-uri pe care le folosesti fragmenteaza memoria intr-un asemenea hal ca nu mai vrea OS-ul sa-ti dea 20 de amariti de megi de memorie, cind tu ai commit size pe la 700 MB.

Mai deunazi, in uzina la noi se barbota cu sirg la un plug-in de Max care trebuia sa coopereze cu un alt plug-in, ce nu era al nostru. Acest alt plug-in avea amabilitatea de a oferi chiar un SDK, adica niste headere cu nume de variabile din maxim 2 litere si fara documentatie, deci nu se intrevedeau incidente. Totul a mers ca uns pina in clipa in care am chemat o metoda numita “Eval” si am luat unresolved external.

Vedeti voi, plug-in-ul nostru nu poate fi linkat cu lib-urile celuilalt plug-in, caci trebuie sa se incarce si daca plug-in-ul alalalt nu exista. Daca punem o dependinta statica catre DLL-ul lor, al nostru nu mai merge decit daca e si al lor instalat. In concluzie, trebuie sa incarcam dinamic un pointer la metoda aia s-o apelam doar daca exista, lucru banal in teorie. Procesoarele, aceste chestii de care in zilele noastre sint mai preocupati gamerii decit programatorii, chiar au o instructiune pentru apelat functii, deci n-ar trebui sa existe articole pe blog-uri despre asta, nu? Ei bine, pina la CPU exista C++, compilator si Max, iar retardarile lor combinate dau o retardare mai mare decit suma partilor.

O metoda e o functie ordinara careia trebuie sa-i pasezi cumva si this-ul pe sub mina. In functie de platforma, dinsul trebuie sa fie pus intr-un registru sau pe stiva. Pe linga instructiunea de apelat functii, CPU-ul, acest lucru magic, are in mod surprinzator si instructiuni pentru pus chestii in registri sau pe stiva. Problema e cum il convingi pe compilator sa le genereze cind tu ai un pointer la functia aia – pe care l-ai luat cu GetProcAddress() – si pointerul la obiect.

Intr-o lume ideala s-ar face ceva de genul:

// In expozitiune:
#ifdef _WIN64
const char* methodName = "?Eval@ClasaMuista@@QEAAMM@Z";
#else
const char* methodName = "?Eval@ClasaMuista@@QAEMM@Z";
#endif
void* method = GetProcAddress(dll, methodName);

// In desfasurarea actiunii:
ClasaMuista* instPtr = ...;
float arg = 0.37f;
float retVal;
#ifdef _WIN64
__asm
{
	mov		rcx, instPtr
	movss	xmm1, arg
	call	method
	movss	retVal, xmm0
}
#else
__asm
{
	mov		ecx, instPtr
	push	arg
	call	method
	fst		retVal
}
#endif

Deoarece nu traim intr-o lume ideala, un mintos de la Microsoft care crede ca procesorul e o legenda urbana s-a decis ca Visual C++ nu va avea inline asm in 64-bit. La momentul la care a fost proclamata, aceasta prostie fenomenala a fost intimpinata cu urlete si mui de catre cei afectati, dar pe Microsoft i-a durut la penis. Motivul oficial este la fel de retardat ca decizia in sine: cica nu poti scrie acelasi asm pentru 32 de biti ca pentru 64. Si ce? D-aia avem ifdef, in mortii lui de ifdef. Nu inteleg ce ii fute pe ei grija ca eu trebuie sa fac gimnastica cu ifdef. Daca trebuie, o fac. Alternativele propuse de mintos sint ori sa te descurci cu intrinsics, ori sa scrii codul intr-un fisier separat si sa-l compilezi cu un assembler. Aia cu intrinsics nu se aplica aici, dar si in cazurile in care se aplica o mai da in bara, caci VC nu e tocmai Tata Lor™ la tradusul intrinsic-urilor in cod (ceea ce ma mira foarte, caci de pe banca de unde stau eu mi se pare destul de usor sa produci cod optim din asa ceva, sau cel putin mult mai usor decit alte optimizari pe care compilatorul lor le face bine). P-aia cu assembler-ul extern nu vreau s-o aplic pentru ca mi se pare insultator sa fiu nevoit sa instalez masm sau alta atrocitate ca sa chem o functie.

Un scurt moment am cochetat cu ideea de a face un const char* cu opcode-urile unei functii care pune argumentele unde trebuie si cheama cacatul de metoda, dupa care sa indrept un function pointer spre el si sa-l chem, da’ cirpeala e cam mare, asa ca am zis sa o luam totusi pe calea ortodoxa: facem un pointer la un member function si-l chemam p-ala. Adica:

// Expozitiune:
typedef float (ClasaMuista::*EvalFunc)(float);
union
{
	void*		voidPtr;
	EvalFunc	funcPtr;
} p;
p.voidPtr = GetProcAddress(dll, methodName);
EvalFunc method = p.funcPtr;

// Desfasurarea actiunii:
ClasaMuista* instPtr = ...;
float retVal = (instPtr->*method)(0.37f);

Observam tigania cu union-ul, care-i necesara pentru ca n-ai voie sa castezi void* la pointer la metoda. C++ e retardat si-ti permite sa derivezi din mai multe clase de-odata, caz in care pointerul ala trebuie sa mai contina niste cacat pentru ajustarea corespunzatoare a this-ului. Desi Java a transat problema asta acum vreo 15 ani (spre surprinderea tuturor, caci e singurul lucru facut bine in Java), C++ inca n-a inteles ca numai cretinii deriveaza din mai multe clase cu date in ele. Ba mai mult, ca sa le dea apa la moara cretinilor, ofera si virtual inheritance, acest goatse al OOP-ului care pretinde ca te scoate din cacat cind diagrama ta de clase seamana cu Coloana Infinitului, dar de fapt sileste compilatorul sa produca cod de o uritenie rar intilnita. In fine, C++ n-o sa primeasca interfete doar pentru ca ma oftic eu aici in fata poporului (la urma urmelor, se chinuie de vreo 12 ani sa-i creasca closures si inca nu e gata), dar in cazul de fata toate aceste griji pot fi date uitarii, caci clasa cu care trebuie sa cooperam nu e derivata din nimic, deci pointerii la metodele ei sint pointeri normali. Deci rulam codul si crapa. Cu WTF-ul pe buze, privim in disassembly si ne minunam:

000000013F191043  movss       xmm1,dword ptr [__real@3ebd70a4 (13F1921B4h)]
000000013F19104B  movsxd      rcx,dword ptr [rsp+28h]
000000013F191050  add         rcx,rbx ; Tu cine pula mea esti?
000000013F191053  call        rax

Ce o fi cu acea adunare de colea, ce seamana izbitor de tare cu o ajustare a this-ului pentru multiple inheritance? In acest moment, ne revine din negura vremurilor amintirea ca VC++ are un flag care-ti permite sa fortezi toti pointerii la metode sa foloseasca formatul extins, cu offseti si cacat. Cautam in MSDN si aflam ca flag-ul se cheama /vmg. Privim in project settings, dar nu-i. Mai privim odata in MSDN si observam ca exista si un pragma care face acelasi lucru, moment in care vedem rosu in fata ochilor.

In acest punct de turnura al naratiunii trebuie sa vorbim un pic despre 3D Studio Max si programatorii care lucreaza la el, caci poate nu-i un subiect familiar tuturor. La Max au lucrat de-a lungul timpului cei mai prosti programatori care au scris vreodata cod. Banuiala mea e ca singura cerinta de angajare la Discreet/Kinetix/Autodesk este sa fi lucrat in prealabil la Yahoo Messenger. Proportiile prostiei celor care au facut Max-ul nu se pot descrie in citeva rinduri, insa pentru a va forma o idee pot mentiona ca absolut toti cei care au pus vreodata mina pe SDK-ul de Max, indiferent de nivelul lor de pricepere, au sfirsit prin a dori moartea violenta a dobitocilor care au gindit si implementat acest program de cacat care nu mai moare odata. As dori sa va pot arata niste cod din matele Max-ului, dar cica n-am voie ca NDA si mui.

Cind am vazut ca exista acel pragma, m-am gindit instantaneu la MAXScript. Acest infect limbaj de scripting este facut de cineva cu stofa de Silviu Ardelean, deci am avut o banuiala ca pe undeva prin mecanismul de expus functii C++ in MAXScript voi gasi muia aia. Am dat cu pragma pointers_to_members in virful headerului precompilat, si prompt VC++ mi-a zis:

c:\program files\autodesk\3ds max 2011\maxsdk\include\maxscrpt\..\maxscript\macros\define_instantiation_functions.h(55) : error C2285: pointers to members representation has already been determined – pragma ignored

Muie Max. N-am rabdare sa vad de ce crede dinsul ca are nevoie de cacatul asta, dar va pot garanta ca e gresit. Sigur au futut ceva prin alta parte si au cirpit-o in felul asta. Din cauza imbecilitatii lor, eu nu pot sa chem un cacat de functie.

Ajungind aici, eu m-am enervat si am plecat acasa, dar jos8cal a mai dat un ochi in pagina aia din MSDN care zicea cum e cu pragma si /vmg si a vazut ca mai exista o cirja cu care se poate abuza limbajul: keyword-ul __single_inheritance. Chiar daca un retard ii spune compilatorului sa faca pointeri de cacat, poti reveni la pointeri ordinari pentru clasele care te intereseaza punind keyword-ul ala in declaratie. Deci l-a pus, si a mers. URA AM REUSIT SA CHEMAM O FUNCTIE!

Sau nu. A doua zi, a trebuit sa repetam manevra intr-un plug-in de Maya. Maya nu-i facut de handicapati, deci nu-i nevoie de pragme si keyword-uri si alte chestii d-astea. Din pacate insa, el ruleaza si pe OS-uri de hipsteri si comunisti, unde rolul Domnului Trandafir este jucat de GCC. GCC nu se osteneste sa faca pointerii la metode sa fie pointeri chiori in cazurile cu imbecilitate redusa; dinsul foloseste pointeri d-aia extinsi tot timpul si n-ai cum sa-l rogi sa faca exceptii. Deci ce cacat facem?

GCC are inline asm si in 32 de biti si in 64:

// Expozitiune:
void* method = dlsym(dll, "_ZN11ClasaMuista4EvalEf");

// Desfasurarea actiunii:
ClasaMuista* instPtr = ...;
float arg = 0.37f;
float retVal;

#ifdef __LP64__
__asm__ __volatile__(
	"call *%%rax"
	: "=Yz"(retVal)
	: "Yz"(arg), "D"(instPtr), "a"(method)
	: "memory"
);
#else
__asm__ __volatile__(
	"push (%2)\n"
	"push %3\n"
	"call *%%eax\n"
	: "=t"(retVal)
	: "a"(method), "r"(&arg), "r"(instPtr)
	: "memory"
);
#endif

Singura manevra aici este sa afli cum se cheama constraint-urile alea pentru template-urile de ASM pentru fiecare clasa de parametru care-ti trebuie. Daca il intrebi pe goagal de “gcc inline assembly constraints”, pagina relevanta (adica asta) nu e printre hit-uri (e o pagina care duce la o pagina care duce la asta, totusi). Sint alte pagini insa, scrise de firtati de-ai lui Silviu care simt nevoia sa se exprime desi habar n-au despre ce vorbesc, si care-ti spun, de exemplu, ca constraint-ul pentru rdi este “di”.

Trebuie notat totusi ca am avut bulan ca functia vrea un singur parametru de tip float. In Linux 64-bit, parametrii de tip float se dau in xmm0, xmm1 etc. Exista un constraint pentru xmm0 (numit intuitiv “Yz”, cum se vede mai sus), dar pentru restul nu mai exista (e doar “x”, care inseamna orice registru SSE, adica il lasi pe nenorocit sa aleaga). Ca sa-ti dai parametrii in cazul ala, ii pui in variabile, dai adresele variabilelor, faci lucru manual cu movss ca sa le incarci in registri si pui registrii respectivi in clobber list. Nu ca ce se intimpla deasupra ar fi optim sau ca ar conta, da’ totusi, obscen.

E de notat ca puteam folosi pointerul la metoda si in GCC, si in VC++ fara __single_inheritance, daca ne bateam capul sa scriem chestii in dinsul astfel incit sa faca call-ul care ne intereseaza (layout-ul e documentat pentru GCC si se gaseste pe net sau cu ochiu’ in dezasamblare pentru VC++). Mie asta mi se pare un hack si mai mare decit un const char* cu opcode-urile care fac ce doresc.

Dragi tovarasi, VICTORIE! Am reusit sa chemam o functie! Programarea e o meserie magica, plina de satisfactii, unde visele devin realitate prin simpla apasare a unor butoane!

PS: toata aceasta voma se putea evita daca aia care au facut celalalt plug-in se prindeau ca atunci cind vrei sa expui clase C++ dintr-un DLL, e o idee buna sa faci o interfata si s-o dai p-aia afara. Daca ClasaMuista::Eval() era virtuala, instanta aia de ClasaMuista pe care o primeam de la ei ar fi avut in ea un pointer la vtable, in care s-ar fi aflat si adresa functiei Eval() si ar fi mers totul ca prin minune. Runtime dispatch, chestii avansate nu gluma. Maxim 0.43% din totalul programatorilor de pe planeta asta sint in stare sa faca un API, dar din pacate un procent mult mai mare incearca.

Tags: , , , , , , , ,

5 Responses to “Da-mi taticule si mie o lopata si-o mistrie”

  1. abc Says:

    Sau cu un .dll/.so intermediar linkat cu lib-urile plug-in-ului cu pricina.

  2. Mihnea Says:

    DLL-ul intermediar ar fi complicat deployment-ul, ca trebuia sa vad unde sa-l pun si cum sa aflu la runtime unde l-am pus, ca sa-l incarc. Daca ar fi trebuit sa chemam mai multe chestii din celalalt plug-in, am fi facut un DLL intermediar, ca sa nu stam sa incarcam jdemii de pointeri la metode, dar mi se pare jenibil sa fac un DLL care cheama o functie si atit.

  3. Luca Says:

    Stimate Domn, rar am apucat sa citesc o scriitura de asemenea deliciu. Cred ca multora dintre noi ne trec zilnic o parte din gandurile de bine exprimate aici in articol in munca noastra de transpirare intelectuala.

    Daca ti-ai aloca timp si ti-ai aduna vointa, as cumpara o carte cu un asemenea continut fara sa discut. La cat mai multe articole!

  4. Tontu Says:

    Domnule, respect adanc, cum zicea unu’ mai sus, rar am cetit o scriitura asa de reala si totusi hazlie, descriind mizeria de meserie in care ne zbatem. Ca nu ne ajungeau uzarii cretini, analistii dobitoci, mai ne distram si cu imbecilitati natur. Da’ fiti mai bland domne cu nea Billy a lu’ Poarta ( de Bil ghete, Gates zic) ca totusi, s avem rezon….netu’ e scula de scula fata de cacatul intens vehiculat si gadilat in cur denumit Java (javra pt cunoscatori). Da-o -n mortii ei de treaba, prefer oricand un cacat finut si cat de cat uzabil unei basini cu pretentii. Plus ca nu l suport pe Oracle. Si nici pe toti astia cu moda lor idioata de “web app” – cat de cretin sa fii sa faci aplicatii de uz intern web-based, cacandu te pe tine cinshpe ani sa ctitoresti un cacat de ecran si futand ca retardu’ la html si javascript ???? Ca sa ce ? Io nu pricep astea, raman la desktoape, nevermind, c am deviat, vroiam sa zic BRAVO si AFERIM la articol !

Leave a Reply

Optionally add an image (JPEG only)