Chapter 3: Recommendations for Managed and Unmanaged Code Interoperability

 

Preface
Chapter 1: The "Longhorn" Opportunity
Chapter 2: Preparing for "Longhorn"

Chapter 3: Recommendations for Managed and Unmanaged Code Interoperability

Karsten Januszewski
Microsoft Corporation

December 2003
Sample updated June 2004

Applies to:
   Longhorn Community Technical Preview releases (both PDC 2003 and WinHEC 2004)
   Note: The downloadable sample code is based only on the Longhorn Community Technical Preview, WinHEC 2004 Build (Build 4074)

Contents of Chapter 3

Introduction to Interop
Recommendations on Interoperability Strategy
Specific Recommendations on Using C++ for Interop
Specific Recommendations on Exposing Managed APIs to COM
Specific Recommendations on Using P/invoke
Performance
Under the Hood: Crossing the Interop Boundary

The preceding chapter put forward the recommendation that moving to managed code can provide immediate benefits as well as make a long term investment that aligns with the technology foundation of "Longhorn" moving forward. With this in mind, many organizations have a large investment in an existing unmanaged code base such as MFC or COM. Thus, the interoperability between .NET and COM is critical.

Since the introduction of managed code with version 1.0 of the .NET Framework, Microsoft has provided a path to interoperability through four technologies: COM Callable Wrappers (CCW), Runtime Callable Wrappers (RCW), Platform Invoke (P/Invoke) and directly in C++ (using the Managed Extensions). Much work has already been published on the options for interoperating between unmanaged and managed code; the Microsoft .NET/COM Migration and Interoperability Patterns and Practices Guide, from October 2001, contains prescriptive guidance on proceeding with interoperability and should be the first document consulted when considering how to interoperate between COM and the .NET Framework.

Building on that guide, the Developer's Guide to Migration and Interoperability in "Longhorn" is an expanded set of recommendations that takes into account features of the .NET Framework as well as a drill down into some of the subtleties one may encounter when attempting to interoperate between managed and unmanaged code.

This expanded guide assumes that the reader is already familiar with the four technologies for COM and .NET interoperability. The first section of the guide provides an introduction to the technologies available for interoperability and concrete recommendations for when to use these different technologies, given different scenarios. The next three sections then provide low-level recommendations on each of the given technologies (C++, COM Interop, P/Invoke) based on the latest engineering practices at Microsoft.

Introduction to Interop

The common language runtime (CLR) enables interaction of managed code with COM components, COM+ services, Win32 APIs, and other types of unmanaged code. Data types, error-handling mechanisms, creation and destruction rules, and design guidelines vary between managed and unmanaged object models. To simplify interoperation between managed and unmanaged code and to ease migration, the CLR interoperability layer conceals the differences in these object models from both clients and servers.

Interoperability is bi-directional, so it's possible to:

  • Use unmanaged APIs from managed code. This can be done for both flat APIs (static DLL exports, such as the traditional Win32 API exposed from DLLs like kernel32.dll or user32.dll) and COM APIs (object models such as the ones exposed by Word, Excel, Explorer, ADO, and so on).
  • Expose managed APIs to unmanaged code. Examples of doing this would be a plug-in for a COM-based application like Windows Media Player, or a managed Windows Forms control embedded on an MFC form.

Three complementary technologies enable these managed/unmanaged interactions:

  • C++, a managed C++ feature which enables flat APIs and COM APIs to be used in a mixed mode image directly from within the same project, as they have always been used. This is the most powerful interoperability technology.
  • P/Invoke, which enables calling any function in any managed language as long as its signature is re-declared in managed source code. This is similar to the Declare functionality in Visual Basic 6.
  • COM interoperability, which enables using COM components in any managed language in a manner similar to using normal managed components, and vice versa. COM interoperability is comprised of core services provided by the CLR, plus some tools and APIs in the System.Runtime.InteropServices namespace.

Security Considerations

The common language runtime ships with a security system that regulates access to protected resources based on information about the origin of an assembly. (For more information, see .NET Framework Security.) Calling unmanaged code presents a major security risk, since unmanaged code could manipulate any state of any managed application in the CLR process or call resources in unmanaged code directly without being subject to any Code Access Security permission checks.

For that reason, any transition into unmanaged code is regarded as a highly protected operation and is protected with a security check for the unmanaged code permission that ensures that the assembly containing the unmanaged code transition, as well as all assemblies calling into it, has the right to actually invoke unmanaged code.

