Interoperability

Snoops

   

 
Migrating to .NET: A Pragmatic Path to Visual Basic .NET, Visual C++ .NET, and ASP.NET
By Dhananjay  Katre, Prashant  Halari, Narayana  Rao  Surapaneni, Manu  Gupta, Meghana  Deshpande

Table of Contents
Chapter 9.   Migrating to Visual C++ .NET


There is nothing to stop you directly calling WIN32 APIs or other C APIs exposed by native DLLs from within your managed code. Just include the header file, link to the corresponding DLL, and off you go. As we explore this most powerful and unique feature of managed C++, we'll find out the power it gives you and the responsibilities it adds.

First things first; the unmanaged world doesn't know anything about managed data types, so they have to be explicitly converted to the corresponding unmanaged data types. If you are passing any managed reference as a parameter to an unmanaged function, you should pin the reference before passing it. Huge variety exists in the representation of character strings, and they will demand the maximum of your attention. We might want to convert a managed string (System::String *) to ANSI or UNICODE strings or to a BSTR. Additionally you may need to specify whether it should be created using COM allocator or WIN32 GlobalAlloc API. A lot of support is available in the System::Runtime::InteropServices:: Marshal class for customized data marshalling. Because you are explicitly asking for memory in this case, it's your responsibility to free it after native calls.

Additionally, you should pass only pinned references as parameters to unmanaged code so that the garbage collector doesn't move the managed object while an unmanaged function is working with it. Note that you are completely on your own once you decide to mix managed and unmanaged code, and the compiler will not be of much help (it's like standard C++ where memory leaks or invalid pointers cannot be detected by the compiler). The compiler cannot warn you if you forget to free the memory or pass an unpinned reference. But if you can do all this responsibly, you get direct access to unmanaged code and can considerably improve performance when there are lots of native calls and relatively less data marshalling to be done. This technique has been aptly named It Just Works, or IJW.

Let's look at a practical scenario that deals with multiple aspects of using IJW. We have created a __gc structure (so that you find the situation similar to your own requirements) that contains both System::String * and char * member variables . In this example we are calling the GetPrivateProfileString() WIN32 API to retrieve the timer driver name from the system.ini configuration file. The API takes file, section, and key names and a default value as [in] parameters and a buffer (to store the retrieved value) as an [out] parameter. Here managed strings need to be converted to character strings on the C++ heap (data marshalling), and the managed object has to be pinned before passing internal pointers as a function parameter. Once marshalled, the unmanaged character strings can be passed to any number of APIs before we explicitly release the memory. The following code is in the directory Simple IJW :

 graphics/icon01.gif // Including windows.h for GetPrivateProfileString() and  // MessageBox() declarations.  #include <windows.h>  #using <mscorlib.dll>  using namespace System;  using namespace System::Runtime::InteropServices;  const int DRIVER_NAME_LENGTH = 20;  // Here we are declaring a __gc struct with few String * and  //char * fields so as to demonstrate customized data  //marshalling and pinning.  // Keeping member variables as public for simplicity sake.  public __gc struct DriverInfo{  public:      String      * m_pszSection;      String      * m_pszKey;      String      * m_pszDefault;      char  * m_pcValue;      char  * m_pcFileName;  };  void main(){      DriverInfo * pGCDriverInfo = new DriverInfo;      // initialize it's properties.      pGCDriverInfo->m_pszSection   = S"drivers";      pGCDriverInfo->m_pszKey       = S"timer";      pGCDriverInfo->m_pszDefault   = S"none";      pGCDriverInfo->m_pcValue = new char[DRIVER_NAME_LENGTH];      pGCDriverInfo->m_pcFileName   = "system.ini";  // DriverInfo is a __gc struct whose first three fields  //are System::String *. We need to explicitly create  //their "char *" representation. remaining two fields  // are "char *. since we'll be passing them directly  //hence we need to pin pGCDriverInfo.      // creating char * representation of first three      // parameters  char * pcSection = (char*) Marshal::StringToHGlobalAnsi  (pGCDriverInfo->m_pszSection).ToPointer();  char * pcKey = (char*) Marshal::StringToHGlobalAnsi  (pGCDriverInfo->m_pszKey).ToPointer();  char * pcDefault = (char*) Marshal::StringToHGlobalAnsi  (pGCDriverInfo->m_pszDefault).ToPointer();      DriverInfo __pin * ppDriverInfo = pGCDriverInfo;  GetPrivateProfileString(pcSection, pcKey, pcDefault,  ppDriverInfo->m_pcValue, DRIVER_NAME_LENGTH,  ppDriverInfo->m_pcFileName);  // Already marshalled character strings may be passed  //to yet another WIN32 API.  MessageBox(0, ppDriverInfo->m_pcValue, pcSection, 0);  Console::WriteLine("Timer driver is {0}",  new String (pGCDriverInfo->m_pcValue));      // Managed object need not remain pinned now.      ppDriverInfo = NULL;      Marshal::FreeHGlobal(pcSection);      Marshal::FreeHGlobal(pcKey);      Marshal::FreeHGlobal(pcDefault);  } // main 

The Marshal::StringToHGlobalAnsi method copies this string to native heap and returns a pointer to it. All Marshal::StringToxxxx methods return System::intPtr ( public __value struct IntPtr : public Iserializable ), which is basically a platform-specific int (holding the memory address). This is the .NET way of referring to memory locations because many languages don't support pointers. Here customized marshalling involves the unnecessary overhead of a ToPointer() call on IntPtr, which returns a void * . Also it's our responsibility to free the memory ( acquired during marshalling) when it's no longer required.

Now let's see how .NET Framework support makes a developer's life simpler. If we decide to use P/Invoke, we just have to import the DLL and specify how we want the data to be marshalled. And all this is achievable using attributes rather than any specialized programming. The following code is in the directory Simple_PInvoke :

 graphics/icon01.gif #using <mscorlib.dll>  using namespace System;  using namespace System::Runtime::InteropServices;  // No need to include "windows.h" as we are providing  //extern declaration.  // Original definition :  // DWORD GetPrivateProfileString(LPCTSTR, LPCTSTR, LPCTSTR,  //LPTSTR, DWORD, LPCTSTR)  // Specifying that paramaters should be marshalled as ANSI  //character string  [DllImport("kernel32", CharSet=CharSet::Ansi)]  extern "C" int GetPrivateProfileString(String *, String *,  String *, char *, int, char *);  // Original definition :  // int MessageBox(HWND, LPCTSTR, LPCTSTR, UINT);  // Specifying how individual parameters should be marshalled  [DllImport("user32")]  extern "C" int MessageBox(void *,  [MarshalAs (UnmanagedType::LPStr)] String *,  [MarshalAs (UnmanagedType::LPStr)] String *, int uType);  const int DRIVER_NAME_LENGTH = 20;  // Same struct as the one used in IJW example  public __gc struct DriverInfo{  public:      String      * m_pszSection;      String      * m_pszKey;      String      * m_pszDefault;      char  * m_pcValue;      char  * m_pcFileName;  };  void main(){      DriverInfo * pGCDriverInfo = new DriverInfo;      // initialize its fields      pGCDriverInfo->m_pszSection   = S"drivers";      pGCDriverInfo->m_pszKey       = S"timer";      pGCDriverInfo->m_pszDefault   = S"Key not found";      pGCDriverInfo->m_pcValue      = new  char(DRIVER_NAME_LENGTH);      pGCDriverInfo->m_pcFileName   = "system.ini";  // Having specified how the data should be marshalled,  //we can now rely on P/Invoke to do the data- //marshalling and pinning, as and when required. Just  //invoke WIN32 APIs      int nRetVal = GetPrivateProfileString  (pGCDriverInfo->m_pszSection, pGCDriverInfo->m_pszKey,  pGCDriverInfo->m_pszDefault, pGCDriverInfo->m_pcValue,  DRIVER_NAME_LENGTH, pGCDriverInfo->m_pcFileName);  // Now driver name should be stored in  //pGCDriverInfo->m_pcValueInvoking yet another WIN32 //API.  MessageBox(0, pGCDriverInfo->m_pcValue,  pGCDriverInfo->m_pszSection, 0);      Console::WriteLine("Timer driver is {0}",  new String (pGCDriverInfo->m_pcValue));      // We never requested memory or pinned any managed  //object, hence no cleanup.  } // main 

In P/Invoke, we do an extern declaration of the method we intend to use (also specify the DLL that implements it) and specify how the parameters should be marshalled (using attributes defined in the InteropServices namespace). Note that we haven't included any header file because we are specifying the DLL name itself (attempts to do so will result in a linker error). Attributed programming is the hallmark of the .NET programming model, and it's about time we get used to it.

In the extern declaration of the MessageBox() API, we have specified that the character-string should be marshalled as UnmanagedType::LPStr . The MarshalAs attribute becomes important when a function is taking more than one character string and we want them to be marshalled differently. But we can save on the effort of specifying it for every parameter when all the strings have to be converted to a common character type.

The preceding examples demonstrate a typical use of P/Invoke and contrast it with the additional work on a programmer's part to achieve the same result. P/Invoke is certainly more elegant and straightforward, and IJW obstructs the normal flow of your program and distracts you from the business logic. But data marshalling and memory allocation/deallocation take place for each call via P/Invoke. This is clearly a waste of effort if the same data is to be passed to more than one API, and in this case IJW becomes a superior choice because of the explicit control you enjoy. Also we have no choice but to use IJW when an unmanaged API allocates some memory on the unmanaged heap that should be freed by the caller.

We saw the example of using WIN32 APIs via IJW and P/Invoke. Methods exported by regular DLLs may be used with the same ease. The process remains exactly the same except that in the case of IJW, we explicitly establish the dependency of our application on the imported DLL (go to Project -> Properties -> Linker -> Input and put in the library name under "Additional Dependencies." Additionally ensure that the DLL is in the searchable path). Another issue is that you cannot use P/Invoke to work with imported classes. It just works with imported methods.

For completeness sake, here is a simplified scenario where you create your own DLL and use it from the managed extensions project. Create a WIN32 DLL using the AppWizard of good-old Visual C++ 6.0 (you can name your project Win32Lib). Choose to create a simple DLL that exports some symbols and modify the default code as follows . The following code is in the directory Using_DLLs/ Win32Lib :

 graphics/icon01.gif  //WIN32Lib.h:  #include <iostream.h>  #ifdef WIN32LIB_EXPORTS  #define WIN32LIB_API __declspec(dllexport)  #else  #define WIN32LIB_API __declspec(dllimport)  #endif  // This class is exported from the Win32Lib.dll  class WIN32LIB_API CMathLib{  public:      int Square(int n);  };  // A Simple method that takes a char * and prints it,  extern "C" WIN32LIB_API int PrintName(char * pcName);  //WIN32LIB.H  // Only relevant part shown for brevity  WIN32LIB_API int CMathLib::Square(int n)      {      return n*n;  }  WIN32LIB_API int PrintName(char * pcName)     {      cout << "Organization : " << pcName << endl << endl;      return 1;  } 

As mentioned earlier, PInvoke isn't much help for using classes exported by some library; hence, we'll use IJW for this. Create a simple managed extensions project, say DotNetClient, and copy Win32Lib.h, Win32Lib.lib, and Win32Lib.dll it your current project folder. Additionally, establish the additional dependency by specifying it in Project -> Properties -> Liner -> Input. Add the following code, which is in the directory Using_DLLs/ DotnetClient_using_IJW :

 graphics/icon01.gif  //Dotnetclient.cpp  #using <mscorlib.dll>  #include "Win32Lib.h"  using namespace System;  using namespace System::Runtime::InteropServices;  void _tmain(void){      CMathLib objMathLib;      int Num = 10;  Console::WriteLine("Square of {0} = {1}",Num.ToString(),      (objMathLib.Square(Num)).ToString());      // Calling a simple method exported by Win32Lib.dll      String * strName = "PATNI COMPUTER SYSTEM";      char * pcName = (char *)  Marshal::StringToHGlobalAnsi(strName).ToPointer();      PrintName(pcName);      Marshal::FreeHGlobal(pcName);  } 

Output:

 Square of 10 = 100  Organization : PATNI COMPUTER SYSTEM 

Let's turn our attention to P/Invoke now. Rewrite the Win32Lib.cpp file as follows. Remember not to include the Win32Lib.h header file. We cannot use the imported class via PInvoke, hence, we'll only be calling the PrintName method. The following code is in the directory Using_DLLs/ DotnetClient_ using_ Pinvoke :

 graphics/icon01.gif #using <mscorlib.dll>  using namespace System;  using namespace System::Runtime::InteropServices;  [DllImport("Win32Lib.dll")]  extern "C" int PrintName([MarshalAs (UnmanagedType::LPStr)]  String *);  // Demonstrating P/Invoke for native DLLs.  void _tmain(void){      // Calling a simple method exported by Win32Lib.dll      String * strName = "PATNI COMPUTER SYSTEM";      PrintName(strName);  } 

This straightforward application was basically a warm-up to something more challenging. An interesting situation arises when we need to pass a function pointer as a callback method. There are many WIN32 APIs that accept a callback method, or you might have created some libraries that expect a function pointer. And you know that we don't deal with direct function pointers in the managed world. Again P/Invoke comes to our rescue. Here we create a delegate type with the required signature and initialize it with our callback method. P/Invoke marshals this delegate type as a function pointer depending on the context. Remember that delegates can contain only member methods, not global methods. Let's modify our workhorse Win32Lib.dll project, to demonstrate implementing callbacks using delegates. Add the following code at the end of the respective files:

 graphics/icon01.gif  //Win32Lib.h:  // Declaring a fucntion pointer type  typedef void (* CllBckFnPtr)(char *) ;  // The method of accepting a callback function has to be a  //global method because PInvoke doesn't support importing  //classes.  extern "C" WIN32LIB_API int AcceptFnPtr(CllBckFnPtr pFunc); 

