Програмиране за платформата. Net взаимодействие между managed и unmanaged код Стоян Йорданов



Дата25.07.2016
Размер133.86 Kb.
Програмиране за платформата .NET
Взаимодействие между managed и unmanaged код
Стоян Йорданов


  • Твърде много време и усилия са хвърлени за разработването на вече съществуващия unmanaged код, за да може да бъде захвърлен той с лека ръка. Това са DLL библиотеки, COM компоненти и т.н., които в някакъв момент може да се наложи да използваме от managed код, или обратно – да се наложи те да използват нашия managed код.

  • Чрез механизмите, които се предлагат от .NET Framework, можем да използваме COM компоненти, DLL библиотеки, API функции и т.н. Съществуват два механизма, които позволяват това – platform invoke и COM interoperability services.


Platform Invoke


  • Platform invoke е механизъм, който позволява managed код да извиква функции от unmanaged код, намиращи се в DLL файлове (независимо дали са DLL-ки, съдържащи функции от Win32 API или пък нещо друго).

  • Когато извикаме unmanaged функция, platform invoke най-напред намира и зарежда DLL файла, който съдържа функцията. След това извиква функцията, предавайки и необходимите параметри, маршализирайки ги както трябва (маршализацията (marshalling) представлява пакетиране и предаване на параметри и данни през границите на процес или application domain).

  • Platform invoke взима необходимата му информация (като името на DLL файла и информация за функцията) от специален атрибут, с който ние му я указваме.


Извикване на функции, намиращи се в unmanaged код

  • Първата стъпка при използването на platform invoke е да декларираме функцията, която ще извикваме. Декларираме я чрез ключовите думи extern (указваща, че функцията не е дефинирана в това асембли) и static (т.е., че функцията не се нуждае от this указател към текущия клас).

  • След като декларираме функцията, трябва да използваме върху нея атрибута System.Runtime.InteropServices.DllImport, чрез който предаваме необходимите параметри. Задължителен параметър на атрибута е името на DLL файла, от който трябва да бъде взета функцията. DllImport има и други параметри, които можем да използваме по желание, ако искаме да модифицираме подразбиращото се поведение. Най-важните от тях са:

    • EntryPoint – чрез този параметър можем да укажем името на функцията, в случай, че се различава от това, което сме декларирали.

    • CallingConvention – указва каква конвенция да бъде използвана при извикване на функцията и предаване на параметрите. Възможни стойности са CallingConvention.Cdecl, StdCall, ThisCall и FastCall (която не се поддържа в .NET Framework 1.0). Подразбиращата се стойност е CallingConvention.Winapi, която използва StdCall при Windows и Cdecl при Windows CE.

    • CharSet – можем да го използваме, за да укажем на platform invoke как да маршализира символните низове. Възможни стойности са CharSet.Ansi (използвано по подразбиране) – за използване на текуща кодова таблица, CharSet.Unicode – за предаване на 2-байтови unicode низове, и CharSet.Auto, който, в зависимост от операционната система, използва Unicode (за Windows NT / 2000) или Ansi.

    • ExactSpelling – булев параметър, който указва, че името на функцията не бива да бъде променяно в зависимост от използвания CharSet. По подразбиране, ExactSpelling е false за CharSet.Auto, иначе е true.

  • Например, нека извикаме Win32 API функцията GetVersion, намираща се в kernel32.dll. Тя има следната сигнатура:
    DWORD GetVersion(void);
    Функцията връща версията на Windows, записана по определен начин във връщаната стойност. За да я използваме, трябва да я декларираме, и да обозначим с DllImport атрибут, че искаме да я ползваме чрез platform invoke:
    using System.Runtime.InteropServices;

    [DllImport(“Kernel32.dll”)]
    extern static uint GetVersion();
    След това вече можем да си я ползваме като обикновена managed функция, като оставяме грижата по изпълнението и на platform invoke:
    uint winVersion = GetVersion();

  • При декларацията на platform invoke функции има два подхода. Единият е да ги декларираме, както видяхме по-горе, във всички асемблита / класове / etc., където възнамеряваме да ги използваме. Другият подход е да си декларираме един клас, в който да изнесем всички такива функции (например всички API функции, които искаме да ползваме), след което да си ползваме този клас. Предимството е, че platform invoke декларациите са концентрирани в един клас, и са по-лесни за поддръжка.