There are some interoperability scenarios where full security checks are not necessary and would unduly limit the performance or reach of the component. For instance, if a resource exposed from unmanaged code has no security relevance (system time, window coordinates, and so on), or the resource is only used internally in the assembly and is not exposed publicly to arbitrary callers. In such cases the full security check for the unmanaged code permission against all callers into the respective API can be suppressed. Note that this presumes a careful security review in which it is determined that no partially trusted code could exploit such APIs.

Reliability

Managed code is designed to be more reliable and robust than unmanaged code. One example is garbage collection, which takes care of freeing unused memory and prevents any memory leaks. Another example is managed type safety, which prevents buffer overrun mistakes, among other things.

When you use any type of interoperability technology, your code might not be as reliable or robust as pure managed code. For example, you might need to allocate unmanaged memory manually and not forget to free it when you are done with it.

Writing any non-trivial interoperability code requires same attention to reliability and robustness as writing unmanaged code. Even when all interoperability code is written correctly, your system will only be as reliable as its unmanaged parts.

Recommendations on Interoperability Strategy

While there are many considerations to determining what interoperability strategy best suits your needs, the following are some high level recommendations.

Consuming DLLs with Static Entry Points

There are two ways to use DLLs with static entry points from managed code: through p/invoke (available in all managed languages) or directly in C++ (#include and linking with the import library).

To create a managed wrapper for the native DLLs with static entry points, the recommended way is to use C++. Managed types implemented in C++ can be usable by any other .NET languages. Unlike other .NET Framework languages, C++ can use original type declarations and function signatures (in the form of the C header files) exported by the DLL and maintaining full type safety. Other languages require redeclaration of the function prototype when declaring the DllImport statement for p/invoke.

For just accessing a few unmanaged methods from other non-C++ .NET Framework languages, the recommended way is to use p/invoke.

Consuming COM Components

There are two ways to use COM components from managed code: through COM interop (available in all managed languages) or through C++. If COM component is IDL based, the recommended way is to use C++ to wrap it, and expose a managed-only facade that is usable by any .NET languages.

If the COM component is OLE Automation compatible, the easiest way is to use COM interop to wrap it. It is recommended to use primary interop assemblies (PIAs) as a managed description for any COM type. If PIA is not available and COM types won't be exposed publicly, managed descriptions can be produced by TlbImp, a tool available in .NET Framework. In some cases when COM object is IDL based, TlbImp can't produce the correct managed definitions. In such cases, using C++ is recommended. If the COM object is OLE Automation compatible, TlbImp is able to produce correct descriptions.

Consuming Managed APIs from Unmanged Code

If the unmanaged code is written in C++, the recommendation is recompile all or part of the code into MSIL (with the /CLR compiler switch) to access the managed APIs. If using C++ is not an option, the recommendation is to use COM Callable Wrappers through using the TlbExp tool that ships with the .NET Framework SDK.

Specific Scenarios for C++

There are many scenarios when is clearly better to use the managed interop capability of C++ directly.

  • The unmanaged implementation and managed API need to be in the same DLL. Only C++ supports mixing unmanaged and managed images in the same DLL. This could be important for performance for example, when is absolutely necessary to minimize number of DLLs loaded.

  • Consuming DLLs with static entry points: calling methods that take complex or nested data structures as parameters or wrapping a very large library. It is hard to redefine method signatures and all its parameter types in managed code. For example, unions and variable length structures as parameters are complex to redefine since there is no similar concept in managed world.

  • Consuming native C++ libraries and DLLs that export C++ types: the C++ type system and method name mangling are virtually impossible to be expressed by p/invoke.

  • Compile time type check is crucial because the native DLL is changing or the COM object is changing and TlbImp can't produce the correct wrapper. It is hard to keep managed code in sync with unmanaged API if the unmanaged API is going through major changes in parallel with development of managed wrappers.

  • It's critical to handle creation, lifetime, and marshaling explicitly for unmanaged object. For example, when unmanaged object doesn't comply with COM rules and requires special handling (non-Iunknown-based, for example) or when unmanaged API is non-COM MFC-based class library.

  • The performance hit of making interop transition is unacceptable. For example, fixed cost of making p/invoke (marshalling) or COM interop (marshalling and extra indirection through the CCW) call might be too high for chatty interfaces.

    Again when writing class libraries or any type of application where the transition call can be made from multiple threads, the performance gain must be weighed against the complexity of getting the apartment and context marshaling correct.

Advantages of Using C++

  • Full control over every aspect of managed/unmanaged transition.
  • Compile time type safety checking of both unmanaged and managed side; build robustness.
  • Full control over parameter marshalling costs. For example, you can amortize the cost of marshalling by referencing native data structures directly in managed types, reusing an instance of a native type across multiple method calls.
  • C++ managed types can contain native data structures, and implement methods that have native parameters. This can greatly simplify the implementation of wrapper classes that make use of complex data structures (for example, ACLs).
  • Just include header file for accessing a C or C++ DLL API; no need to redefine data structures, methods or interface definitions.
  • No need to have separate interop assemblies.
  • Full control of COM object lifetime.

Advantages of Using P/invoke and COM Interop

There are number of very common tasks that are handled automatically by interop layer:

  • Type marshaling. For example, managed Unicode strings are marshaled to char* or BSTR based on the context and attributes, depending on the target platform strings are marshaled as ANSI or Unicode automatically, BestFit mapping can be controlled explicitly to avoid potential security problems, and so on.
  • Object lifetime. No need for reference counting because the runtime keeps track of it. Note that control of the lifetime of the object is lost such that an early release cannot be invoked.
  • Call marshaling. No need to understand all threading rules regarding COM apartments and COM+ unmanaged contexts. The interop layer will marshal the calls to the correct destination apartment/context, or it will marshal the proxy to the correct calling apartment/context.
  • Error handling: interop converts error HRESULTs to the closest managed exception, no need for checks after every call.
  • Possible to produced code is 32/64 bit agnostic.

There are some parts of p/invoke and COM interop that can be fine tuned for performance or correctness, if required.

  • P/invoke signatures or COM method signatures in interop assemblies can be changed to get better performance in some specific cases. Sometimes it's better to use no-cost IntPtr type and do marshaling manually instead relying on default marshaling behavior. For example, if an unmanaged API returns a long array of strings and you know that you'll need just a few elements, it might be better to declare the return parameter as IntPtr and access unmanaged array directly then let interop marshaler marshal the whole array back and perform conversion on every element.

    There are a lot of methods available on the Marshal class that provide ways for allocating and accessing unmanaged memory directly and for converting from/to unmanaged/managed types. In combination with using IntPtr type, you can have a full control over type marshaling if needed.

    Note that when manipulating unmanaged memory directly through the Marshal class your code still stays verifiable.

  • P/invoke signatures can be made type safe. For example, if unmanaged API has void* parameters and you know that you'll always pass just strings for it; you can redefine it to String parameter and catch any incorrect usage during compile time.

  • It's possible to control COM objects lifetime explicitly by calling Marshal.ReleaseComObject method. Note that calling this method could be dangerous because it disconnects the RCW from the underlying COM object. Any other managed object that holds a reference to this RCW and tries to call a method after this API is called will get an exception.

Specific Recommendations on Using C++ for Interop

This recommendation applies to all managed code written in C++ and accessing COM objects directly.

Note Use /EHsc switch on C++ compiler when using these templates to specify the synchronous exception handling model. C++ compiles by default without exception-aware stack unwind semantics, which means that in the presence of exceptions, stack destructors will not run (and you'll leak memory if you're counting on smart objects to clean up for you). If you compile /EHsc, it puts the destructor calls inside finally blocks.

Visual Studio code-named "Whidbey" C++ will change to make /EHsc the default.

Storing COM Objects

There are a number of ways to store COM objects depending on their complexity. The following are a few of the common scenarios.

Storing a simple COM object:

  • DO use com_handle for storing unmanaged interface pointers as fields in managed structures and classes.
  • DO use com_handle for passing unmanaged interface pointers as parameters of managed methods.
public __gc class CFoo
{
   public:
      CFoo()
      {
         m_comHandle.create_instance( CLSID_CBAR, NULL, 
CLSCTX_ALL );
      }

      int DoSomething()
      {
         com_ptr<IBar> spBar(m_comHandle);
         return spBar->DoSomething();
      }
   private:
      com_handle m_comHandle;
};

Storing a COM object with special marshaling requirements:

  • DO use com_handle_context_bound when an unmanaged interface pointer is not marshalable or there is no proxy/stub registered.
  • DO use com_handle_context_bound when you want avoid cross-apartment or cross-context marshalling for performance reasons.
  • DON'T use com_handle_context_bound unless absolutely necessary. It will make any managed API that relies on it unnatural to use and will raise exceptions, which managed users wouldn't expect.
public __gc class CFoo
{
   public:
      CFoo()
      {
         m_comHandle.create_instance(CLSID_CNONMARSHALABLE, NULL, CLSCTX_ALL); 

      }

      int DoSomething()
      {
         com_ptr<INonMarshalable> spNonMarshalable(m_comHandle);
         return spNonMarshalable->DoSomething();
      }
   private:
      com_handle_context_bound m_comHandle;
};

Storing a COM object with special release semantics:

  • DO use com_handle_disposable when an unmanaged COM object holds on to precious resources that require aggressive releasing. Always call Dispose() explicitly when you are finished.
  • DO use com_handle_disposable when an unmanaged COM object's semantics require releasing in order to work properly (for example, you can't get a new instance unless the old one is released). Always call Dispose() explicitly when you are finished.
