Utilizarea com din dll este invizibilă pentru client

Utilizarea com din dll este invizibilă pentru client

Într-o zi, când scriam un DLL, am avut o sarcină elementară, la prima vedere: de a crea și de a folosi un obiect COM. Dar sa dovedit că acest lucru nu este atât de ușor. Principalele probleme au fost că clientul meu DLL nu cunoștea nimic despre COM, așa că nu am intenționat să îl inițializez, dar am avut o grămadă de fire, fiecare dintre ele numit periodic funcții DLL. Mi se pare, în urma unor experimente de gândire (și forum de consultare RSDN COM / DCOM / COM +) de ansamblu și a fost găsit destul de soluție elegantă, și care este descrisă în acest articol.

Trebuie să scrieți un DLL care nu prezintă cerințe legate de COM la client. Această nevoie apare dacă există deja mai mulți clienți diferiți folosind un DLL cu o interfață similară și trebuie să îl înlocuiți. Sau dacă scrieți un plug-in pentru orice.

Din anumite motive, în timpul implementării, chiar vreau să folosesc COM. În cazul meu, motivul a fost posibilitatea utilizării unui server transparent (pentru clientul meu DLL) într-un alt proces (bine, nu am vrut să folosesc metode tradiționale de interacțiune între procese). A trebuit să inventez. În cazul dvs., aceasta poate fi, de exemplu, prezența componentelor COM care implementează funcționalitatea necesară.

  • Unele fire de client nu știu nimic despre COM. Și DLL să folosească doriți. Și CoInitialize [Ex] înainte de a apela DLL nu va fi apelat.
  • Unele fire de client știu ceva despre COM. Acestea apartin apartamentelor diferite, dar, in ciuda acestui fapt, se vor intoarce (sau simultan) la DLL.

Specificarea sarcinii

Vom rezolva o versiune simplificată a problemei mele. Acesta este un caz simplu, dar evident. Sper că sarcinile tale pot fi reduse la ceva similar sau cel puțin să folosești aceeași abordare.

Trebuie să creați un DLL care exportă următoarele funcții:

Dacă nu există funcții precum Init and Cleanup în interfața DLL-ului, le puteți suna implicit. Asta este, dacă SomeFunc detectează că Init nu a fost încă chemat, o numește ea însăși. Curățirea poate fi apelată de la DllMain. Dacă utilizați implementarea Init descrisă mai jos, apelați Init din DllMain va avea ca rezultat o blocare reciprocă a firelor.

În plus, există o componentă COM CoolServer care implementează interfața ICoolServer, care conține SomeFunc. Dacă COM a fost inițializată peste tot și nu ar exista probleme cu apartamentele, ar fi necesar să se implementeze DLL aproximativ în felul următor:

Adică trebuie să utilizați doar un singur pointer la interfață, obținut atunci când biblioteca este inițializată și eliberată în timpul curățării. Dacă puteți crea un obiect în SomeFunc și îl puteți distruge acolo, atunci funcțiile Init și Cleanup pot fi eliminate, problemele cu apartamentele, de asemenea, dispar. Cazul în care o instanță a unui obiect COM trebuie să fie stocată între apeluri este mai interesantă.

Deci, trebuie să:

Dezincriminarea COM este importantă! Un scenariu posibil: clientul invocă o funcție DLL, acesta inițializează COM, nu curăță după ei înșiși, atunci clientul încearcă să se auto-inițializa COM și se rupe, pentru că el vrea să-l facă un pic greșit (preferă STA, nu MTA, sau vice-versa, iar acești clienți sunt imprevizibil.).

Pentru a îndeplini primele două cerințe, vă propun să creați un fir suplimentar, să inițializați COM în el și să creați un obiect. După aceasta, firul va aștepta curățarea, timp în care eliberează pointerul la interfață, de-inițializează COM și se termină.

Un thread suplimentar este necesar pentru a asigura existența obiectelor create în acesta între apelurile funcționale. Prin urmare, în cazul în care între apeluri funcții DLL nu aveți nevoie pentru a stoca indicii la interfețe, ar trebui să nu se joace cu fluxul suplimentar, Init-lea-lea și Cleanup este mai ușor de a crea și șterge obiecte în acele funcții pe care le utilizează.

În pseudocod se arată astfel:

Cateva lucruri pe care sa le luati in considerare pentru ca aceasta sa functioneze:

Când așteptați finalizarea inițializării în funcția Init, este necesar să porniți ciclul de recuperare a mesajelor. În caz contrar, clientul care a inițializat din greșeală COM și a intrat în STA înainte de a apela Init va fi blocat în InitCleanupThread atunci când încearcă să creeze un obiect.