Маршализация

  • Маршализацията е процес, при който данните се пакетират и предават в подходящ вид през границите на процеси или application domain-и.

  • Простите типове се маршализират лесно към съответните им unmanaged еквиваленти. Ето какви са C# съответствията на типовете, използвани в unmanaged код:

    • BOOLEAN int

    • BYTE byte

    • CHAR char

    • DOUBLE double

    • FLOAT float

    • HANDLE int

    • INT int

    • LONG int

    • LPSTR String, или StringBuilder, ако се очаква писане в него.

    • SHORT short

    • UINT uint

    • ULONG ulong

    • WORD ushort

  • Повечето стрингови типове могат да се представят чрез string, ако не се очаква извикваната функция да пише в стринга, или чрез StringBuilder, ако това се налага.

  • Ако се налага да предаваме указател към някакъв тип, трябва да използваме ключовата дума ref в езика C#.

  • На unmanaged типа LONG съответства C# тип int, защото LONG е 32-битово число, каквото е и int в C# (int е еквивалентно на System.Int32).

Ограничения на Platform Invoke

  • Platform Invoke може да се използва само за извикване на глобални функции, експортирани от DLL файл.

  • Не се поддържа маршализация на всички типове данни.

  • Поддържа се предаване на callback функции, но в .NET Framework 1.0 има ограничение техните параметри да са само от тип int.


Използване на COM Oбекти от Managed Код
Runtime Callable Wrappers

  • За да използване на COM обект, .NET runtime използва т.нар. Runtime Callable Wrapper – обект, съдържащ необходимата информация, за да може да се извика COM компонентът, грижи се за правилния reference counting и т.н.

  • Освен това runtime callable wrapper-ът се грижи за правилната маршализация на данните към и от COM обекта, като например да конвертира System.String към BSTR и обратно.

  • За .NET-ски класове, runtime callable wrapper-ът изглежда като обикновен .NET клас, който предлага същите интерфейси и методи като COM компонента, с изключение на интерфейсите IUnknown (който се използва в COM за reference counting) и IDispatch (който се използва за т.нар. “dynamic bounding”, т.е. за извличане на информация за предлаганите интерфейси и методи по време на изпълнение, горе-долу така, както това става при .NET Remoting). Интерфейсите IUnknown и IDispatch не се предлагат от runtime callable wrapper-а на останалите .NET класове.

  • Runtime callable wrapper-ите освобождават COM обектите, когато самите те трябва да бъдат събрани от Garbage Collector-а (т.е. COM обектите се унищожават, когато трябва да бъде унищожен runtime callable wrapper-ът).

Създаване на Runtime Callable Wrapper-и

  • Можем да създаваме runtime callable wrapper-и по три начина:

    • Чрез използване на Type Library Importer-а (tlbimp.exe), който е част от .NET Framework SDK.

    • Чрез добавяне към проекта ни на reference към COM обекта във Visual Studio, което е еквивалентно на използването на tlbimp.exe, но е по-лесно, в случай, че използваме Visual Studio .NET.

    • Можем да си създадем собствен (custom) wrapper, което обаче е твърде сложно, за да се занимаем тук с него.

  • Type Library Importer (tlbimp.exe) ни позволява да създаваме runtime callable wrapper-и директно от COM type libraries. За целта трябва да му подадем пътя към type library-то, и той ни генерира асембли (DLL файл), съдържащ runtime callable wrapper-а и готов за ползване от нашия .NET код:
    tlbimp.exe MyCOMLib.tlb

  • Чрез опцията /out: можем да указваме на tlbimp.exe името на асемблито, в случай, че искаме то да е различно от името на type library-то. Ако например type library-то ни се намира в MyCOMLib.dll, ние може да искаме cuntime callable wrapper-ът да се казва по друг начин. Можем да използваме опцията /out така:
    tlbimp.exe MyCOMLib.dll /out: MyCOMLib_RCW.dll