public __gc class CFoo : IDispose
{
   public:
      CFoo()
      {
         m_disposableHandle.create_instance(CLSID_CEXPENSIVERESOURCE, 
NULL, CLSCTX_ALL);
      }
      int DoSomething()
      {
         com_ptr<IExpensiveResource> 
spExpensiveResource(m_disposableHandle);
         return spExpensiveResource->DoSomething();
      }
      Dispose()
      {
         m_disposableHandle.Dispose();
      }

   private:
   com_handle_disposable m_disposableHandle;
};

Storing a COM object with special marshaling requirements AND with special releasing semantics:

  • DO use com_handle_context_bound_disposable when performance is critical and you want avoid cross-apartment or cross-context marshalling AND when an unmanaged COM object holds on to precious resources that require aggressive releasing or when an unmanaged COM object's semantics require releasing in order to work properly.
  • DO use com_handle_context_bound_disposable an unmanaged interface pointer is not marshalable or there is no proxy/stub registered. AND when unmanaged COM object holds on to precious resources that require aggressive releasing or when an unmanaged COM object's semantics require releasing in order to work properly.
  • DON'T use com_handle_context_bound_disposable unless absolutely necessary. It will make any managed API that relies on it unnatural to use and will raise exceptions, which managed users wouldn't expect.