Now let's have a look at the Win32Lib.cpp . This is in the directory Implemeting_Callbacks/ Win32Lib .

 graphics/icon01.gif WIN32LIB_API int AcceptFnPtr(CllBckFnPtr pFunc){  cout << "From within AcceptFnPtr method of Win32Lib.dll"  << endl;  cout << "Invoking the callback method passed to this function"  << endl << endl;  pFunc("This string is passed from AcceptFnPtr method of  Win32Lib.dll");      return 1;  } 

In the preceding we have declared a function pointer type (CllBckFnPtr is a type definition for a pointer to functions that take a char * and return an int). Next we declare a method, AcceptFnPtr, which takes a function pointer of CllBckFnPtr type as its sole argument and returns an int. This function outputs some text (information about itself) and then invokes the passed function. This is a common scenario in the WIN32 programming model and having to use it in a managed world can give one nightmares if left to oneself. Now let's see how to call AcceptFnPtr from managed extensions and pass a member-function of a managed class. This code is in the directory Implemeting_Callbacks/ DotnetClient_using_PInvoke .

 graphics/icon01.gif  //DotnetClient.cpp  #using <mscorlib.dll>  using namespace System;  using namespace System::Runtime::InteropServices;  __delegate void CallbackMethod(char*);  [DllImport("Win32Lib.dll")]  extern "C" int AcceptFnPtr(CallbackMethod *);  [DllImport("Win32Lib.dll")]  extern "C" int PrintName([MarshalAs (UnmanagedType::LPStr)]  String *);  __gc class COwnerClass{  public:  void NotificationHandler(char * pcMessage){  Console::WriteLine("From within  COwnerClass::NotificationHandler ");  Console::WriteLine("Notification Message is as follows : ");  Console::WriteLine(new String(pcMessage));    }  };  // Implementing callbacks by using delegates  void _tmain(void){      // Calling a simple method exported by Win32Lib.dll      String * strName = "PATNI COMPUTER SYSTEM";      PrintName(strName);      // Now experimenting with callbacks      Console::WriteLine("Now experimenting with callbacks");  // Create and delegate type and initialize it.      COwnerClass * pGCOwner = new COwnerClass ;      CallbackMethod * fnPtr = new CallbackMethod(pGCOwner,            &(COwnerCass::NotificationHandler));  // Pass this delegate to native method expecting a  //callback method.      AcceptFnPtr(fnPtr);  } 

Output:

 Organization : PATNI COMPUTER SYSTEM  Now experimenting with callbacks  From within AcceptFnPtr method of Win32Lib.dll  Invoking the callback method passed to this function  From within COwnerClass::NotificationHandler  Notification Message is as follows:  This string is passed from AcceptFnPtr method of Win32Lib.dll 

The trick here is declaring a delegate that can contain methods to be passed as callback functions. We have defined a managed class, COwnerClass, which has a member function accepting a char * and returning an int. An appropriate delegate has been declared at the top of DotnetClient.cpp. COwnerClass is instantiated in main(), and its NotificationHandler method is wrapped in the CallbackMethod delegate type. The crucial support is provided by following lines:

 graphics/icon01.gif __delegate void CallbackMethod(char*);  [DllImport("Win32Lib.dll")]  extern "C" int AcceptFnPtr(CallbackMethod *); 

Here we have redeclared the imported AcceptFnPtr method to take a pointer to delegate. When we make the actual call, P/Invoke steps in to do the necessary translations.

To sum up our discussion thus for, let's consider the strengths and limitations of each approach (P/Invoke versus IJW). IJW offers you direct control over how you want your data to be marshalled and invokes the best method based on your data type (whether you have an ANSI or wide character string). P/Invoke makes these choices for you and calls the most appropriate method. Also it has to check for the need to pin managed references. All this makes it a bit slower compared to IJW in certain circumstances. P/Invoke has an operational overhead of between 10 and 30 (x86) instructions per call; hence, in a situation requiring multiple native calls with little data marshalling, it can seriously degrade the performance. But P/Invoke is desirable when few native calls with lots of data marshalling are involved. As mentioned (and experienced by you), customized data marshalling is highly obtrusive to the flow of your code, and you better be very careful in converting your data to the right type and pinning managed references. But it's not always a choice between performance and ease of use. P/Invoke doesn't work with imported classes; hence IJW is all that we've got for it. And P/Invoke is the hands down winner when it comes to implementing callback functionality.


Snoops

   
Top


Migrating to. NET. A Pragmatic Path to Visual Basic. NET, Visual C++. NET, and ASP. NET
Migrating to. NET. A Pragmatic Path to Visual Basic. NET, Visual C++. NET, and ASP. NET
ISBN: 131009621
EAN: N/A
Year: 2001
Pages: 149

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net