Când creați un obiect în InitCleanupThread, este foarte de dorit ca modelul de streaming al obiectului să vă permită să îl creați în STA principal. În caz contrar, în cazul în care principalul STA există, un obiect este creat în ea, și, în consecință, distrugerea principal STA (apel CoUninitialize corespunzătoare flux) de obiect, de asemenea, va dispărea. O referință la pointerul obosit la interfața obiectului va reveni RPC_E_SERVER_DIED_DNE.

Fișierul InitCleanupThread trebuie să fie în MTA. În caz contrar, el ar putea fi accidental șeful STA, va fi creat un client de obiecte, și atunci când toate obiectele vor muri după fluxul (și metodele lor vor reveni RPC_E_SERVER_DIED_DNE), clientul va fi foarte surprins. Chiar, aș zice, încremenit. El este de asemenea conștient de existența fluxului nu este ghicit.

Dacă, din anumite motive, firul InitCleanupThread este încă inclus în STA, ar trebui să aștepte curățarea din bucla de recuperare a mesajelor.

Pentru a îndeplini a treia cerință, puteți face acest lucru:

1. Încercați să inițializați COM.

dacă CoInitialize [Ex] a returnat eroarea RPC_E_CHANGED_MODE, atunci COM a fost deja inițializată, doar un pic greșit.

dacă CoInitialize [Ex] a returnat S_OK, atunci COM a fost inițializată cu succes.

dacă CoInitialize [Ex] a returnat S_FALSE, atunci COM a fost deja inițializată și cu aceiași parametri.

dacă CoInitialize [Ex] a returnat altceva, sa întâmplat ceva teribil.

2. Obțineți un pointer valabil la interfața dorită pentru utilizare în acest apartament. Metoda de obținere a pointerului poate depinde de rezultatul apelului către CoInitialize [Ex].

3. Apelați metoda SomeFunc.

4. Dacă CoInitialize [Ex] returnează S_OK sau S_FALSE, de-inițializați COM.

1. Încercați să obțineți un pointer valabil la interfața dorită pentru utilizare în acest apartament.

  • dacă rezultatul este o eroare CO_E_NOTINITIALIZED, mergeți la pasul 2;
  • dacă totul este ok la punctul 4;
  • dacă altceva, ceva groaznic sa întâmplat.

2. Inițializați COM.

3. Obțineți un pointer valabil la interfața dorită pentru utilizare în acest apartament.

4. Apelați metoda SomeFunc.

5. Dacă a fost efectuat pasul 2, inițializați COM.

Dacă fluxul InitCleanupTheard intră în MTA, atunci indiferent dacă COM a fost inițializat în firul curent și CoUnmarshalInterface IGlobalInterfaceTable :: funcții GetInterfaceFromGlobal întoarce S_OK mine. În acest caz, apelul la metoda SomeFunc funcționează. Dar nu voi da nici o garanție că va funcționa întotdeauna. Pentru ceva, probabil, în același timp, este necesar să inițializați COM?

În cazul în care fluxul de InitCleanupTheard intră STA (pentru că, desigur, nu pot face asta, dar mă întrebam.), Apoi IGlobalInterfaceTable :: GetInterfaceFromGlobal întoarce E_UNEXPECTED, atâta timp cât cel puțin o dată să nu execute o secvență de CoInitialize [Ex] + IGlobalInterfaceTable :: GetInterfaceFromGlobal. După aceea, ea, așa cum era de așteptat, se va întoarce CO_E_NOTINITIALIZED.

Nu se recomandă utilizarea practică.

Puteți obține "un pointer la interfața dorită pentru utilizare în acest apartament" în trei moduri:

CoMarshalInterface este mai bine să sunați cu pavilionul MSHLFLAGS_TABLESTRONG. Deoarece cu MSHLFLAGS_TABLEWEAK uneori nu funcționează. Sincer, până mi-am dat seama de ce.

Înainte de a apela CoUnmarshalInterface, trebuie să derulați IStream la început. Aceasta este ceva de genul:

pStream-> Căutați (li, STREAM_SEEK_SET, NULL);

În caz contrar, nu va funcționa. Și, apropo, nu te aștepta ca Seek să se întoarcă vreodată CO_E_NOTINITIALIZED, înăuntru - o operație pur mecanică. Deși pentru a verifica, desigur, nu dăunătoare.

Această din urmă metodă este aplicabilă numai în cazul în care firul care a creat obiectul (InitCleanupThread), iar fluxul de care dorește să folosească această facilitate, fac parte din apartament, care este, ambele fiind incluse în MTA; Primele două metode se aplică în orice caz.

De exemplu, am scris două servere (unul - exe, altul - DLL), două DLL-uri (câte unul câte unul pentru fiecare server) și un client cu un set de teste.