public __gc class CFoo : IDispose
{
   public:
      CFoo()
      {
         disposableHandle.create_instance(CLSID_CHIGHMAINTENANCE, NULL, CLSCTX_ALL);
      }
      int DoSomething()
      {
         com_ptr<IHighMaintenance> spHighMaintenance(m_disposableHandle);
         return spHighMaintenance->DoSomething();
      }
      Dispose()
      {
         disposableHandle.Dispose();
      }
   private:
      com_handle_context_bound_disposable m_disposableHandle;
};

Working with COM Objects

Making calls on a COM object stored in one of the wrappers:

  • DO use com_ptr to extract an interface pointer from the wrapper. This com_ptr can be used to make calls on the interface.
  • DO use com_ptr locally.
  • DON'T store them as fields in managed structures and classes.
  • DON'T use raw interface pointers directly. Instead always manipulate them through com_ptr.
public __gc class CFoo
{
   public:
      CallMeth()
      {
         com_ptr<IBar> spBar(comHandle);
         spBar->Meth();
      }

   private:
      com_handle m_comHandle;
};

Passing COM objects as arguments to calls out to unmanaged code:

  • DO use com_ptr to extract an interface pointer from the wrapper. This com_ptr can be used as an argument directly.
  • DON'T use raw interface pointers directly as arguments.
interface IFoo
{
   HRESULT DoSomething(IBar *pBar, [out, retval] int *pRet);
};

public __gc class CFoo
{
   public:
      CFoo()
      {
         m_comHandle.create_instance( CLSID_CFOO, NULL, 
CLSCTX_ALL );      }

      int DoSomething()
      {
         int ret = 0;

         com_ptr<IBar> spBar;
         spBar.create_instance(CLSID_CBAR); 

         com_ptr<IFoo> spFoo(m_comHandle);

         HRESULT hr = spFoo->DoSomething(spBar, &ret);
         If (FAILED(hr))
            Marshal::ThrowExceptionForHR(hr);
         return ret;
      }

   private:
      com_handle m_comHandle;
};

