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


Wyszukiwarka