Threading модели

  • За разлика от .NET, COM компонентите използват т.нар. “апартаменти” (apartments), за да осигурят правилна синхронизация и многонишкова работа.

  • При работа с COM обекти, .NET Framework създава апартамент, който да използва при извикването на COM обекти. Това може да бъде или еднонишков апартамент (single-threaded apartment – STA), който съдържа само една нишка, или многонишков апартамент (multi-threaded apartment – MTA), който може да съдържа и повече от една нишка.

  • Ако апартаментите, използвани от COM компонента и .NET Framework са съвместими, извикващият thread може директно да извиква COM обекта. Ако обаче не са, COM създава съвместим с COM обекта апартамент, и маршализира извикванията към него чрез прокси.

  • В .NET можем да контролираме какъв apartment модел използва даден thread чрез неговото свойство ApartmentState, което е от изброимия тип System.Threading.ApartmentState с възможни стойности STA, MTA и Unknown. То обаче може да се установява само веднъж на thread.

  • Тъй като трябва да установим това свойство преди да се инициализира COM библиотеката за съответния thread, хубаво е да го направим възможно най-рано.

  • За да установим threading модела на главната нишка, можем да маркираме Main метода чрез атрибутите [STAThread] или [MTAThread]. Visual Studio дори автоматично генерира [STAThread] атрибут над Main метода.

Преобразуване на сигнатурата на методите; Обработка на грешки

  • Стандартната конвенция при COM е методите да връщат код за грешка от тип HRESULT. При .NET обаче не се използва HRESULT, ами вместо това, за да сигнализират за грешки, методите предизвикват изключения.

  • За да се постигне баланс между двата подхода, при създаването на runtime callable wrapper сигнатурата на COM метода се преобразува до различна сигнатура за .NET метода. Връщаната HRESULT стойност се скрива, и runtime callable wrapper-ът генерира изключение, ако методът върне HRESULT с код за грешка. Ако COM методът има изходен параметър, маркиран като [out, retval] в type library-то, в .NET сигнатурата той представлява връщаната от метода стойност. Например методът с COM сигнатура
    HRESULT GetNumberOfSpaces([in] BSTR str, [out, retval] short* spaceCount);
    получава следната .NET сигнатура:
    short GetNumberOfSpaces(string str);

  • Проблемът при този подход е, че ако връщаната от COM метода HRESULT стойност е различна от грешка, няма начин да разберем каква точно е тази стойност, тъй като е скрита. За избягване на този проблем трябва да си създадем собствен runtime callable wrapper.

  • Ако пък трябва да се конвертира .NET сигнатура към COM сигнатура (както може да се наложи, ако COM клиент трябва да използва нашия обект – за това ще говорим по-долу), това става, като връщаната от метода стойност става [out, retval] параметър в COM сигнатурата, а връщаната стойност в COM сигнатурата е от тип HRESULT.

  • Понякога обаче, при конвертиране на .NET сигнатура към COM сигнатура, може да се наложи да запазим сигнатурата, т.е. да не се слага автоматично връщана стойност от тип HRESULT и да се остави истинската връщана стойност. Можем да постигнем това, като маркираме .NET метода с атрибута PreserveSig. Например методът
    [PreserveSig]
    short GetNumberOfSpaces(string str);
    ще получи следната COM сигнатура:
    short GetNumberOfSpaces([in] BSTR str);