Implementing a callback interface:

  • DO implement an unmanaged class that implements a callback interface (since unmanaged interfaces cannot be directly implemented on a managed class). This class can have a field that is gcroot to a managed class (for forwarding calls to it, for example).
  • DO use gcroot to store a reference to managed class inside the unmanaged callback object.
  • DO make sure that your unmanaged sink methods catch any exceptions that throw any return failure HRESULTs when appropriate.
[object, 
 uuid(8ED84E9C-ECAF-4B3E-A052-806D194F7919), 
 pointer_default(unique)]
interface ICallback : IUnknown
{
   HRESULT SomethingHappened(int i);
};

class ATL_NO_VTABLE CSink : 
   public CComObjectRootEx<CComMultiThreadModel>,
   public ICallback
{
   public:

BEGIN_COM_MAP(CSink)
       COM_INTERFACE_ENTRY(ICallback)
END_COM_MAP()


      CSink()
      {
      }

      void SetManagedSink( CManagedSink* pManagedSink ) 
      {
         m_hndManagedSink = pManagedSink; 
      }

      STDMETHOD( SomethingHappened )( int i )
      {
         try
         {
           m_hndManagedSink->SomethingHappened( i );
            return S_OK;
         }
         catch(Exception *e)
         {
            return Marshal::GetHRForException(e);
         }
      }

   private:
      gcroot<CManagedSink*> m_hndManagedSink;
}

public __gc class CManagedSink
{
   public:
      void SomethingHappened( int i )
      {
         LibUtils::DoWeCare();
      }
};

Specific Recommendations on Exposing Managed APIs to COM

As discussed above, the recommendation for interoperability between managed and unmanaged code is through the usage of C++. Because of the built-in support C++ provides for intermixing native code and unmanaged code, this is the most efficient and effective way to interoperate. However, in cases when using C++ is not an option, public managed classes can be exposed to unmanaged clients through COM interoperability. The COM interoperability layer will handle all the COM plumbing and marshaling. For example, when exposed to COM, every managed class appears to implement IUnknown, IDispatch, ISupportErrorInfo, as well as other standard COM interfaces.

While it is possible to expose managed APIs through COM interoperability, the managed object model and the COM object model different significantly. Consequently, exposing managed APIs through COM should always be an explicit design decision when the usage of C++ is not an option. The reason is because there are some features available in managed code that have no equivalent in COM and thus won't be usable from COM clients. Because of this, there can be difficulties in exposing the functionality of the managed API to COM with full equivalence and parity.

Detailed descriptions below list some of the differences between the managed code and the COM object model as well as recommendations on how to handle these differences.

Type Marshaling

When a managed type is passed to or from COM, the interop marshaler transforms it to an appropriate unmanaged form. For example, the managed type System.String is marshaled as BSTR by default. Sometimes, depending on a COM client, the default marshaling is not correct. For example, a managed string should be marshaled as char* instead as BSTR. In such cases, apply the MarshalAsAttribute on method parameters to instruct the marshaler what kind of transformation should be done.

**Recommendation   **Make sure that managed parameters are exposed as expected COM types.

Class Interfaces

There are three ways to expose a managed class to COM based on the ClassInterfaceAttribute:

  • Default AutoDispatch: expose just an empty IDispatch interface.
  • AutoDual: expose all class methods as well as all base class methods in a dual interface.
  • None: expose just explicitly implemented interfaces as a dual interfaces.

The third method is recommended since it requires managed class developers to explicitly express their contract for COM consumers and more importantly, exposes any versioning problems. If a managed class developer wants to update an interface in a new version and adds a method, a new interface will also need to be created instead of just updating the old one, since interfaces shouldn't be changed in any way.

Unfortunately, the first and second ways for exposing a managed class to COM can hide potential versioning problem from managed class developers.

