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