Маршализация

  • При маршализирането на данните, маршализаторът трябва да знае на кой managed тип какъв unmanaged тип съответства. Той може автоматично да разбере кое как се маршализира чрез изследване на сигнатурата на съответния метод.

  • Можем да използваме System.Runtime.InteropServices.MarshalAsAttribute върху параметър, поле на клас или връщана стойност, за да променяме поведението на маршализатора (в случай, че не се е ориентирал правилно). Например, символният низ (string) винаги се маршализира към BSTR при работа с COM, освен ако изрично не укажем друго. Ето защо, ако искаме някакъв стринг да се маршализира, например, като LPWSTR, трябва да укажем това с MarshalAs атрибут:
    public void SomeFunction([MarshalAs(UnmanagedType.LPWStr)] string message);
    приложено върху върната стойност пък, това изглежда така:
    [return: MarshalAs(UnmanagedType.LPWStr)]
    public string SomeOtherFunction();

  • Все пак има сложни случаи, когато маршализацията по подразбиране не помага и може да имаме нужда от наша си собствена. Тогава може да се наложи да си напишем custom wrapper.

За какво трябва да се внимава

  • Тъй като извикването от managed на unmanaged код или обратно е свързано с някакво забавяне, добре е да минимизираме тези преходи. Можем, например, вместо често да извикваме unmanaged методи, да извикаме един unmanaged метод, който да извика няколко пъти други unmanaged методи.

  • Трябва също така да се внимава за threading моделите, тъй като използването на несъвместими threading модели от .NET кода и от COM компонента предизвиква забавяне поради нуждата от използване на прокси. За целта трябва да използваме ApartmentState свойството на thread-овете, за да укажем правилен threading модел, който да се използва при работата с COM.

  • Security проверките за друго нещо, което може да предизвика забавяне. При всяко извикване на unmanaged код, runtime проверява дали имаме UnmanagedCode permission. За да избегнем тези многократни проверки, можем да маркираме класа или модула си с атрибута [SuppressUnmanagedCodeSecurity]. Той може да бъде използван само върху код, който има UnmanagedCode permission. Тъй като това обаче изключва security проверките, друг код, който няма такъв permission, може да използва нашите методи, за да извиква unmanaged код. Ето защо, трябва много да внимаваме, когато използваме този атрибут, и да сме сигурни, че не предоставяме такава възможност.


Използване на .NET Обекти от COM
COM Callable Wrappers

  • Тъй като COM клиенти не могат да използват .NET обекти директно, когато това се налага, .NET Framework създава т.нар. “COM callable wrapper” – proxy обект, достъпен за COM клиента, който служи за достъп до истинския .NET обект, правилна маршализация, както и управление на времето на живот на обектите.

  • COM callable wrapper-ите не са видими за .NET обекти. Те не подлежат на Garbage Collection, затова са директно достъпни от unmanaged код (какъвто е един COM клиент).

  • Както вече споменахме, COM callable wrapper-ите помагат при управлението на живота на обектите. Те разчитат на reference counting механизъма, използван при COM – когато вече никой не ползва обекта, COM callable wrapper-ът го “освобождава”, т.е. престава да пази референция към него, и той вече може да бъде събран от Garbage Collector-а.

  • За всеки managed обект, който се ползва от COM клиенти, се създава само по един COM callable wrapper. Този COM callable wrapper обслужва извикванията към този конкретен обект и само към него, като не пречи междувременно обектът да бъде ползван и от друг managed код.

  • За да бъде ползван от COM клиенти, за .NET обектът трябва да бъде създадена COM type library. За целта се използва програмката Type Library Exporter (tlbexp.exe), която е част от .NET Framework SDK и която използва метаданните на асемблито, за да генерира type library. Тази type library вече може да бъде импортирана от COM клиентите и те могат да бъдат разработвани, сякаш работят с обикновен COM обект. Когато се опитат обаче да извикат .NET обекта, runtime-ът използва метаданните на асемблито, за да създаде COM callable wrapper.

  • Можем да оказваме влияние върху това как точно ще изглежда COM callable wrapper-ът чрез различни атрибути.

  • Трябва да внимаваме с thread safety-то на използваните от COM обекти, тъй като при тези механизми COM клиентът указва какъв threading model да се използва. Ето защо, когато създаваме обекти, които могат да бъдат ползвани от клиенти, използващи многонишкови апартаменти (multi-threaded apartment – MTA), трябва да се уверим, че са безопасни за извикване от няколко нишки едновременно.

Какво се случва при експортиране?

  • Когато използваме Type Library Exporter, за да експортираме някакво асембли до type library, класовете и интерфейсите, които сме декларирали в асемблито, се представят по определен начин в type library-то.

  • При експортирането, всички публични класове от асемблито се конвертират до компонентни класове (component classes – coclasses), притежаващи същото име като .NET класовете. Експортират се също така и интерфейсите, използвани от тези класове. Не се експортират обаче методите и свойствата на класовете.

  • Освен това, при експортирането, всички интерфейси се конвертират до COM интерфейси, които по подразбиране са двойни (dual, т.е. които са достъпни както чрез vtable, така и чрез IDispatch интерфейс). Ако искаме да променим това, можем да използваме атрибута [InterfaceType] върху интерфейса, подавайки му като параметър константа от изброимия тип ComInterfaceType – InterfaceIsDual, InterfaceIsIDispatch или InterfaceIsIUnknown. Ако например искаме даден интерфейс да бъде достъпен само през IDispatch, можем да го постигнем по следния начин:
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    interface IMyInterface
    {

    }


  • За всеки клас, за който се създава coclass (т.е. за всеки публичен клас), се експортва също така и специален интерфейс, наречен class interface, който вече дава достъп до публичните методи и свойства на класа. Този клас обикновено носи името на класа, предшествано от символа за подчертаване. Ако класът наследява други класове, се експортва class interface и за него. Например, ако класът SpecialBall наследява класа Ball, coclass-ът на SpecialBall ще експортва class interface-ите _SpecialBall, _Ball и _Object.

  • Class interface-ът се генерира автоматично, като по подразбиране е достъпен само през IDispatch. Можем да променим това поведение, като използваме върху класа атрибута [ClassInterface], на който подадем подходящ параметър от изброимия тип ClassInterfaceType, с възможни стойности AutoDual (за да бъде интерфейсът двоен), AutoDispatch (подразбиращата се стойност), или None (да не бъде създаван class interface).

  • Използването на двоен class interface трябва да се избягва на всяка цена, защото може да доведе до много сериозни проблеми с версиите в бъдеще, когато прекомпилираме или променим леко класа. Най-добрата възможна практика е винаги да използваме само изрично имплементирани интерфейси, и да използваме ClassInterface с параметър ClassInterfaceType.None, за да не се създава class interface за класа.

Регистриране на .NET компоненти в системното registry

  • За да може един COM обект да бъде използван от COM клиенти, той трябва да бъде правилно регистриран в системното registry. Това важи и за .NET обектите, които трябва да бъдат достъпни чрез COM – те трябва да бъдат регистрирани в registry-то като COM обекти.

  • За целта се използва инструментът Assembly Registration Tool (regasm.exe), който е част от стандартната инсталация на .NET Framework и на който трябва да подадем името на асемблито, което трябва да бъде регистрирано.

  • Друг вариант, ако искаме програмно да регистрираме асемблито (вместо чрез regasm.exe), е да използваме RegistrationServices класа, намиращ се в неймспейса System.Runtime.InteropServices.

  • Това, което се случва при регистрация е, че в registry-то се регистрира нашият COM обект, но като изпълним файл се посочва .NET runtime-ът – mscoree.dll. По този начин, когато някой се опита да използва .NET обектите ни чрез COM, се извиква .NET runtime, който поема грижата за това.

Добри практики

  • Избягвайте генерирането на class interface-и. Старайте се винаги да използвате изрично имплементирани интерфейси, и използвайте атрибута ClassInterfaceAttribute с параметър ClassInterfaceType.None, за да предотвратите автоматичното създаване на class interface.

  • На всяка цена избягвайте генерирането на двойни (dual) class interface-и. Макар, че могат да бъдат полезни по време на тестване, могат да доведат до много сериозни проблеми с версиите в бъдеще.


База данных защищена авторским правом ©obuch.info 2016
отнасят до администрацията

    Начална страница