Chapter 3: .NET Interoperability

As introduced in the “Interoperating with the Existing Code” section in Chapter 1, “Introduction to .NET” of this volume, Microsoft® .NET provides several interoperability mechanisms to interoperate with the unmanaged code, thus facilitating the migration process and preserving the investments in the existing code. This chapter discusses how these interoperability mechanisms can be applied to reuse the existing code with minimal changes. With this knowledge, you can evaluate the available options for migrating your UNIX application to Microsoft Windows® using .NET and choose the best migration approach to carry out the migration exercise. This chapter also provides appropriate examples that you can use as a basis for constructing your new .NET application.

On This Page

.NET Interoperability Mechanisms .NET Interoperability Mechanisms
Marshaling Arguments Marshaling Arguments

.NET Interoperability Mechanisms

The various interoperability mechanisms provided by .NET are:

  • Wrapping unmanaged C++ classes with Managed Extensions for C++.

  • Platform Invocation services (P/Invoke).

  • C++ Interoperability, popularly known as IJW (It Just Works).

  • COM Interop services.

Note COM Interop services are not very useful for migrating from UNIX to .NET, and hence will not be covered at length in this guide.

Wrapping Unmanaged C++ Classes with Managed Extensions for C++

Managed Extensions for C++ supports the interoperation of code written in any .NET Framework-compliant language with the code already existing in C++. ("Unmanaged C++" refers to C++ that is compiled to the assembly language of a processor.) Interoperability is achieved by writing a proxy, or "wrapper," class in Managed Extensions for C++ for an unmanaged C++ class. A wrapper class interoperates with its unmanaged counterpart and serves as a managed proxy for it. It provides an API with a functionality that is similar to the unmanaged class. The API can be called by code written in any managed language.

If the C++ program on UNIX can be compiled on Windows with minimal code changes, then use this method to reuse the existing code in .NET. For example, low-level file access programs on UNIX can be compiled on Windows with a few header file changes. In such a case, consider using this method of interoperation.

The following C++ example code in UNIX writes to the standard output file descriptor. If any errors occur, it writes an error message to the standard error file descriptor. This program can be compiled on Windows by replacing the UNIX header file <unistd.h> with the Windows header file <io.h>; in this case, the technique of wrapping unmanaged classes is an ideal choice for migrating this code to .NET.

UNIX example: Code for writing to the standard output

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
class StandardOutput
{
public:
StandardOutput(){}
void WriteToStandardOutput();
~StandardOutput() {}
};
void StandardOutput :: WriteToStandardOutput()
{
if ((write(1, "Here is some data\n", 18)) != 18)
        write(2, "A write error has occurred on 
file descriptor 1\n",46);
}
int main()
{
    StandardOutput *objSO = new StandardOutput();
    objSO->WriteToStandardOutput();
}

(Source File: U_WrapUnmanagedC++-UAMV4C3.01.cpp)

The following example shows the migrated Microsoft Win32® code for the UNIX example. As you can see, the header <unistd.h> has been replaced with <io.h>.

Windows example: Code for writing to the standard output

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <io.h>
class StandardOutput
{
public:
StandardOutput(){}
void WriteToStandardOutput();
~StandardOutput() {}
};
void StandardOutput :: WriteToStandardOutput()
{
if ((write(1, "Here is some data\n", 18)) != 18)
            write(2, "A write error has occurred on 
file descriptor 1\n",46);
}
int main()
{
    StandardOutput *objSO = new StandardOutput();
    objSO->WriteToStandardOutput();
}

(Source File: W_WrapUnmanagedC++-UAMV4C3.01.cpp)

The following steps explain how the unmanaged class, StandardOutput, can be wrapped using a managed class, MStandardOutput.

To wrap the unmanaged class , StandardOutput , using a managed class , MStandardOutput

  1. Write a managed wrapper class MStandardOutput for the unmanaged class StandardOutput and declare a single member of MStandardOutput for which the type is StandardOutput*. This is achieved by adding the __gc keyword before the MStandardOutput class, which indicates that it is garbage collected and that its lifetime is managed by the CLR.

  2. Define an MStandardOutput constructor for each constructor of StandardOuput. This creates an instance of StandardOutput through the unmanaged new operator by calling the original constructor of StandardOutput class.

  3. If the managed class MStandardOutput holds the only reference to the unmanaged class StandardOutput, define a destructor for MStandardOutput, which calls the delete operator on the member pointer to StandardOutput.

  4. For each remaining method in StandardOutput, declare an identical method that delegates the call to the unmanaged version of the method in StandardOutput, performing any parameter marshaling if required.

The following Managed C++ code sample illustrates how the unmanaged class StandardOutput is wrapped.

Windows example: Code illustrating wrapping of unmanaged classes for writing to the standard output

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
#include <io.h>
class StandardOutput
{
public:
void WriteToStandardOutput();
};
void StandardOutput :: WriteToStandardOutput()
{
    if ((write(1, "Here is some data\n", 18)) != 18)
           write(2, "A write error has occurred on file
descriptor 1\n",46);
}
__gc class MStandardOutput
{
private:
    StandardOutput *ob;
public:
MStandardOutput()
    {
        ob = new StandardOutput();
    }
~MStandardOutput()
    {
        delete(ob);
    }
void MWriteToStandardOutput()
    {
        ob->WriteToStandardOutput();
    }
};
int main()
{
    MStandardOutput *objSO = new MStandardOutput();
    objSO->MWriteToStandardOutput();
}

(Source File: N_WrapUnmanagedC++-UAMV4C3.01.cpp)

Wrapping Technique Considerations

The specific technique that is used for wrapping an unmanaged class depends on the semantics of the class. It may not be necessary to wrap all the member functions or data members of the unmanaged class. Wrap only the selected members of the unmanaged class, such as the members that must be accessed by managed objects.

Before wrapping an unmanaged C++ class, consider its structure and decide which members must be wrapped. Following are some simple guidelines for identifying members that need to be wrapped:

  • If a member function or data member is private, then by design it is not meant to be accessed by other unmanaged classes. That member must not be accessible to managed objects either.

  • Typically, helper functions are used internally by a class and are not designed to be accessed by other classes. These functions too must not be wrapped.

Note More information on wrapping unmanaged classes with Managed Extensions for C++ is available in Part I of the Managed Extensions for C++ Migration Guide at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcmxspec/html/vcmanexmigrationguidepart1_start.asp.

Data Marshaling

When calling unmanaged functions that take arguments, the data must be marshaled to convert managed types to unmanaged types. A typical example is the conversion of the .NET Unicode string to the Win32 ANSI string. For each argument, you must copy the contents of the string from the common language runtime (CLR) heap into the C++ run-time heap and return a pointer to the string. The classes provided as part of the .NET Framework class library enable this. The System::Runtime::InteropServices::Marshal class contains a collection of methods to handle tasks, such as managed to unmanaged type conversions, unmanaged memory allocations, and copying of unmanaged memory blocks. The static methods defined in the Marshal class provide a method to convert between managed and unmanaged data. In general, the methods in the Marshal class return an IntPtr. This is a CLR pointer.

One approach is to use the ToPointer() member function of the IntPtr class. This returns a pointer of type System::Void ** that can be cast to char * as illustrated in the following code example.

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

String __gc* str = S"managed string";
char __nogc* pStr = static_cast<char*>(Marshal::
StringToHGlobalAnsi(str).ToPointer()); 

Platform Invocation Services

Platform Invocation services (also known as P/Invoke), provided by the .NET Framework CLR, enables managed code to call C-style functions in the existing unmanaged DLLs. P/Invoke can simplify customized data marshaling because the marshaling information is provided declaratively in attributes, instead of writing procedural marshaling code.

P/Invoke services first load the DLL containing the function into memory; they then locate the address of the function in the memory, stack up all the marshaled arguments, and transfer control to the unmanaged function. If any exceptions are raised in the unmanaged function, the functions are returned back to the managed caller for resolution. The P/Invoke functionality is also bidirectional and the Win32 API unmanaged functions can call back into the managed code.

Figure 3.1 shows the P/Invoke functionality.

Figure 3.1. P/Invoke services

Figure 3.1. P/Invoke services

P/Invoke with UNIX Code

The following UNIX code sample in C reads characters from the standard input file descriptor and writes that information to the standard output file descriptor. If any input/output (I/O) errors occur, an error message is sent to the standard error file descriptor.

An example code in UNIX for using the standard input and output is shown as follows.

UNIX example: Using standard input and output

#include <unistd.h>
int main()
{
      char buffer[129];
      int num_read;
      num_read = read(0, buffer, 128);
      if (num_read == -1)
            write(2, "A read error has occurred\n", 26);
      if ((write(1,buffer,num_read)) != num_read)
            write(2, "A write error has occurred\n",27);
      exit(0);
}

(Source File: U_PInvoke-UAMV4C3.01.c)

Windows example: Using standard input and output through P/Invoke

#include <io.h>
void ReadandWrite()
{
      char buffer[129];
      int num_read;
      num_read = read(0, buffer, 128);
      if (num_read == -1)
            write(2, "A read error has occurred\n", 26);
      if ((write(1, buffer, num_read)) != num_read)
            write(2, "A write error has occurred\n", 27);
}

(Source File: W_PInvoke-UAMV4C3.01.cpp)

The following steps enable reuse of the existing code in your .NET application:

  • Migrate the UNIX code in the preceding UNIX example to Win32 by changing the header file. The changed code is described in the preceding Windows example.

  • Compile the changed code into a DLL (ReadandWrite.dll).

  • After compiling into a DLL, the code can be accessed from any .NET project by using P/Invoke. The following code example shows how the changed code is accessed from a C# project using the DllImport attribute.

    # using <mscorlib.dll>
    

using namespace System; using namespace System.Runtime.InteropServices; [DllImport("dllFileAccess",CharSet=CharSet::Ansi)] extern void ReadandWrite(); int main() {     ReadandWrite(); }

(Source File: N\_PInvoke-UAMV4C3.01.cpp)
P/Invoke with Win32 API

P/Invoke is also used for calling Win32 functions. A UNIX code that uses the fork() call to create a process can be easily migrated to Win32, using either the CreateProcess() or _spawnlp() calls.

The following code example illustrates the use of P/Invoke to call the Win32 API function _spawnlp() to create a Notepad process. The native function _spawnlp() is defined in msvcrt.dll. The DllImport attribute is used for the declaration of _spawnlp().

.NET example: Calling the Win32 API function _spawnlp through P/Invoke

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

# using <mscorlib.dll>
using namespace System;
using namespace System::Runtime::InteropServices;
[DllImport("msvcrt",CharSet=CharSet::Ansi)]
extern "C" int _spawnlp(int,[MarshalAs(UnmanagedType
::LPStr)] String*,[MarshalAs(UnmanagedType::LPStr)]
String*,int);
int main()
{
    _spawnlp(1,S"notepad",S"notepad",0);
}

(Source File: N_PInvokeWin32API-UAMV4C3.01.cpp)

In the previous example, the CharSet parameter of the DllImport attribute specifies how the managed strings must be marshaled. In this case, they are marshaled to an ANSI string for the native side.

The MarshalAs attribute, located in the System::Runtime::InteropServices namespace, is used to specify the marshaling information for individual arguments on the native side. There are several choices for marshaling a managed String * argument, such as BStr, ANSIBStr, TBStr, LPStr, LPWStr, and LPTStr. The default is LPStr and it can be used to marshal other data types such as arrays.

C++ Interoperability – It Just Works

This technique is basically used to invoke unmanaged code from Managed C++ without using the DLLImport attribute. While migrating the UNIX code to Windows with minimal changes, the migrated code can also be compiled in a .NET environment using the /CLR switch for the compiler. The It Just Works (IJW) interoperability feature allows you to use the unmanaged APIs directly in managed code without having to use the DllImport attribute. This is done by including the header file and linking the import library. However, this feature is available only if the .NET programming language is Managed Extensions for C++.

The following example illustrates how the _spawnlp() function, used in the P/Invoke example, is implemented with IJW.

.NET example: Calling the Win32 API function _spawnlp

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::Runtime::InteropServices;
#include <process.h>
int main()
{
    String *pStr = S"notepad";
char *pChars = (char *)Marshal:StringToHGlobalAnsi
(pStr).ToPointer();
    _spawnlp(1,pChars,pChars,0);
    Marshal::FreeHGlobal(pChars);
}

(Source File: N_IJW-UAMV4C3.01.cpp)

Explicit marshaling APIs return IntPtr types for 32-bit to 64-bit portability. Hence, you must use additional ToPointer() calls as shown in the earlier example.

The IJW mechanism is slightly faster because the IJW stubs do not need to check for the need to pin or copy data items as that is done by the developer. More importantly, if many unmanaged APIs need to be called using the same data, marshaling the APIs once up front and passing the marshaled copy around is more efficient than remarshaling APIs every time. However, you must specify the marshaling explicitly in your code. As the marshaling code is inline, it invades the flow of application logic.

If the migrated application mainly uses unmanaged data types and calls more unmanaged APIs than the .NET Framework APIs, then consider using the IJW feature. If the migrated application mainly uses managed data types and makes only occasional calls to the unmanaged APIs, then consider using P/Invoke with DllImport.

Marshaling Arguments

With P/Invoke, no marshaling is required between blittable types. Blittable types have the same representation in both the managed and the unmanaged world. For example, no marshaling is required between Int32 and int and Double and double.

Marshaling is required for types that do not have the same form, such as char, string, and struct types.

Table 3.1 lists the mappings used by the marshaler for various types.

Table 3.1. Data Type Mapping for Marshaler

Win32 Data Types in wtypes.h

C++

Managed Extensions

CLR

HANDLE

void *

void *

IntPtr, UIntPtr

BYTE

unsigned char

unsigned char

Byte

SHORT

Short

short

Int16

WORD

unsigned short

unsigned short

UInt16

INT

Int

Int

Int32

UINT

unsigned int

unsigned int

UInt32

LONG

Long

Long

Int32

BOOL

Long

Bool

Boolean

DWORD

unsigned long

unsigned long

UInt32

ULONG

unsigned long

unsigned long

UInt32

CHAR

Char

Char

Char

LPSTR

char *

String * [in], StringBuilder * [in, out]

String [in], StringBuilder [in, out]

LPCSTR

const char *

String *

String

LPWSTR

Wchar_t *

String * [in], StringBuilder * [in, out]

String [in], StringBuilder [in, out]

LPCWSTR

const wchar_t *

String *

String

FLOAT

Float

Float

Single

DOUBLE

Double

double

Double

Download

Get the UNIX Custom Application Migration Guide

Update Notifications

Sign up to learn about updates and new releases

Feedback

Send us your comments or suggestions