In the case when a managed class is exposed through an AutoDual interface, every time a new method is added, the ordering of the methods can be changed, which will in turn change the generated v-table layout and break existing COM clients. An even subtler change can occur when a developer adds a new method on a base class (which doesn't need to be exposed to COM at all). This will cause COM clients of a derived class to be broken.

For the case when a managed class is exposed through AutoDual or AutoDispatch interfaces and has overloaded methods, see "Overloading" immediately below.

**Recommendation   **Make sure that managed parameters are exposed as expected COM types.

Overloading

Managed code allows overloading of functions, yet COM does not support this. In order to make overloaded methods accessible to COM clients, the Type Library Exporter (TlbExp.exe) tool and the runtime interop layer will rename them. For example:

DoSomething()
DoSomething(int i)

will be exposed as

DoSomething()
DoSomething_2(int i)

First, the method renaming might not be what COM clients would expect. Thus, it is worth considering using different names instead of overloading in cases where it is important to consider COM clients.

Second, it is important to realize that method overloading introduces potential versioning problems. Every time a new overloaded method is added to a managed class exposed through AutoDual or AutoDispatch interface, the ordering of the methods can be changed, which in turn changes generated method names and breaks existing COM clients. An even subtler change can occur when a developer adds a new overload on a base class (which doesn't need to be exposed to COM at all). This will cause COM clients of a derived class to be broken. For example, the following three managed methods

DoSomething()
DoSomething(int n)
DoSomething(string s)

will become

DoSomething()
DoSomething_2(int n)
DoSomething_3(string s)

If a fourth DoSomething(double d) is added, the methods can very well look like this

DoSomething()
DoSomething_2(double d)   // not the same method any more!
DoSomething_3(int n)      // same problem   
DoSomething_4(string s)   // same problem.

Since managed clients are not affected by this renaming they will continue to work, yet the unmanaged clients using the last three methods will not.

**Recommendation   **Avoid method overloading especially on classes that will be versioned.

Parameterized Constructors

The unmanaged clients cannot call the parameterized constructors since there is no way to pass parameters during COM object activation. In fact, constructors are not exposed to COM at all.

**Recommendation   **Make a default constructor for every class exposed to COM.

Static Members

Static members are available in the managed world, but unmanaged clients cannot make use of them. In fact the type library does not expose static member functions or static fields.

**Recommendation   **Don't rely on functionality in static methods.

Error Management

Managed code uses structured exception handling as opposed to COM, which relies on HRESULTs. When creating managed code that will be consumed by COM, the exceptions that are created need to have an associated unique error code so that COM can detect the exception. Otherwise, COM will be unable to differentiate between the various exceptions. If unique error codes are not set, the exceptions will appear to COM as E_FAIL, making them indistinguishable.

The error codes need to be defined in the exception class. This is usually done in the constructor of the exception. Note that for exceptions that are defined inside the runtime this code is already set to the closest available HRESULT.

**Recommendation   **Define HRESULT mapping for every custom exception.

Inheritance

In terms of the interface inheritance, the hierarchy is lost as one moves from the managed to the unmanaged world. The runtime flattens out the interface inheritance, thereby exposing the methods that are defined in the interface itself plus all methods defined in the base interface(s). All managed interfaces exposed to COM will inherit only from IUnknown or IDispatch. This is due to the fact that managed interfaces support multiple inheritance and COM interfaces don't.

For example in the managed world:

   public interface IWork
   {
      void Method1();
   }
   public interface IWork2 : IWork
   {
      void Method2();
   }

In the COM world it becomes:

   public interface IWork : IDispatch
   {
      void Method1();
   }
   public interface IWork2 : IDispatch
   {
      void Method1();
      void Method2();
   }

Size Issues

There are some size changes that have taken place in .NET Framework. For instance, a long is now 64 bits rather than 32 bits as well as an integer is 32 bits rather than 16 bits. A char refers to a Unicode character meaning it represents 16 bits rather than the ANSI 8 bits. The changes predominantly affect Visual Basic developers.

**Recommendation   **Be aware of type size differences.

Another effect of size differences is that unmanaged code can only handle 32-bit sized enums as opposed to managed code which can handle different sized enums.

**Recommendation   **When declaring an enumeration, limit enum type to 32 bits or less.

32/64 Bit Platform Differences

By default, TlbExp generates 32-bit platform-specific type libraries that cannot be used on 64-bit platforms. The list of cases when this happens is:

  • A SafeHandle parameter is exposed as an unmanaged 32-bit long type.
  • An IntPtr is exposed as an unmanaged 32-bit long type.
  • Custom marshaled strings and arrays are exposed as unmanaged 32-bit long types.
  • Delegates marshaled as unmanaged function pointers are exposed as unmanaged 32-bit int types.

In all of these cases, managed types are exposed as 32-bit values where in fact they should be exposed as platform-independent pointers such as void*. Unfortunately, there is no way to express platform-agnostic types such as void* in a type library. For this reason, a new switch on TlbExp.exe is available to export 64-bit versions of type libraries.

**Recommendation   **Produce both 32- and 64-bit versions of type libraries if managed API will be used on both platforms.

C-Style Arrays

By default, when a managed array is used as an in parameter on a method exposed to COM clients, it will be marshaled as SAFEARRAY. SAFEARRAYs are self-describing, so the runtime doesn't need any additional information to marshal it to a managed array once the method is called. For VB developers, having a SAFEARRAY is perfectly acceptable. For C/C++ developers C-style arrays are a more natural choice. If COM clients will be passing C-style arrays, two things need to be done. First, array parameters need to be decorated with the MarshalAs(UnmanagedType.LPArray) attribute to indicate a C-style array. Second, since C-style arrays are not self describing, the interop marshaler needs to be informed about array size and this size needs to be provided in an additional method parameter.

The following code shows what needs to be done:

public interface ITest
{
// method designed to be called from COM clients, size parameter is 
used only by    
// marshaler – in this case the size parameter is index 1, meaning that the
// parameter that holds the array size is on position 1 in the method signature
void Test([MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] int[] 
data, int size); 
}
 

**Recommendation   **When using C-style arrays as in parameters, provide additional parameter for a caller to specify array size. It means that parameter that holds array size is on position 1 in the method signature (positions are 0-based).

Events

With respect to events, COM uses connection points, whereas managed code uses delegates. Although there is a difference, when using COM objects from managed clients, TlbImp takes care of bridging the connection points to delegates. In the other direction, when exposing a managed object to COM, some explicit actions need to be taken. Before exposing a managed class that sources events to COM, it is necessary to provide an interface that lists all the managed events just for COM clients. This interface needs to be marked with the COMSourceInterfacesAttribute.

**Recommendation   **When exposing events to COM make sure that all the required parts are in place (special interface, correct decoration with attributes, and so on).

Apartments

Managed code has no notion of apartments. All managed objects are registered as being of type Both for threading model setting. Depending on the apartment setting of a thread that is creating a managed object, the managed object will be created in a single-threaded apartment (STA) or a multi-threaded apartment (MTA). Note that there is no way to restrict apartment preference explicitly on a managed object except by using EnterpriseServices.

**Recommendation   **Be aware that your managed object can be created in both apartment types, STA, and MTA.

Signing

When a managed class or interface is exposed to COM, GUIDs for them are automatically generated, unless they are explicitly specified in GuidAttribute. The algorithm used to automatically generate these GUIDs will use the full assembly name as one of parameters. If the assembly is signed and has a strong name, then GUIDs generated will have unique values. Otherwise, it is quite possible that two completely different assemblies with the same file names and containing the same interface will get the same IDs.

**Recommendation   **Sign all assemblies exposed to COM in order to insure unique GUID generation (even if these assemblies won't be installed in the global assembly cache).

Specific Recommendations on Using P/invoke

In order to call any unmanaged method through p/invoke, you need to give the runtime managed equivalent of unmanaged method's signature. Basically, you need to redefine method signature plus all its parameter types (if they are not simple types like String but structures, for example). There are many DllImport attribute fields available for fine tuning call like calling convention, best fit mapping, and so on.

Below are recommendations and some important aspects to be aware of when using p/invoke.

Lifetime Issues

If you pass x.Ptr to some p/invoke call where x is a managed class instance and Ptr is some field on this class, there might be some issues with x's lifetime. If there are no further uses of the object x, then the GC is free to collect that object while you are in the method. In other words, the runtime can collect the "this" of a method while you are executing code in that method. As part of the collection, the Finalize method will be called and you will clean up the resource associated with x.Ptr.

You can use GC.KeepAlive(x) after p/invoke call to make the just-in-time (JIT) compiler think that you still need "x" for some reason. Therefore the JIT compiler keeps reporting it until you reach the call to KeepAlive. A well-written class should never expose a public member like x.Ptr. If, instead, you can keep Ptr encapsulated inside the object, then only your class needs to call GC.KeepAlive. Once you expose a property like Ptr, all your clients are now responsible for annotating their own code with calls to GC.KeepAlive. Below is a sample of how to call GC.KeepAlive from your class.

public class Foo {
public IntPtr handle; 
. . .
}

public class Bar {
public void UseHandle() {
            Foo myFoo;
            NativeMethods.UseHandle(myFoo.handle);
GC.KeepAlive(myFoo);
}

In Visual Studio code-named "Whidbey," SafeHandle class solves this problem (and several others) for you automatically. For the cases where it meets your needs, it is a much better approach than using GC.KeepAlive.

Dynamic P/invoke Issues

DllImport declaration requires an unmanaged DLL name. Because of this it's not possible to have dynamic p/invoke, that is, a DLL name can't be obtained during runtime. It has to be known during compile time. There is one small exception to this rule. If a DLL file name is known during compile time but the full path isn't, it's possible to write a DllImport declaration using just a file name, p/invoke to the LoadLibrary API to load the DLL during runtime, and then call the method. In such case, runtime will use the already preloaded DLL instead trying to load it again.

In Visual Studio code-named "Whidbey," new APIs are available to wrap unmanaged function pointers (returned from LoadLibrary/GetProcAddress, for example) into delegates and call on them. Check the SDK documentation for Marshal.GetFunctionPointerForDelegate() and GetDelegateForFunctionPointer() for more info.

Performance

With every transition from managed code to unmanaged code (and vice versa), there is overhead.

Approximate overhead for p/invoke call: 10 machine instructions (on x86)

Approximate overhead for COM interop call: 50 machine instructions (on x86)

These instruction counts represent the raw overhead; assuming no parameter marshaling is done, and that the SuppressUnmanagedCodeSecurityAttribute is used to prevent the full stack walk for unmanaged code permission.

Based on these characteristics, the following recommendations help you to use interop in the most performant manner:

  • Use chunky versus chatty interfaces. If you control the interface between managed and unmanaged code, make it chunky rather than chatty, to reduce the total number of transitions made.

  • Chatty interfaces are interfaces that make a lot of transitions without doing any significant work on the other side. For example, property setters and getters are chatty. Chunky interfaces are interfaces that make only a few transitions and work done on the other side is significant. For example, a method that opens a database connection and retrieves some data is chunky. Chunky interfaces involve fewer interop transitions, which eliminates overhead.

  • Avoid Unicode/ANSI conversions if possible. Converting strings from Unicode to ANSI and vice versa is an expensive operation. For example, if some strings need to be passed but their content is not important you can declare the parameter as IntPtr and the interop marshaller won't do any conversions.

  • Declare parameters and fields as IntPtr.For high-performance scenarios, declaring parameters and fields as IntPtr can boost performance (at the expense of ease-of-use and maintainability).

  • Sometimes it is faster to do manual marshaling using methods available on the Marshal class then rely on default interop marshaling. For example, if large arrays of strings need to be passed across interop boundary but only few elements will be needed, declaring the array as IntPtr and accessing only those few elements manually will be much faster.

  • Use InAttribute and OutAttribute wisely to reduce unnecessary marshaling. The interop marshaler will use default rules when deciding if some parameter needs to be marshaled in before the call and out after the call. These rules are based on level of indirection and type of parameter. Some of these operations might not be necessary depending on the method's semantics.

  • Use SetLastError=false on p/invoke signatures if you're not going to call Marshal.GetLastWin32Error afterward. Setting SetLastError=true on p/invoke signatures will require some additional work from the interop layer to preserve last error code. Use this only when you rely on this information and will use it after the call is made.

  • Reduce the number of security checks. If (and only if) unmanaged calls are exposed in a non-exploitable fashion, use the SuppressUnmanagedCodeSecurityAttribute to reduce number of security checks.

    Security checks are very important. They will make sure that nobody can misuse your API. Sometimes your API doesn't expose any protected resources/sensitive information or they are well protected. In those situations extensive security checks might introduce unnecessary overhead.

Under the Hood: Crossing the Interop Boundary

Below are three diagrams that walk through exactly what happens during interoperability.

Calling a Flat API: Step by Step

Calling a COM API: Step by Step

Calling a Managed API from COM: Step by Step

Continue to Chapter 4: Recommendations for Win32, ActiveX, and "Longhorn"

© 2003 Microsoft Corporation. All rights reserved.

Microsoft, Win32, Windows, Windows NT, Windows Server, WinFX, and ActiveX are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries.