⬆️ ⬇️

.NET wrappers for native C ++ / CLI libraries

Translator's Preface



This article is a translation of Chapter 10 from the book of Expert C ++ / CLI: .NET for Visual C ++ Programmers by Marcus Heege. This chapter discusses the creation of wrapper classes for native C ++ classes, ranging from trivial cases to support for hierarchies and virtual methods of native classes.



The idea of ​​this translation appeared after the article “Unmanaged C ++ library in .NET. Full integration . The translation took longer than expected, but perhaps the approach shown here will also benefit the community.



Content



  1. Creating wrappers for native libraries

    1. Before you start

      1. Separate DLL or integration into a native library project?
      2. What part of the native library should be accessible through the wrapper?
    2. Language interaction
    3. Creating wrappers for classes

      1. Mapping native types to CLS compliant types
      2. Mapping C ++ Exceptions to Managed Exceptions
      3. Mapping of managed arrays to native types
      4. Display of other non-primitive types
      5. Support inheritance and virtual methods
    4. General recommendations

      1. Simplify wrappers from the start
      2. Consider the .NET philosophy
    5. Conclusion




Creating wrappers for native libraries



There are many situations that require writing a wrapper over a native library. You can create a wrapper for a library whose code you can modify. You can wrap a part of the Win32 API, the wrapper for which is missing in the FCL. Perhaps you are creating a wrapper for a third-party library. The library can be both static and dynamic (DLL). Moreover, it can be both C and C ++ library. This chapter contains practical advice, general tips, and solutions for several specific problems.



Before you start



Before writing code, you should consider the different approaches to writing wrappers and their advantages and disadvantages from the point of view of both creating and using your library.



Separate DLL or integration into a native library project?



Visual C ++ projects may contain files compiled into managed code [for more information, see Chapter 7 of the book]. Integration of wrappers into the native library may seem like a good idea, because then you will have one less library. In addition, if you are integrating wrappers into a DLL, then the client application will not have to load an extra dynamic library. The smaller the DLL is loaded, the shorter the load time, the less virtual memory is required, the less likely it is that the library will be moved to memory due to the inability to load it at the original base address.



However, the inclusion of wrappers in the library being wrapped is generally not beneficial. To better understand why, you should separately consider building a static library and DLL.

')

Strange as it may sound, managed types can be included in a static library. However, this can easily lead to problems of type identity. An assembly in which a type is defined is part of its identifier. Thus, the CLR can distinguish between two types from different assemblies, even if the type names are the same. If two different projects use the same managed type from a static library, then this type will be arranged in both assemblies. Since the assembly is part of a type identifier, we get two types with different identifiers, although they were defined in the same static library.



Integration of managed types into the native DLL is also not recommended, since loading the library will require CLR 2.0 in load time. Even if an application using the library refers only to native code, to load the library, it is required that CLR 2.0 is installed on the computer and that the application does not download an earlier version of the CLR.



What part of the native library should be accessible through the wrapper?



As is often the case, it is very useful to clearly define the tasks of a developer before starting to write code. I know that this sounds like a quotation from a second-rate book on software development from the early 90s, but for the wrappers of native libraries, the statement of the problem is especially important.



When you start creating a wrapper for the native library, the task seems obvious - you already have an existing library and the managed API should bring its functionality to the world of managed code.



For most projects, such a general description is completely inadequate. Without a clearer understanding of the problem, you will probably write a wrapper for each native class of the C ++ library. If the library contains more than one single central abstraction, then it is often not worth creating one-to-one wrappers with native code. This will force you to solve problems that are not related to your specific task, as well as generate a lot of unused code.



To better describe the problem, think about the problem as a whole. To formulate the task more clearly, you need to answer two questions:





As a rule, by answering these questions, you can simplify your task by cutting back unnecessary parts and adding abstractions-wrappers based on usage scenarios. For example, take the following native API:



namespace NativeLib { class CryptoAlgorithm { public: virtual void Encrypt(/* ...      ... */) = 0; virtual void Decrypt(/* ...      ... */) = 0; }; class SampleCipher : public CryptoAlgorithm { /* ...      ... */ }; class AnotherCipherAlgorithm : public CryptoAlgorithm { /* ...      ... */ }; } 


This API gives the programmer the following features:





Implementing all these features in a managed wrapper is much more complicated than it might seem. Especially difficult to implement support for inheritance. As will be shown later, supporting virtual methods requires additional proxy classes, which leads to overhead during execution and takes time to write code.



However, it is very likely that the wrapper is only needed for one or two algorithms. If the wrapper for this API does not support inheritance, then its creation is simplified. With this simplification, you don’t have to create a wrapper for the abstract CryptoAlgorithm class. With virtual methods Encrypt and Decrypt it will be possible to work the same way as with any other. To make it clear that you do not want to support inheritance, it suffices to declare wrappers for SampleCipher and AnotherCipherAlgorithm as sealed classes.



Language interaction



One of the main goals in creating .NET was to ensure the interaction of different languages. If you create a wrapper for a native library, the ability to interact with different languages ​​becomes particularly important, as developers using the library are more likely to use C # or other .NET languages. Common Language Infrastructure (CLI) is the basis of the .NET specification [for more information, see Chapter 1 of the book]. An important part of this specification is the Common Type System (CTS). Although all .NET languages ​​are based on a common type system, not all languages ​​support all features of this system.



To clearly determine whether languages ​​can communicate with each other, the CLI contains the Common Language Specification (CLS). CLS is a contract between developers using .NET languages ​​and developers of libraries that can be used from different languages. CLS sets the minimum feature set that each .NET language must support. In order for the library to be used from any .NET language corresponding to the CLS, the language features used in the library's public interface should be limited to the CLS capabilities. The public interface of the library refers to all types with public visibility defined in the assembly, and all members of such types with public, public protected or protected visibility.



You can use the CLSCompliantAttribute attribute to designate a type or its member as the corresponding CLS. By default, types not marked with this attribute are considered non-compliant with CLS. If you apply this attribute at the assembly level, by default all types will be considered compliant with CLS. The following example shows how to apply this attribute to assemblies and types:



 [assembly: CLSCompliant(true)]; //    public  CLS,     namespace ManagedWrapper { public ref class SampleCipher sealed { // ... }; [CLSCompliant(false)] //        CLS public ref class AnotherCipherAlgorithm sealed { // ... }; } 


According to CLS Rule 11, all types that are present in the signatures of class members (methods, properties, fields, and events) visible outside the assembly must comply with CLS. To properly use the [CLSCompliant] attribute, you need to know if the parameter types of the CLS method match. To determine the compliance of CLS, you need to check the attributes of the assembly in which the type is declared, as well as the attributes of the type itself.



The CLSCompliant attribute is also used in the Framework Class Library (FCL). mscorlib and most other FCL libraries use the [CLSCompliant (true)] attribute at the assembly level and mark non-CLS types with the [CLSCompliant (false)] attribute.



Note that the following primitive types in mscorlib are marked as inappropriate with CLS: System :: SByte, System :: UInt16, System :: UInt32, and System :: UInt64. These types (or their equivalent type names char, unsigned short, unsigned int, unsigned long, and unsigned long long in C ++) cannot be used in type member signatures that are considered to be CLS compliant.



If a type is considered to conform to CLS, then all its members are also considered as such, unless explicitly stated otherwise. Example:



 using namespace System; [assembly: CLSCompliant(true)]; //    public  CLS,     namespace ManagedWrapper { public ref class SampleCipher sealed // SampleCipher  CLS -   { public: void M1(int); // M2    CLS,          CLS [CLSCompliant(false)] void M2(unsigned int); }; } 


Unfortunately, the C ++ / CLI compiler does not show warnings when the type marked as the corresponding CLS violates the CLS rules. To understand whether to mark a type as an appropriate CLS or not, you need to know the following important CLS rules:





Translator's note - what is int ^
Unlike C #, explicitly specifying the type of the packed value is allowed in C ++ / CLI. For example:

  System::Int32 value = 1; System::Object ^boxedObject = value; //   value;  object boxedObject = value;  C# System::Int32 ^boxedInt = value; //    value,     C#     






Creating wrappers for C ++ classes



Despite some similarity of the type system in C ++ and CTS in .NET, the creation of managed wrapper types for C ++ classes often presents unpleasant surprises. Obviously, if you use the features of C ++, which have no analogues in managed code, then creating a wrapper can be difficult. For example, if the library actively uses multiple inheritance. But even if there are similar constructions in all managed C ++ features used in managed code, the reflection of the native API in the wrapper API may not be obvious. Let's look at possible problems.



You cannot declare a managed class with a field of type NativeLib :: SampleCipher [for more information, see Chapter 8 of the book]. Since the fields of managed classes can only be pointers to native types, you should use a field of type NativeLib :: SampleCipher *. An instance of the native class must be created in the wrapper constructor and destroyed in the destructor.



 namespace ManagedWrapper { public ref class SampleCipher sealed { NativeLib::SampleCipher* pWrappedObject; public: SampleCipher(/*      */) { pWrappedObject = new NativeLib::SampleCipher(/*      */); } ~SampleCipher() { delete pWrappedObject; } /* ... */ }; } 


In addition to the destructor, it is also worth implementing the finalizer [more details can be found in chapter 11 of the book].



Mapping native types to CLS compliant types



After you have created a wrapper class, you need to add methods, properties, and events to it, which allow client code in .NET to access members of the wrapped object. In order for a wrapper to be used from any .NET language, all types in signatures of members of a wrapper class must conform to CLS. Instead of unsigned integers in signatures of the native API, you can usually use signed numbers of the same size. The choice of equivalents for native pointers and links is not always as simple. Sometimes, you can use System :: IntPtr instead of a native pointer. In this case, the managed code can receive the native pointer and pass it as an input parameter for further call. This is possible because on the binary level, System :: IntPtr is structured in the same way as a native pointer. In other cases, one or more parameters must be converted manually. This may take a long time, but it cannot be avoided. Consider the various options for wrappers.



If a C ++ reference or a pointer with the transfer semantics by reference is passed to the native function, then the monitored reference is recommended in the function wrapper [see note below]. Suppose a native function has the following form:



 void f(int& i);      : void fWrapper(int% i) { int j = i; f(j); i = j; } 


Translator's note - what is a tracking link
The tracking reference or tracking reference in C ++ / CLI is similar to the ref and out parameters in C #.



To call a native function, you must pass a native reference to an int. For this argument, it is necessary to perform marshalling manually, since the type conversion from the tracking link to the native link does not exist. Since there is a standard type conversion from int to int &, a local variable of type int is used, which serves as a buffer for the argument passed by reference. Before calling the native function, the buffer is initialized with the value passed as parameter i. After returning from the native function to the wrapper, the value of the parameter i is updated in accordance with changes in buffer j.



As can be seen from this example, in addition to the cost of the transition between managed and native code, wrappers are often forced to spend processor time on marshalling types. As will be shown later, for more complex types, these costs can be significantly higher.



It should be noted that some other .NET languages, including C #, distinguish between arguments passed by reference and arguments used only to return a value. The value of the argument passed by reference must be initialized before the call. The called method can change its value, but it is not obliged to do so. If the argument is used only for return, then it is passed uninitialized and the method must change or initialize its value.



By default, it is considered that the tracking link means passing by reference. If you want the argument to be used only to return a value, you must apply the OutAttribute attribute from the System :: Runtime :: InteropServices namespace, as shown in the following example:



 void fWrapper([Out] int% i); 


Argument types for native functions often contain a const modifier, as in the example below:



 void f(int& i1, const int& i2); 


The const modifier is translated into an optional method signature modifier [for more details, see Chapter 8 of the book]. The fWrapper method can still be called from managed code, even if the caller does not accept the const modifier:



 void fWrapper(int% i1, const int% i2); 


To pass a pointer to an array as a parameter of a native function, it is not enough just to use the tracking link. To disassemble this case, suppose that the native SampleCipher class has a constructor that accepts an encryption key:



 namespace NativeLib { class SampleCipher : public CryptoAlgorithm { public: SampleCipher(const unsigned char* pKey, int nKeySizeInBytes); /* ...       ... */ }; } 


In this case, it is not enough to simply display const unsigned char * in const unsigned char%, because the encryption key passed to the native type constructor contains more than one byte. Best to use the following approach:



 namespace ManagedWrapper { public ref class SampleCipher { NativeLib::SampleCipher* pWrappedObject; public: SampleCipher(array<Byte>^ key); /* ... */ }; } 


In this constructor, both arguments of the native constructor (pKey and nKeySizeInBytes) are mapped to a single type argument of the managed array. This can be done because the size of the managed array can be determined at run time.



How to implement this constructor depends on the implementation of the native SampleCipher class. If the constructor creates an internal copy of the key passed as an argument to pKey, then you can pass a docking pointer to the key:



 SampleCipher::SampleCipher(array<Byte>^ key) { if (!key) throw gcnew ArgumentNullException("key"); pin_ptr<unsigned char> pp = &key[0]; pWrappedObject = new NativeLib::SampleCipher(pp, key->Length); } 


However, the docking pointer cannot be used if the native SampleCipher class is implemented as follows:



 namespace NativeLib { class SampleCipher : public CryptoAlgorithm { const unsigned char* pKey; const int nKeySizeInBytes; public: SampleCipher(const unsigned char* pKey, int nKeySizeInBytes) : pKey(pKey), nKeySizeInBytes(nKeySizeInBytes) {} /* ...         ... */ }; } 


This constructor requires the client not to release the memory containing the key, and the pointer to the key remains valid until an instance of the SampleCipher class is destroyed. The wrapper constructor does not fulfill any of these requirements. Since the wrapper does not contain a handle to the managed array, the garbage collector can collect the array before the instance of the native class is destroyed. Even if you save the object handle so that the memory is not freed, the array can be moved during garbage collection. In this case, the native pointer will no longer point to the managed array. In order for the memory containing the key not to be released and not moved when garbage is collected, the key must be copied to the native heap.



