2006 10 Łączenie kodu C z zarządzanym kodem NET [Inzynieria Oprogramowania]
Inżynieria oprogramowania Aączenie kodu C++ z zarządzanym kodem .NET Marek Więcek elem artykułu jest prezentacja sposobu łą- Listing 1. Klasa NativeDataProvider w C++ czenia zwykłego kodu w języku C++ (ang. Cnative code) z językami zgodnymi z plat- #include formą .NET, których głównym reprezentantem jest #include C#. Istnieje wiele sytuacji, w których takie połącze- using namespace std; nie okazuje się konieczne lub jest najlepszym roz- class NativeDataProvider { wiązaniem. public: Dostawca oprogramowania dla platformy Win- NativeDataProvider(const char* query_text); dows może odczuwać presję aby przenieść swój pro- dukt do środowiska .NET Framework, chociażby ze // Rozmiar danych. względów marketingowych, a jednocześnie niechęt- int getRowCount() const; ny jest przepisywaniu całego istniejącego i działają- int getColCount() const; cego kodu na nowo. W tej sytuacji możliwa jest stop- // Funkcja zwraca nazwy kolumn. niowa migracja dla istniejącego rdzenia aplikacji w vector getColNames() const; C++ powstają elementy interfejsu użytkownika, które najszybciej i najłatwiej jest oprogramować w C#. // Funkcja zwraca zadaną wartość Przejście, choćby częściowe na platformę .NET // w postaci tekstu. może też być wymuszone przez konieczność inte- string getValue(int row, int col) const; gracji z oprogramowaniem, które już pracuje na no- }; wej platformie. W sytuacji kiedy aplikacja tworzona jest od pod- staw w technologii .NET może się okazać, że po- mówi językiem zdefiniowanym w ramach standardu trzebne jest wykorzystanie w niej zewnętrznej biblio- CLI (ang. Common Language Infrastructure), z któ- teki napisanej w standardowym C++. Niektóre modu- rym zgodne są wszystkie języki .NET. Jego środo- ły aplikacji mogą także wymagać specyficznego za- wiskiem uruchomieniowym jest wirtualna maszyna rządzania pamięcią lub wykorzystania języka niskie- CLR (ang. Common Language Runtime). Obiekt kla- go poziomu ze względów wydajnościowych (np. sil- sy DataView potrafi komunikować się z obiektami, któ- nik programu obliczeniowego). re mówią językiem zgodnym z CLI i żyją we wspól- nym środowisku uruchomieniowym CLR. Obiekt kla- Tworzenie zestawu sy NativeDataProvider mówi natomiast językiem C++ mieszanego w C++/CLI i żyje poza światem CLR. Potrzebny jest obiekt-tłu- Dla potrzeb artykułu załóżmy, że mamy klasę Native- macz, który mówi zarówno standardowym językiem DataProvider, napisaną w standardowym C++, która C++ obiektu NativeDataProvider jak i językiem CLI dostarcza nam pewne dane. Naszym zadaniem jest obowiązującym w świecie .NET. Microsoft dostar- oprogramowanie w języku C# formatki, która prezen- cza kompilator tylko dla jednego języka programo- tuje dane dostarczone przez obiekt klasy NativeData- wania, w którym można tworzyć tego typu obiekty- Provider. Niech klasa formatki nosi nazwę DataView. tłumacze. Językiem tym jest C++/CLI, nazywany po- Obiekty klas DataView oraz NativeDataProvider czątkowo Managed C++ albo C++.NET. Jest to stan- nie mogą bezpośrednio ze sobą współpracować po- dardowy język C++ poszerzony o pewne dodatkowe nieważ nie rozumieją się należą do różnych świa- elementy, które umożliwiają wskazanie kompilatoro- tów i mówią różnymi językami. Obiekt klasy DataView wi klas i fragmentów kodu, dla których zostanie wy- generowany kod zarządzany (ang. managed code), uruchamiany w środowisku CLR. Dzięki temu moż- Autor zajmuje się tworzeniem oprogramowania zawo- liwe jest stworzenie modułu, który nazywany jest w dowo od 1994 roku, kiedy uzyskał tytuł magistra infor- terminologii .NET zestawem mieszanym (ang. mixed matyki na Uniwersytecie Jagiellońskim. Specjalizuje się assembly) ponieważ zawiera obydwa rodzaje kodu: w wykorzystaniu technologii obiektowych i komponento- zwykły i zarządzany. wych. Interesuje się systemami agentowymi oraz pro- Język C++/CLI umożliwia zatem tworzenie klas gramowaniem urządzeń mobilnych. Od wielu lat współ- pracuje z firmą Robobat, między innymi nad projektem zarządzanych, które potrafią komunikować się z kla- Robot Open Standard (http://www.robobat.com/n/ros/). sami napisanymi w innych językach zgodnych z CLI. Kontakt z autorem: m_w@robobat.pl Z drugiej strony w implementacji klasy zarządzanej w języku C++/CLI można korzystać ze zwykłych klas 48 www.sdjournal.org Software Developer s Journal 10/2006 Aączenie kodu C++ z zarządzanym kodem .NET cza, że parametr funkcji lub zmienna nie reprezentuje zwykłe- Listing 2. Klasa DataProvider w C++/CLI go obiektu C++, ale jest uchwytem (ang. handle) do obiektu using namespace System; przechowywanego na zarządzanej stercie (ang. managed he- using namespace System::Collections; ap). Klasa String została wykorzystana do zastąpienia zwy- class NativeDataProvider; kłego ciągu znaków reprezentowanego przez wskaznik const namespace Mixed { char* oraz do zastąpienia typu danych string pochodzącego public ref class DataProvider z biblioteki STL (ang. Standard Template Library) języka C++. { Funkcja getColNames w oryginalnej klasie NativeDataProvi- public: der zwraca obiekt typu vector, który pochodzi z biblio- DataProvider(String^ query_text); teki STL. Swego rodzaju odpowiednikiem STL w zarządza- ~DataProvider(); nym świecie .NET jest biblioteka klas .NET Framework (ang. int getRowCount(); .NET Framework Class Library). Wcześniej wykorzystaliśmy z int getColCount(); tej biblioteki klasę String, a teraz poszukamy w niej zamienni- IEnumerable^ getColNames(); ka dla klasy vector. Z punktu widzenia klienta klasy DataProvi- String^ getValue(int row, int col); der, który wywoła metodę getColNames, istotne jest jedynie to, protected: aby móc przejrzeć wszystkie zwrócone nazwy kolumn.W bi- !DataProvider(); bliotece klas .NET Framework zdefiniowano interfejs IEnume- private: rable, który służy właśnie do przeglądania elementów kolek- NativeDataProvider* _native; cji. Wystarczy zatem aby funkcja getColNames zwróciła obiekt, }; który implementuje ten interfejs. } Z deklaracji funkcji składowych klasy DataProvider, w stosun- ku do zwykłej klasy NativeDataProvider, usunięto słowo kluczowe const, ponieważ w standardzie CLI const może zostać użyte tyl- standardowego języka C++. W ten sposób na poziomie imple- ko w odniesieniu do pól klasy (ang. field) lub do zmiennych lokal- mentacji klasy zarządzanej możliwa jest integracja obydwu nych. Nie zapominajmy, że C++ oraz C++/CLI to jednak dwa róż- światów. W celu wykonania naszego zadania stworzymy ze- ne języki programowania. Chcemy, żeby funkcje składowe klasy staw mieszany przy użyciu języka C++/CLI. DataProvider były dostępne z poziomu języka C#, dlatego defini- cja tych funkcji musi spełniać wymagania standardu CLI. Definicja zwykłej klasy Klasa DataProvider posiada dodatkowo zwykły wskaznik Definicję wyjściowej klasy NativeDataProvider przedstawiono do obiektu klasy NativeDataProvider. Obiekt klasy NativeData- na Listingu 1. Klasa została napisana w standardowej skład- Provider nie może być wprost składnikiem klasy DataProvider, ni C++. Konstruktor klasy pobiera jako parametr wskaznik do przechowywanym przez wartość. Stworzenie obiektów oby- ciągu znaków, który może posłużyć do parametryzacji danych dwu klas wymaga bowiem wykorzystania dwu różnych me- udostępnianych przez obiekt tej klasy. Udostępniane dane są chanizmów zarządzania pamięcią. Obiekt klasy DataProvi- zorganizowane w strukturę tabelaryczną o bezpośrednim do- der zostanie utworzony na stercie zarządzanej podczas gdy stępie do każdego elementu pojedyncza wartość jest iden- obiekt klasy NativeDataProvider powstanie na zwykłej stercie. tyfikowana poprzez parę indeksów (wiersz, kolumna). Funkcja O zwolnienie pamięci po obiekcie klasy DataProvider zatrosz- getValue zwraca pojedynczą wartość w postaci tekstu gotowe- czy się mechanizm zbierania śmieci (ang. garbage collector) go do wyświetlenia na formatce. Wektor z nazwami poszcze- środowiska CLR, natomiast o zwolnienie pamięci po obiekcie gólnych kolumn danych udostępnia funkcja getColNames. W ten klasy NativeDataProvider musimy zatroszczyć się sami. sposób dane dostarczane przez klasę NativeDataProvider sa- me się opisują i zaprezentowanie ich na formatce nie będzie Implementacja klasy zarządzanej wymagać dodatkowej obróbki. Implementacja zarządzanej klasy DataProvider jest tym miej- scem, w którym następuje komunikacja i wymiana danych po- Definicja klasy zarządzanej między obydwoma światami: zwykłym i zarządzanym. Interfejs klasy DataProvider stanowi odwzorowanie interfejsu zwykłej klasy NativeDataProvider na język zgodny ze standar- Listing 3. Zwolnienie niezarządzanych zasobów dem CLI. Definicję klasy DataProvider zapisaną w składni ję- zyka C++/CLI przedstawiono na Listingu 2. Należy podkreślić, DataProvider::~DataProvider() że wykorzystano tutaj najnowszą wersję języka dostarczoną z { Visual Studio 2005, ponieważ w stosunku do wcześniejszych // wywołanie finalizatora wersji Managed C++ wprowadzono wiele zmian. this->!DataProvider(); Klasa DataProvider jest klasą zarządzaną o czym informu- } je nowe złożone słowo kluczowe ref class. Aby móc wykorzy- stać tę klasę na zewnątrz zestawu, musi ona być zdefiniowa- DataProvider::!DataProvider() na jako klasa publicznie dostępna, dlatego na początku defini- { cji umieszczono słowo public. delete _native; Konstruktor klasy DataProvider jako parametr przyjmuje native = 0; obiekt klasy String, którą zdefiniowano w bibliotece klas .NET } Framework w celu reprezentacji ciągu znaków. Znak ^ ozna- Software Developer s Journal 10/2006 www.sdjournal.org 49 Inżynieria oprogramowania W konstruktorze klasy DataProvider tworzony jest, przy Listing 5. Implementacja metody getColNames pomocy standardowego operatora new, obiekt klasy NativeDa- taProvider, który będzie udostępniał dane. Wydaje się natu- IEnumerable^ DataProvider::getColNames() ralne, że obiekt ten powinien zostać zniszczony w destrukto- { rze klasy DataProvider. Niestety pojawia się pewien problem ArrayList^ ret = gcnew ArrayList(); - nie mamy gwarancji, że destruktor klasy DataProvider zosta- if (_native) { nie kiedykolwiek wykonany i to niekoniecznie z powodu błędu vector names = _native->getColNames(); programisty. Związane jest to ze sposobem działania mecha- for (vector::iterator i = names.begin(); i != nizmu odzyskiwania śmieci (ang. garbage collection) w śro- names.end(); ++i) dowisku CLR. Jest to obszerne zagadnienie nadające się na ret->Add(textFromNative((*i).c_str())); osobny artykuł, które z konieczności potraktujemy skrótowo. } Załóżmy, że nasz niezarządzany obiekt klasy NativeDataPro- return ret; vider tworzy fizyczne połączenie z serwerem bazy danych. } Dlatego chcemy mieć pewność, że obiekt zostanie znisz- czony i tym samym zwolni połączenie z serwerem. Oczywi- ście obiekt klasy NativeDataProvider nie może zostać znisz- konwersja typów danych. Tak jest w przypadku ciągu zna- czony za wcześnie najlepiej aby został zniszczony dopie- ków, który w świecie .NET jest reprezentowany przez klasę ro wtedy kiedy zostanie zniszczony zarządzany obiekt klasy String, natomiast w standardowym C++ za pomocą wskaz- DataProvider. Jeżeli obiekt klasy DataProvider zostanie znisz- nika const char* lub klasy string z biblioteki STL. Na Listingu czony na skutek celowego działania programisty C# (np.: po- 4 przedstawiono implementacje funkcji konwertujących ciągi przez wywołanie metody Dispose), to zostanie wywołany de- znaków. Dla celów konwersji typów danych stworzona zosta- struktor klasy DataProvider. Jeżeli natomiast obiekt klasy Data- ła przestrzeń nazw (ang. namespace) Runtime::InteropServi- Provider zostanie zniszczony w wyniku działania mechanizmu ces. Z tej właśnie przestrzeni nazw pochodzi funkcja String- odśmiecania, to zostanie wywołany finalizator (ang. finalizer) ToHGlobalAnsi, statyczna metoda klasy Marshal, która przepro- klasy DataProvider. Na Listingu 3 przedstawiono implemen- wadza konwersję obiektu typu String na zwykły ciąg znaków. tację destruktora i finalizatora klasy DataProvider. Kod odpo- Funkcja ta zwraca wskaznik do ciągu znaków na zwykłej (nie- wiedzialny za zwolnienie obiektu klasy NativeDataProvider zo- zarządzanej) stercie, który może zostać przekazany wprost stał umieszczony wewnątrz finalizatora, który dodatkowo wo- do konstruktora klasy NativeDataProvider. Ostatecznie wskaz- łany jest przez destruktor. Dzięki takiej implementacji mamy nik ten musi zostać zwolniony przy pomocy funkcji FreeHGlo- pewność, że niezarządzany obiekt zostanie zniszczony w od- bal. Dla konwersji w przeciwną stronę wykorzystano metodę powiednim momencie i fizyczny zasób w postaci połączenia z PtrToStringAnsi. serwerem bazy danych zostanie zwolniony. Przyjrzyjmy się implementacji metody getColNames, któ- Aby wymiana danych pomiędzy światem zarządzanym i rą przedstawiono na Listingu 5. Metoda ta musi zwrócić do- niezarządzanym była możliwa, konieczna może okazać się wolny obiekt, który implementuje interfejs IEnumerable zde- Listing 4. Konwersja typów danych reprezentujących Listing 6. Wykorzystanie klasy DataProvider z poziomu tekst języka C# // Konwersja zarządzanego typu danych String do // wczytaj dowolne dane z obiektu dostawcy // zwykłego typu string using (DataProvider dp = new DataProvider("")) { string textToNative(String^ txt) // przygotuj kolumny tabeli { grid.Columns.Clear(); using namespace Runtime::InteropServices; foreach (String name in dp.getColNames()) { IntPtr ip = Marshal::StringToHGlobalAnsi(txt); DataGridViewColumn col = new DataGridViewColumn(new const char* str = static_castchar*>(ip.ToPointer()); col.Name = name; string ret(str); grid.Columns.Add(col); Marshal::FreeHGlobal(ip); } return ret; // wypełnij tabelę danymi } for (int i = 0; i < dp.getRowCount(); ++i) { // Konwersja zwykłego ciągu znaków do zarządzanego DataGridViewRow row = new DataGridViewRow(); // typu danych String row.CreateCells(grid); String^ textFromNative(const char* txt) for (int j = 0; j < dp.getColCount(); ++j) { { using namespace Runtime::InteropServices; row.Cells[j].Value = dp.getValue(i, j); return Marshal::PtrToStringAnsi(static_ } cast(const_ grid.Rows.Add(row); cast(txt))); } } } 50 www.sdjournal.org Software Developer s Journal 10/2006 Aączenie kodu C++ z zarządzanym kodem .NET funkcja getColNames zwraca standardowy interfejs IEnumerable W Sieci możliwe jest bezpośrednie użycie jej wewnątrz bardzo wygod- nej pętli foreach języka C#. l http://www.microsoft.com/net/ miejsce, w którym zebrano peł- Dodatkowo obiekt klasy DataProvider został użyty we- ną informację na temat .NET; wnątrz instrukcji using. Dzięki temu w miejscu, w którym koń- l http://msdn.microsoft.com podstawowe zródło wiedzy pro- czy się zakres instrukcji using zostanie automatycznie wywo- gramistów korzystających z narzędzi Microsoft; łany destruktor klasy DataProvider. Tym samym połączenie z l http://www.gotdotnet.com witryna społeczności programi- serwerem bazy danych zostanie zamknięte już teraz a nie do- stów dla platformy .NET Framework piero podczas odzyskiwania zasobów przez mechanizm od- śmiecania. finiowany w bibliotece klas .NET Framework. Każda kla- W ten sposób, korzystając ze składni języka C#, poprzez sa z tej biblioteki, która definiuje kolekcję implementuje in- obiekt klasy zarządzanej DataProvider zdefiniowanej w języku terfejs IEnumerable. W tym przypadku wybrano klasę Array- C++/CLI pobieramy dane z obiektu niezarządzanej klasy Na- List. Obiekt klasy ArrayList jest tworzony na stercie zarzą- tiveDataProvider, napisanej w standardowym C++. dzanej przy pomocy słowa kluczowego gcnew. Nazwy ko- lumn pobrane z macierzystego obiektu zostają przekonwer- Podsumowanie towane do obiektów typu String, a następnie dodane do wy- Wykorzystanie języka C++/CLI dla łączenia kodu zarządzane- nikowej listy nazw. go ze zwykłym kodem C++ określane jest jako współdziałanie C++ (ang. C++ Interop). Jest to rozwiązanie najbardziej wydaj- Wykorzystanie ne z możliwych. Należy jednak zauważyć, że każde przekro- zestawu mieszanego w C# czenie granicy pomiędzy kodem wykonywanym w środowisku Klasę DataProvider możemy teraz wykorzystać wprost w pro- CLR i kodem niezarządzanym powoduje wykonanie dodatko- gramie napisanym w C# lub dowolnym innym języku .NET wych operacji przejścia (ang. transition thunk), których koszt wszystkie one są zgodne ze standardem CLI. Wystarczy do nie jest zerowy. Dodatkowo, tak jak w przedstawionym przy- projektu w C# dodać referencję do naszego zestawu mie- kładzie, może być wymagana konwersja typów danych. Na- szanego aby uzyskać dostęp do definicji klasy DataProvider. leży o tym pamiętać projektując komunikację pomiędzy oby- Na Listingu 6 przedstawiono funkcję, która wypełnia kontrol- dwoma światami w ten sposób aby zminimalizować liczbę kę DataGridView danymi pobranymi z obiektu klasy DataProvi- przejść (wywołań funkcji) pomiędzy nimi. Zachęcam do esk- der. Dzięki zgodności języków C++/CLI oraz C# ze standar- perymentów z przykładowym kodem, który w postaci projek- dem CLI możemy teraz używać obiektu klasy zarządzanej Da- tu Visual Studio 2005 został zamieszczony na płytce dołączo- taProvider tak jakby została napisana w języku C#. Ponieważ nej do magazynu. n R E K L A M A