Ambele servere implementează următoarea interfață:

SomeFunction returnează pur și simplu S_OK, WorkFunction afișează șirul care îi este transmis (serverul din DLL este prin printf, serverul din exe este prin MessageBox). Prima este folosită pentru a compara viteza diferitelor metode de apelare, cea de-a doua este folosită pentru a testa performanța lor de bază.

Ca de obicei, serverele trebuie să fie înregistrate. În același timp, ambii încearcă să-și înregistreze bibliotecile de tip. Ambele servere presupun că biblioteca se numește "iface.tlb" și se află în același director ca fișierul executabil al serverului.

Clientul, în funcție de valoarea de pe linia de comandă (numărul de la 0 la 6, absența argumentelor este echivalent cu "0"), efectuează unul dintre teste. Testul "0" verifică funcționalitatea, restul vă permite să evaluați performanța. Rezultatele acestora sunt prezentate în secțiunea "Statistici".

Ele diferă între ele numai prin numele funcțiilor exportate și GUID-urile obiectelor create. Construcțiile teoretice din secțiunea "Soluție" sunt implementate după cum urmează:

  • Fluxul InitCleanupThread este inclus în MTA.
  • Fișierul InitCleanupThread îi spune lui Init să finalizeze inițializarea utilizând un eveniment care acceptă ca parametru.
  • Curățarea indică firul InitCleanupThread pentru a începe curățarea cu un eveniment global.
  • Curățarea nu așteaptă curățarea pentru a termina. Lenea.
  • InitCleanupThread înregistrează interfața în GIT.
  • InitCleanupThread generează intercalări de interfață în IStream global. CoMarshalInterface este apelat cu steagul MSHLFLAGS_TABLESTRONG.
  • Când încercați să inițializați COM, toate firele încearcă să intre în MTA. Prin urmare, dacă reușesc, nu este necesară înălțarea.
  • Dll exportă următoarele funcții ( «XX» - sau «În» sau «out»): xx_Init, xx_Cleanup, xx_One, xx_Two, xx_Three, xx_Four, xx_One_Work, xx_Two_Work, xx_Three_Work, xx_Four_Work. «Unul», «Doi», «Trei», «Patru» - cele patru metode de obținere a unui pointer la obiect. Funcțiile de lucru numesc WorkFunction, funcțiile obișnuite sunt SomeFunction.

Parametrul init este setat la true dacă CoInitializeEx a fost apelat și a lucrat cu succes și, prin urmare, înainte de a reveni din DLL, este necesar să sunați CoUninitialize.

statistică

Cel mai probabil, după ce ați citit că pentru fiecare apel trebuie să plătiți prin inițierea / dezinitializarea COM sau ceva de genul asta, ați pus întrebarea: cât costă? De asemenea, am fost interesat, am pus câteva experimente.

Toate experimentele au fost efectuate după cum urmează:

COM nu a fost inițializată

Mă voi ajuta puțin :) Datele din coloanele "COINIT_APARTMENTTHREADED" și "COINIT_MULTITHREADED" au fost obținute aproximativ în felul următor:

Aceasta înseamnă că COM a fost inițializată înainte de a apela xx_Init. Acest lucru explică rezultatele ciudate din coloana "COINIT_APARTMENTTHREADED" a primului tabel. În acest caz, firul principal inițial a inițializat COM, motiv pentru care a intrat în STA principal, iar obiectul CoolServer a fost creat în acesta. Prin urmare, unele apeluri către SomeFunction au trecut direct.

P.S. Chiar și Microsoft face acest lucru ...

Te-ai gândit vreodată la modul în care este implementată funcția ShellExecute [Ex]? Pe de o parte, nu necesită pre-inițializare a COM și, pe de altă parte, o folosește în mod evident ... Nu m-am gândit la asta. Dar se pare că folosește o abordare similară.

Funcția SHCoInitialize arată astfel:

Adică dacă nu a reușit să inițializeze COM cu parametrul COINIT_APARTMENTTHREADED, încearcă să-l inițializeze cu COINIT_MULTITHREADED. Dacă nu se întâmplă nimic neașteptat, după efectuarea acestei funcții, puteți:

  • asigurați-vă că este inițializată COM
  • îndrăzneț sunați CoUninitialize, deoarece clientul nu va face rău

ShellExecuteExW face acest lucru.

mulțumesc

În timp ce scriam codul, care a servit ca bază pentru acest text și pentru exemplu, am fost foarte ajutat de Vi2 (Victor Sharakhov). Mulțumesc foarte mult. În al doilea rând, mulțumesc lui Pavel Bludov, care mi-a atras atenția asupra funcției ShellExecute [Ex].

Articole similare