Úskalí míchání nativního a řízeného kódu

5. 12. 2011 17:01 zboj

Když píšete v nějakém jazyce s bajtkódem a GC (např. pro .NET nebo v Javě), narazíte občas na potřebu použít nativní kód (například kvůli výkonu nebo máte nějaký z dřívějška a nechce se vám ho přepisovat). Volání nativního kódu z řízeného a naopak s sebou přináší pár problémů, na které je třeba dát pozor.

Paměť

Voláme-li nativní kód z řízeného, obcházíme samozřejmě GC a musíme se o správu paměti postarat sami. V C++ sice (dovolím si parafrázovat jeden známý výrok) chytrý programátor new nepoužívá a blbci je to jedno, jenže v tomto případě se mu nevyhneme. Řízená třída (na řízené haldě) může narozdíl od nativních tříd obsahovat pouze ukazatel na instanci nativní třídy, tedy např. Test* inst, ale ne Test inst. Musíme tedy použít new Test s tím, že instanci zrušíme při likvidaci rodičovského objektu. Jenže o ten se stará GC, takže nad životním cyklem objektu nemáme kontrolu. Abychom zamezili úniku paměti, dáme nezbytné delete inst do finalizéru. Jenže nikdo nám negarantuje, že ten se vůbec kdy zavolá. Jediný stoprocentně funkční způsob je uvolnit objekt ve finalizéru a zároveň v destruktoru (ten u řízené třídy odpovídá rozhraní IDisposable). Musíme tedy mít:

~Trida() { delete inst; } !Trida() { delete inst; }

Běhové prostředí se postará, že se zavolá právě jedna z těchto metod (tj. nikdo se delete inst neprovede dvakrát). Lepší je samozřejmě použít deterministickou destrukci (v C++ zásobníkovou sémantiku, v C# blok uvozený using), jinak bychom naprosto zbytečně okupovali paměť na neřízené haldě až do spuštění GC.

Chceme-li mít instanci řízené třídy v nativním objektu, narazíme na více potenciálních problémů. Přímo to není vůbec možné, ale máme k dispozici pomocné nativní třídy gcroot a auto_gcroot, do kterých můžeme řízený objekt „zabalit“. Použití gcroot je důležité proto, aby GC věděl, že někde existuje reference na řízený objekt a nezrušil jej předčasně. Neméně důležitým důvodem je, že GC objekty v paměti přesouvá, pokud bychom měli jen holý okazatel, po jednom cyklu GC by se mohlo stát, že by ukazoval na naprosto náhodná data (pořád na stejnou adresu, ale objekt by už byl jinde).

Problém s přesunem řízených objektů v rámci haldy lze řešit také „přišpendlením“ (z anglického pin). K dispozici máme pomocnou třídu pin_ptr, které dáme ukazatel a tím zabráníme přesunu objektu, pokud se během používání objektu spustí GC. Například pin_ptr<int> p = &a[0] přišpendlí celé řízené pole, dokud instance pin_ptr nezanikne. Je samozřejmé, že bychom tuto pomocnou třídu měli používat s rozumem, protože může způsobit fragmentaci řízené haldy (mnohdy je lepší použít podobnou třídu interior_ptr, která nezabraňuje přesunu objektu, ale automaticky zabalený ukazatel aktualizuje na novou adresu v paměti).

Použití gcroot s sebou nese jistou režii, máme-li tedy například gcroot<T^> wrapper, je lepší místo volání wrapper->Metoda() (operátor → je přetížený podobně jako u chytrých ukazatelů) použít přetížený operátor přetypování (pokud chceme volat více metod nebo jednu metodu několikrát):

T^ obj = wrapper; obj->Metoda();

Pro úplnost dodávám, že výše uvedené není jen záležitostí Microsoftu, například Apple měl ve svém „Java bridge“ blahé paměti (byl v OS X až do verze 10.5) něco velmi podobného – bylo možné míchat kód v ObjC a v Javě, přičemž problémy okolo GC se řešily velmi podobně. Javu už v Applu hodili přes palubu, ale stále je možné v rámci jednoho souboru mísit ObjC s C++ (ObjC sice není řízený jazyk ve smyslu .NETu, ale stále je možnost používat GC apod.), kde jsou některé problémy s kompatibilitou velmi podobné.

NB: V novém WinRT vše funguje ještě úplně jinak, z řízeného kódu lze používat jen nativní komponenty (ref class v C++/CX) a wrapper z .NETu manipuluje s čítačem referencí (destruktor či finalizér objekt uvolní automaticky). Při použití .NET komponenty z C++ naopak zanikne reference na řízený objekt, jakmile se u nativního wrapperu sníži čítač referencí na nulu (a řízený objekt se tak nechá na pospas GC).

Výjimky

Ošetření výjimek není zdaleka tak ošemetné. Pokud nativní kód vyhodí výjimku, vrstva mezi nativním a řízeným kódem (interop) výjimku odchytí a převede na řízenou, typicky SEHException (SEH=structured exception handling). Lepší je nativní výjimku odchytit v kódu a vyhodit (angl. rethrow) řízenou, například takto:

catch (std::exception& ex) { throw gcnew Exception(gcnew String(ex.what())); }

V případě problémů tak dostaneme přesnější informace, co se vlastně stalo.

CLR umožňuje vyhodit jako výjimku libovolný objekt (v C# nám překladač neumožní vyhodit cokoliv, co nedědí ze System.Exception, to je ale umělé omezení). Při použití neznámé knihovny je proto vždy lepší (v C#) odchytávat všechny objekty (nespecifikujeme Exception v bloku catch).

Pro porovnání, v ObjC++ nejsou výjimky z ObjC a C++ kompatibilní, musíme tedy jednu výjimku zachytit a vyhodit druhou. V ObjC se používají syntakticky odlišná klíčová slova – @try/@catch/@finally.

Sdílet