2006 10 Łączenie kodu C z zarządzanym kodem NET [Inzynieria Oprogramowania]

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-

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


Wyszukiwarka

Podobne podstrony:
2006 05 Antywzorce w zarządzaniu projektami informatycznymi [Inzynieria Oprogramowania]
Przykład diagramu sekwencji, Studia-WSTI (vizja.net), Inżynieria oprogramowania
2006 10 Przegląd modeli cyklu życia oprogramowania [Inzynieria Oprogramowania]
2006 08 Zarządzanie pamięcią w systemach operacyjnych [Inzynieria Oprogramowania]
2006 09 Data Protection API i NET Framework 2 0 [Inzynieria Oprogramowania]
10 Laczenie, podzial, przekszta lcanie spolek FOLIE
2006 10 trendy
IPN 08 2006 10 06
2006 06 Wstęp do Scrum [Inzynieria Oprogramowania]
9 10 pierwotne kzn 2012 13 net Nieznany (2)
2006.10.30 psychometria cw, Psychologia, Psychometria
ZARZĄDZANIE PROJEKTAMI - WYKŁADY, Inżynieria Produkcji, Zarządzanie Projektem
2006.10.16 psychometria ćw, Psychologia, Psychometria
inf wstep NET, Inżynieria Środowiska [PW], sem 4, Infa, woiągi, Płyta;Inf i Prog

więcej podobnych podstron