To do this, you need to make changes to both the constructor and the managed wrapper destructor. The following code shows a possible implementation of the constructor and destructor:



 public ref class SampleCipher { unsigned char* pKey; NativeLib::SampleCipher* pWrappedObject; public: SampleCipher(array<Byte>^ key) : pKey(0), pWrappedObject(0) { if (!key) throw gcnew ArgumentNullException("key"); pKey = new unsigned char[key->Length]; if (!pKey) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); try { Marshal::Copy(key, 0, IntPtr(pKey), key->Length); pWrappedObject = new NativeLib::SampleCipher(pKey, key->Length); if (!pWrappedObject) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } catch (Object^) { delete[] pKey; throw; } } ~SampleCipher() { try { delete pWrappedObject; } finally { //  pKey,    pWrappedObject   delete[] pKey; } } /*       */ }; 


Translator's note - what is a descriptor
The handle is a reference to an object in the managed heap. In C # terms, this is just an object reference.



If you do not take into account some esoteric problems [discussed in Chapter 11 of the book], the code provided here ensures the correct allocation and release of resources, even when exceptions occur. If an error occurs during the creation of an instance of ManagedWrapper :: SampleCipher, all allocated resources will be freed. The destructor is implemented in such a way as to free the native array containing the key, even if the destructor of the object being wrapped throws an exception.



This code also shows the characteristic overhead of the managed wrappers. In addition to the overhead of calling the wrapped native functions from the managed code, the overhead of the mapping between native and managed types is often added.



Mapping C ++ Exceptions to Managed Exceptions



In addition to resource management that is resistant to exceptions, the managed wrapper must also take care of mapping the C ++ exceptions thrown by the native library into managed exceptions. For example, suppose the SampleCipher algorithm supports only 128 and 256-bit keys. The constructor NativeLib :: SampleCipher could throw a NativeLib :: CipherException exception if a key of the wrong size is passed to it. C ++ exceptions are mapped to System :: Runtime :: InteropServices :: SEHException exceptions, which is not very convenient for the library user [discussed in Chapter 9 of the book]. Therefore, it is necessary to intercept native exceptions and to throw managed exceptions containing equivalent data.



To display constructor exceptions, you can use a try block at the function level, as shown in the following example. This will allow to catch exceptions thrown both during initialization of class members and in the body of the constructor.



 SampleCipher::SampleCipher(array<Byte>^ key) try : pKey(0), pWrappedObject(0) { ..//       } catch(NativeLib::CipherException& ex) { throw gcnew CipherException(gcnew String(ex.what())); } 


Here, a try block is used at the function level, although the initialization of class members in this example should not result in throwing exceptions. This way, exceptions will be intercepted even if you add new members to the class or add SampleCipher inheritance from another class.



Mapping of managed arrays to native types



Now, having understood the constructor implementation, let's look at the Encrypt and Decrypt methods. Earlier, the indication of the signatures of these methods was postponed, now we give them in full:



 class CryptoAlgorithm { public: virtual void Encrypt( const unsigned char* pData, int nDataLength, unsigned char* pBuffer, int nBufferLength, int& nNumEncryptedBytes) = 0; virtual void Decrypt( const unsigned char* pData, int nDataLength, unsigned char* pBuffer, int nBufferLength, int& nNumEncryptedBytes) = 0; }; 


Data that must be encrypted or decrypted is transmitted using the pData and nDataLength parameters. Before calling Encrypt or Decrypt, allocate a memory buffer. The value of the pBuffer parameter must be a pointer to this buffer, and its size must be passed as the value of the nBufferLength parameter. The size of the output data is returned using the nNumEncryptedBytes parameter.



To display the Encrypt and Decrypt, you can add the following method to ManagedWrapper :: SampleCipher:



 namespace ManagedWrapper { public ref class SampleCipher sealed { // ... void Encrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumOutBytes) { if (!data) throw gcnew ArgumentException("data"); if (!buffer) throw gcnew ArgumentException("buffer"); pin_ptr<unsigned char> ppData = &data[0]; pin_ptr<unsigned char> ppBuffer = &buffer[0]; int temp = nNumOutBytes; pWrappedObject->Encrypt(ppData, data->Length, ppBuffer, buffer->Length, temp); nNumOutBytes = temp; } } 


In this implementation, it is assumed that NativeLib :: SampleCipher :: Encrypt is a non-blocking operation and that it completes execution in a reasonable amount of time. If you cannot make such assumptions, you should avoid pinning managed objects for the duration of running the native Encrypt method.To do this, you can copy the managed array to native memory before transferring the array to Encrypt and then copying the encrypted data from the native memory to the managed buffer. On the one hand, this entails additional costs for marshalling types, on the other hand, it prevents long-term consolidation .



Display of other non-primitive types



Previously, all types of parameters of displayed functions were either primitive types, or pointers or references to primitive types. If you need to display functions that take C ++ classes as parameters, or pointers or references to C ++ classes, additional actions are often required. Depending on the specific situation, the solutions may be different. To show different solutions with a specific example, consider another native class for which a wrapper is required:



 class EncryptingSender { CryptoAlgorithm& cryptoAlg; public: EncryptingSender(CryptoAlgorithm& cryptoAlg) : cryptoAlg(cryptoAlg) {} void SendData(const unsigned char* pData, int nDataLength) { unsigned char* pEncryptedData = new unsigned char[nDataLength]; int nEncryptedDataLength = 0; //    cryptoAlg.Encrypt(pData, nDataLength, pEncryptedData, nDataLength, nEncryptedDataLength); SendEncryptedData(pEncryptedData, nEncryptedDataLength); } private: void SendEncryptedData(const unsigned char* pEncryptedData, int nDataLength) { /*     ,   */ } }; 


As you can guess about the name of this class, its purpose is to send encrypted data. In the framework of the discussion, it does not matter where the data is sent and which protocol is used. For encryption, you can use classes that are inherited from CryptoAlgorithm (for example, SampleCipher). The encryption algorithm can be specified using the CryptoAlgorithm & type constructor parameter. An instance of the CryptoAlgorithm class that is passed to the constructor is used in the SendData method when calling the virtual method Encrypt. The following example shows how EncryptingSender can be used in native code:



 using namespace NativeLib; unsigned char key[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; SampleCipher sc(key, 16); EncryptingSender sender(sc); unsigned char pData[] = { '1', '2', '3' }; sender.SendData(pData, 3); 


To create a wrapper over NativeLib :: EncryptingSender, you can define the managed class ManagedWrapper :: EncryptingSender. Like the wrapper of the SampleCipher class, it must store a pointer to the wrapped object in the field. To create an instance of the wrapped class EncryptingSender requires an instance of the class NativeLib :: CryptoAlgorithm. Suppose the only encryption algorithm you want to support is SampleCipher. Then you can define a constructor that takes a value of type array <unsigned char> ^ as the encryption key. As well as the constructor for the class ManagedWrapper :: SampleCipher, the constructor of the EncryptingSender can use this array to create an instance of the native class NativeLib :: SampleCipher. Then, a link to this object can be passed to the NativeLib :: EncryptingSender constructor:



 public ref class EncryptingSender { NativeLib::SampleCipher* pSampleCipher; NativeLib::EncryptingSender* pEncryptingSender; public: EncryptingSender(array<Byte>^ key) try : pSampleCipher(0), pEncryptingSender(0) { if (!key) throw gcnew ArgumentNullException("key"); pin_ptr<unsigned char> ppKey = &key[0]; pSampleCipher = new NativeLib::SampleCipher(ppKey, key->Length); if (!pSampleCipher) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); try { pEncryptingSender = new NativeLib::EncryptingSender(*pSampleCipher); if (!pEncryptingSender) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } catch (Object^) { delete pSampleCipher; throw; } } catch(NativeLib::CipherException& ex) { throw gcnew CipherException(gcnew String(ex.what())); } // ..           }; 


With this approach, you don’t have to map the CryptoAlgorithm & type parameter to the managed type. However, sometimes this approach is too limited. For example, you want to give the opportunity to transfer an existing instance of SampleCipher, rather than creating a new one. To do this, the ManagedWrapper :: EncryptingSender constructor must have a parameter of type SampleCipher ^. To create an instance of the NativeLib :: EncryptingSender class inside the constructor, you need to get an object of the NativeLib :: SampleCipher class, which is wrapped in ManagedWrapper :: SampleCipher. To get the wrapped object, you need to add a new method:



 public ref class SampleCipher sealed { unsigned char* pKey; NativeLib::SampleCipher* pWrappedObject; internal: [CLSCompliant(false)] NativeLib::SampleCipher& GetWrappedObject() { return *pWrappedObject; } ...   SampleCipher    ... }; 


The following code shows a possible implementation of such a constructor:

 public ref class EncryptingSender { NativeLib::EncryptingSender* pEncryptingSender; public: EncryptingSender(SampleCipher^ cipher) { if (!cipher) throw gcnew ArgumentException("cipher"); pEncryptingSender = new NativeLib::EncryptingSender(cipher->GetWrappedObject()); if (!pEncryptingSender) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } // ...    EncryptingSender    ... }; 


ManagedWrapper::SampleCipher. EncryptingSender CryptoAlgorithm, , GetWrappedObject . :



 public interface class INativeCryptoAlgorithm { [CLSCompliant(false)] NativeLib::CryptoAlgorithm& GetWrappedObject(); }; 


SampleCipher :

 public ref class SampleCipher sealed : INativeCryptoAlgorithm { // ... internal: [CLSCompliant(false)] virtual NativeLib::CryptoAlgorithm& GetWrappedObject() = INativeCryptoAlgorithm::GetWrappedObject { return *pWrappedObject; } }; 


This method is implemented as internal, because code using the wrapper library should not directly call methods of the wrapped object. If you want to give the client access directly to the wrapped object, you should pass a pointer to it using System :: IntPtr, because the type System :: IntPtr corresponds to CLS.



Now the constructor for the class ManagedWrapper :: EncryptingSender takes a parameter of type INativeCryptoAlgorithm ^. To get an object of class NativeLib :: CryptoAlgorithm, necessary for creating an instance of EncryptingSender to be wrapped, you can call the GetWrappedObject method on the parameter of type INativeCryptoAlgorithm ^:



 EncryptingSender::EncryptingSender(INativeCryptoAlgorithm^ cipher) { if (!cipher) throw gcnew ArgumentException("cipher"); pEncryptingSender = new NativeLib::EncryptingSender(cipher->GetWrappedObject()); if (!pEncryptingSender) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } 




Support inheritance and virtual methods



INativeCryptoAlgorithm, ManagedWrapper::EncryptingSender. EncryptingSender . , . -.



, NativeLib::CryptoAlgorithm. GetWrappedObject, - :



 public ref class CryptoAlgorithm abstract { public protected: virtual void Encrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumOutBytes) abstract; virtual void Decrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumOutBytes) abstract; //        }; 


To implement your own cryptographic algorithm, you need to create a managed class-successor ManagedWrapper :: CryptoAlgorithm and override the virtual methods Encrypt and Decrypt. However, these abstract methods are not enough to override the virtual methods NativeLib :: CryptoAlgorithm Encrypt and Decrypt. Virtual methods of the native class, in our case, NativeLib :: CryptoAlgorithm, can be redefined only in the native class descendant. Therefore, you need to create a native class that inherits from NativeLib :: CryptoAlgorithm and overrides the required virtual methods:



 class CryptoAlgorithmProxy : public NativeLib::CryptoAlgorithm { public: virtual void Encrypt( const unsigned char* pData, int nNumInBytes, unsigned char* pBuffer, int nBufferLen, int& nNumOutBytes); virtual void Decrypt( const unsigned char* pData, int nNumInBytes, unsigned char* pBuffer, int nBufferLen, int& nNumOutBytes); //        }; 


This class is called CryptoAlgorithmProxy because it serves as an intermediary for the managed class that implements Encrypt and Decrypt. Its implementation of virtual methods should call equivalent virtual methods of the class ManagedWrapper :: CryptoAlgorithm. For this, CryptoAlgorithmProxy needs an instance handle of the class ManagedWrapper :: CryptoAlgorithm. It can be passed as a constructor parameter. To save a handle, you need a gcroot template. (Since CryptoAlgorithmProxy is a native class, it cannot contain descriptor type fields.)



 class CryptoAlgorithmProxy : public NativeLib::CryptoAlgorithm { gcroot<CryptoAlgorithm^> target; public: CryptoAlgorithmProxy(CryptoAlgorithm^ target) : target(target) {} // Encrypt  Decrypt    }; 


Instead of serving as a wrapper for the native abstract class CryptoAlgorithm, the managed class serves as a wrapper for a particular inheritor of CryptoAlgorithmProxy. The following code shows how to do this:



 public ref class CryptoAlgorithm abstract : INativeCryptoAlgorithm { CryptoAlgorithmProxy* pWrappedObject; public: CryptoAlgorithm() { pWrappedObject = new CryptoAlgorithmProxy(this); if (!pWrappedObject) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } ~CryptoAlgorithm() { delete pWrappedObject; } internal: [CLSCompliant(false)] virtual NativeLib::CryptoAlgorithm& GetWrappedObject() = INativeCryptoAlgorithm::GetWrappedObject { return *pWrappedObject; } public protected: virtual void Encrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumEncryptedBytes) abstract; virtual void Decrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumEncryptedBytes) abstract; }; 


As mentioned earlier, the CryptoAlgorithmProxy class must implement virtual methods in such a way that the control is passed to equivalent ManagedWrapper :: CryptoAlgorithm methods. The following code shows how CryptoAlgorithmProxy :: Encrypt calls ManagedWrapper :: CryptoAlgorithm :: Encrypt:



 void CryptoAlgorithmProxy::Encrypt( const unsigned char* pData, int nDataLength, unsigned char* pBuffer, int nBufferLength, int& nNumOutBytes) { array<unsigned char>^ data = gcnew array<unsigned char>(nDataLength); Marshal::Copy(IntPtr(const_cast<unsigned char*>(pData)), data, 0, nDataLength); array<unsigned char>^ buffer = gcnew array<unsigned char>(nBufferLength); target->Encrypt(data, buffer, nNumOutBytes); Marshal::Copy(buffer, 0, IntPtr(pBuffer), nBufferLength); } 




General recommendations



In addition to the specific steps outlined earlier, you should also consider the general recommendations for creating managed wrappers.



Simplify wrappers from the start



As can be seen from the previous sections, creating wrappers for class hierarchies can be very time-consuming. Sometimes you need to create wrappers for C ++ classes in such a way that managed classes can override their virtual methods, but often such an implementation does not have practical meaning. Determining the required functionality is the key to simplifying the task.



No need to reinvent the wheel. Before creating a wrapper for some library, make sure that the FCL does not contain a ready-made class with the required methods. FCL has more to offer than it seems at first glance. For example, BCL already contains quite a lot of encryption algorithms. They are in the System :: Security :: Cryptography namespace. If the encryption algorithm you need is already in FCL, you do not need to re-create a wrapper for it. If FCL does not contain the implementation of the algorithm for which you want to create a wrapper, but the application is not tied to the algorithm implemented in the native API, then it is usually preferable to use one of the standard FCL algorithms.



Consider the .NET philosophy



CLS, , , .NET. [ 5 .] :





In addition to the capabilities of the type system, also consider the capabilities of FCL. Given that FCL implements security-related algorithms, consider the interchangeability of your algorithms and FCL algorithms. To do this, you have to accept the design adopted in FCL and inherit your classes from the abstract base class SymmetricAlgorithm and implement the ICryptoTransform interface from the System :: Security :: Cryptography namespace.



Adapting to FCL design generally simplifies the wrapper library from the point of view of the library consumer. The complexity of this approach depends on the design of the native API and the design of the FCL types that you want to support. Whether or not this additional effort is acceptable is determined separately in each case. In this example, it can be assumed that the security algorithm is used only in one specific case and, therefore, it is not worth it to integrate it with FCL.



If the library for which you are creating a mapping manages tabular data, consider the System :: Data :: DataTable and System :: Data :: DataSet classes from the FCL part called ADO.NET. Although the consideration of these types is beyond the scope of this text, they deserve mention because of their applicability in the creation of wrappers.



DataTable DataSet , .NET. XML , .NET Remoting -, Windows Forms, Windows Presentation Foundation (WPF) ADO.NET. diffgram, , , .



Conclusion



. , . -- . , , . CLS, FCL .NET .



, C++. , , . .NET, — , CLS. , .



(, , ). — [ 11 ].



- 11
11 IDisposable, , (ThreadAbortException, StackOverflowException ..) SafeHandle. , , , .

Source: https://habr.com/ru/post/318224/



All Articles