background image

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

;

}

;

background image

Łą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

;

}

background image

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

);

    

}

}

background image

Łą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