Interop: Common Issues and Debugging Techniques
This post will describe some of the types of problems that can arise when interoperating between managed and native code using the .Net Compact Framework Version 2. An overview of how the runtime works under the scenes is also given, which may help promote better quality and more efficient code as well as give insight into the cause of problems.
When interoperating with native code, a conversion process is necessary to translate argument data between managed and native code. The .Net Compact Framework supports marshaling many different managed types, both to and from native code. Each managed type that is supported has default marshaling behavior, which may be different depending on the type of call being made (COM interface function call, delegate callback, or PInvoke). You can also override the default marshaling behavior by using the MarshalAs attribute. The runtime will marshal (or convert) data automatically when an interop function call is made, as specified in the managed PInvoke signature, delegate callback signature, or COM interface signature. You can also use the functions in the InteropServices.Marshal class to manually convert data between managed objects and an IntPtr representing the native object. For some examples, see GetIUnknownForObject, GetObjectForIUnknown, PtrToStringBSTR, StringToBSTR, StructureToPtr, or PtrToStructure.
There are a few types of issues that come up when interoperating with native code that involve the marshaling process:
1. A managed NotSupportedException may be thrown, due to an unsupported type being specified as an argument or return value in the managed interop signature. Although almost all parameter types the desktop framework supports are now supported in the .Net Compact Framework, there are a few situations where there is limited support. These exceptions include fields in structures, elements in arrays, and return types. The workaround for these limitations is to use the functions in the InteropServices.Marshal class to manually do the conversion process, and specify an IntPtr as the type in the signature or structure.Interoperating using methods, types, or COM interface classes that are generic is also not supported.The interop log file will specify the particular method causing the issue, which can be especially useful when using COM interfaces. The best way to diagnose this is to isolate the unsupported type by systematically changing parameter types to IntPtr or a known supported type. Once the unsupported type is identified, consider using another similar type or doing the conversion work manually. If the unsupported type is identified to be a structure, removing the MarshalAs specifier for structure fields may solve the issue (assuming you can specify a field type that has the appropriate default marshaling behavior). .NetCF will allow only a few limited cases of using the MarshalAs attribute to change the default marshaling behavior of a field in a structure. You can also consider using IntPtr marshaling using the various functions in the InteropServices.Marshal class.See the resources section for more information about how to determine what the default marshaling behavior is for a particular type.
2. The native function call may not produce the expected results or causes a native exception, due to types being marshaled incorrectly and not matching the specifications of the native function signature.The best way to diagnose these types of issues is to use the signature output in the interop log file and compare how the runtime is marshaling parameters to what is expected by the native function. You can then change the types used in the managed signature or use the MarshalAs attribute to ensure data is marshaled correctly. Keep in mind the default marshaling behavior of a particular type will sometimes be different depending on the type of interop call (PInvoke, Com, Delegate callback). The ref keyword must also be applied correctly to the parameters in the managed interop signature. In the event the signature takes a structure parameter, you should also make sure the structure alignment is specified correctly using the StructLayout attribute.Other common marshaling errors include using a managed 'bool' instead of an 'int' to represent a native Win32 BOOL type. Likewise, an managed 'long' is actually 64 bits and is not equalivant to a Win32 LONG, which is 32 bits.
The .Net Compact Framework has two differences from the desktop framework with regard to default marshaling of types for a PInvoke call, for backward compat reasons.
3. A native exception occurs after the native function has returned or at inconsistent times, due to improper use of runtime memory by the native code.During the marshaling process, the runtime will sometimes place the parameter data inside a temporary buffer or temporarily on the stack. For this reason, it is not always safe for native code to hold a reference to the memory containing marshaled data. Although, in some cases this will work with our current implementation depending on the particular types marshaled, it is recommended that native code always copy data into its own buffer when marshaling any complex types. If you must avoid copying data, use an IntPtr in your managed interop signature. In the event you are passing a pointer associated with managed object data to native code that will live beyond the return of the native function call, be aware that the GC can cause objects to move around. It is recommended that you maintain a pinned reference to the object, by using a GCHandle.When returning from a native function call, the marshaler will free native memory associated with arguments after the back propagation into the managed object has completed. This includes cases where only out-marshaling has occurred as well as cases where the content pointed to has changed while in the native function. The runtime will use CoTaskMemFree (or SysFreeString in the case of BSTRs), and expects any non-null memory returned to have been allocated with CoTaskMemAlloc. When necessary, the runtime will free both the original data allocated during the in-marshaling process, and the new data allocated inside the native function call. To avoid this behavior, you can use IntPtr in the managed interop signature, in which case you are then responsible for cleaning up the memory yourself. The types that are affected by this include, but are not limited to, Formatted Classes, Strings, StringBuilders, and Arrays. You must also use IntPtr marshaling when interacting with native APIs that will return data from the middle of an allocated buffer, such as VerQueryValue. You can see more information about cases where IntPtr marshaling may be necessary for certain APIs here:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconmemorymanagement.asp
4. There appears to be a memory leak when using BSTR marshaling, due to a native cache in the Windows CE OS.The memory used for BSTR allocations is cached, so a BSTR is not always freed completely upon a call to SysFreeString. There is currently no way to turn this cache off under Windows CE, and given the low memory conditions of embedded devices this may present itself as an unacceptable memory leak. To work around this issue, consider using CoTaskMemFree instead of SysFreeString, which prevents any free space in the cache from accumulating.
The runtime will use SysFreeString under two conditions:
The native COM component being used will also need to be screened for SysFreeString use.You can see some more information about the BSTR cache here:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/automat/htm/chap7_2xgz.asp
Making Native function calls from Managed code
There are 2 main types of managed to native function calls:
In all three of these cases, there is a stub generated that converts between the managed calling convention and the native calling convention. In order to create this stub, the signature of the PInvoke or COM interface method is processed and any error checking that only requires knowledge of the parameter types and not the actual data occurs.During runtime when the function is called and this stub is executed, the arguments are in-marshaled as specified, then the native function is called, and finally the out-marshaling occurs along with some runtime post-processing, cleanup, and error handling. If an exception occurs during the in-marshaling process, the native function will not be called.If a native function call is considered PreserveSig(false), the runtime will assume the native function returns a HResult. In the event that a failure HResult is returned, the runtime will translate this into a managed exception, and managed objects used as parameters will not necessarily be modified or initialized during the out-marshaling process as normal. In this case, you must also keep in mind that the managed signature will not look exactly like the native signature. You can use interop logging to determine the exact native signature the runtime will be expecting. For COM calls PreserveSig defaults to false, where as for PInvoke calls it defaults to true, if not explicitly specified.
Knowledge of this process can help you diagnose and handle a few common issues:
5. A MissingMethodException occurs when trying to call a PInvoke function, due to the .dll not being found by the runtime or because the function requested is not exported as specified in the native .dll.In this case, the exception occurs at stub generation time rather then at runtime. Normally a PInvoke function is jitted upon its first call, however you can use the Marshal.PreLink function to force the runtime to jit the stub, allowing you to handle these cases smoothly in managed code and present the user with an appropriate message.If the runtime is failing to find the native .dll, ensure it is present and consider writing a simple native application that calls LoadLibraryW to load the .dll. In some cases under generic windows CE, a native .dll may have a dependency on the components of the image, and may fail to load if the dependencies are not present in the image.If the runtime is failing to find the particular exported function in the native .dll, ensure the function is present and properly exported using the dumpbin utility as described here: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetcomp/html/netcfdumpbinpinvoke.asp. Keep in mind, you can reference the exported functions either by ordinal or name.
6. A managed exception occurs when calling a native function from managed code, due to a runtime marshaling issue.In the case a PInvoke function call is being made, it is worthwhile to use Marshal.PreLink and ensure no exceptions are thrown. This will rule out jit-time errors that occur when the stub is generated, such as those resulting from unsupported marshaling types. Some marshaling issues will not occur until the function is called and the actual conversion work happens. These typically happen when trying to convert a native object into a managed object, either during the out-marshaling process of a managed to native function call, or during the in-marshaling process of a native to managed call. Some types have specific ranges in which the data is valid, such as the case with the DateTime type. In the case of VARIANTs, the runtime many not be able to coerce the type specified inside the native variant to the managed type specified in the function signature. The best practice to diagnose these issues is to use a process of elimination to determine the particular parameter causing the issue. You can use the [in] and [out] attributes to prevent marshaling in a particular direction for each parameter.
7. A native COM function results in an unexpected managed exception or the native function behaves incorrectly, due to the managed COM interface signatures being incorrect with regard to the PreserveSig attribute.If the PreserveSig attribute is used incorrectly, the runtime will assume an incorrect native signature and may call the function with un-initialized parameters, or interpret the return valid as a failure HResult when the native function does not return a HResult. The interop log file will reveal the expected native signature, and this should be compared to the documentation associated with the COM interface.
Making Managed function calls from Native code
There are 2 main types of native to managed function calls:
The process for making callbacks from native code into managed code is similar to the other direction. Both stub generation errors and runtime errors are possible. Likewise, based on the PreserveSig attribute, the runtime may translate managed exceptions to HResults. If the managed function call is considered PreserveSig(false) and an uncaught exception occurs while in managed code, the runtime will return a HResult associated with the exception to the native caller. For COM calls PreserveSig defaults to false, where as for delegate callbacks it results to true, if not explicitly specified. If a COM call is marked as PreserveSig(true) and the managed signature returns a 4-byte value such as an int, the runtime will still translate any uncaught exceptions to a HResult.The .Net Compact Framework does not support hosting the runtime from native code. This means that you will not be able to call CoCreateInstace to instantiate a managed object, or register managed objects as COM objects on the system. You will also not be able to call CorBindToRuntime or ClrCreateManagedInstance. In order to call managed functions from native code, you must first use the runtime to marshal a managed interface or a delegate down to native code. This means you must always start out in managed code (with a .net executable) in order to expose .net components to native code.
8. A native exception occurs when attempting to call a Delegate callbacks from native code, due to the managed delegate being finalized.When using delegate callbacks, you often first make a PInvoke call to register the callback function pointer which will be used after the original PInvoke call returns. Because the generated stub associated with the callback stub is cleaned up when a delegate is finalized, you must ensure the delegate object stays alive until it will no longer be used by native code. Simply keeping a reference to the delegate in managed code will be enough to keep it alive (there is no need to pin the delegate object).
9. Native code that uses native to managed function calls is behaving unexpectedly because of a managed exception firing or runtime marshaling errors, due to interface signatures being incorrect with regard to the PreserveSig attribute.When a managed exception occurs inside a managed function call that is not caught before the call returns, operating under PreserveSig(false) is the only way to ensure this exception will be translated to a HResult and properly returned from the managed call so native code can handle the error. If operating under preserveSig(true), it is recommended that you wrap a try/catch around your exposed managed function to ensure there are no uncaught exceptions and handle any errors in managed code since native code will not be able to know there was an error or handle it properly. Also keep in mind, that an exception can also occur due to a runtime marshaling error and cause the managed function to not be called.
10. A CCW function call works when called through the VTable, but not when called through IDispatch->Invoke despite the interface being marked as InterfaceIsDual, due to missing or improper use of attributes.Individual methods, as well as interfaces, can be marked ComVisible. When a method is marked ComVisible(false), a VTable stub is still generated for it allowing it to be called directly through the VTable, but it is protected from being called through IDispatch->Invoke. In the case of properties, the individual get or set method is checked, as well as the property declaration itself.If native code calls QueryInterface requesting a pure IDispatch pointer, the runtime will return the interface that it has determined is the 'default interface' that supports IDispatch. Typically, under the desktop framework this resolves to the class interface and exposes all methods to native code. Under .NetCF, since class interfaces are not automatically generated, this will resolve to the first interface present in metadata that the class implements which supports IDispatch. It's recommended that you explicitly declare a default interface using the ComDefaultInterfaceAttribute. For methods to be exposed through IDispatch, they must be explicitly marked with the DispIdAttribute.If native code is relying on IDispatch->GetIDsOfNames to resolve methods, there are a couple of limitations. The interop log file will contain more detailed output if unsupported functionallity is being requested or an error is being returned to native code from GetIDsOfNames.
The .Net Compact Framework supports interoperating with native COM objects from managed code as well as allowing native code to interact with managed objects using COM.
TlbImp is the only tool supported under the .Net Compact Framework. While the use of other tools, such as AxImp, and TlbExp may work in some cases, they are not tested and may not always produce code that will run without modification under the .Net Compact Framework.
11. There appears to be a memory leak when using COM interop features, due to Runtime Callable Wrappers not being freed immediately.Normally, RCWs are freed upon finalization when the managed object associated with the native COM object is no longer in use and the GC starts collecting object memory. At this point, the original object returned from CoCreateInstance is Released, along with any interface pointers that the runtime called QueryInterface to retrieve.Marshal.ReleaseComObject and Marshal.FinalReleaseComObject can be used to manage the lifetime of an RCW manually. Keep in mind that once these are used you will not be able to make native COM interface method calls using the object any longer and will receive an exception upon attempting to do so.There is also a native bug in Windows CE TypeLib support that can cause a memory leak when a native COM object that uses LoadTypeLib or LoadRegTypeLib is freed from a thread other then the one it was created on, including the thread finalizers are run on. In this case, it is highly recommended you use the marshal class functions to manually release the native COM object.
12. A native control doesn't appear properly or doesn't work properly when using COM interop features to host an ActiveX control in a managed Form, due to a bad ActiveX container implementation.Be sure to review the following references and understand how to write a container in native code. http://msdn.microsoft.com/library/default.asp?url=/workshop/components/containers/overview/containers.asphttp://msdn.microsoft.com/library/default.asp?url=/workshop/components/activex/intro.aspIt is also recommended that you create a simple native ActiveX control to test your container with (possibly using the Visual Studio MFC wizard), which will allow you to debug into the native source of the control. When debugging an ActiveX container implementation, it may helpful to examine the interop log file for cases where native code called QueryInterface and received an E_NOINTERFACE return value. This can help you diagnose when a required interface was not implemented by the container.If the application is running, but the control doesn't appear in the form, use the Remote Spy tool to examine the window hierarchy and position/sizing of the native control and its parents.
13. A native exception or memory leak occurs when using Com Callable Wrappers to provide native code an interface mapping to managed functions, due to improper reference counting logic.A CCW will be cleaned up upon the last reference count release from native code. Native code is expected to release any interfaces it acquired from a call to QueryInterface, as well as other functions that return an interface (such as IConnectionPointContainer::FindConnectionPoint for example). If native code wishes to keep a reference to a CCW beyond the return from the PInvoke where a CCW was marshaled down, the particular interface desired should be retrieved through QueryInterface. When the PInvoke returns and the out-marshaling process completes, the CCW will be cleaned up if there are no outstanding references.When using the runtime implemented connection point interfaces to allow native code to get notified when a managed event fires, the runtime will increment the CCW reference count upon a call to IConnectionPoint::Advise. Native code must call IConnectionPoint::Unadvise when it no longer wishes to be notified of managed events firing or the CCW will not be cleaned up.You can use the Marshal Class functions to retrieve an IntPtr representing an interface from a managed object (See GetIUnknownForObject), You can also examine the current reference count for an IntPtr representing an interface (including a CCW) by using Marshal.AddRef and Marshal.Release (which both return the current reference count).
14. A managed exception occurs when instantiating or using a Runtime Callable Wrapper or a Com Callable Wrapper despite the same code working on the desktop framework, due to limitations in com interop support under the .Net Compact Framework.The interop log file will often contain more detailed information about the cause of an exception that occurs due to differences in COM interop support between the desktop framework and the .Net Compact framework.
15. A managed InvalidComObjectException occurs when using a Runtime Callable Wrapper to call a native COM function, due to the Runtime Callable Wrapper having been freed previously.If the application has used Marshal.ReleaseComObject or Marshal.FinalReleaseComObject, you will not be able to make native COM interface method calls using the object any longer. If an attempt to do this has been made upon a previously valid RCW object, the interop log file will contain an error message.
16. A managed InvalidComObjectException occurs when attempting to instantiate a Com Callable Wrapper, due to the use of a type that requires an unsupported marshaling feature.When a CCW is constructed, the object's entire interface hierarchy is walked by the runtime. All interface methods visible to COM that are implemented will have a stub generated for them to allow the managed function call from native code. If the runtime cannot marshal any parameter type for any method, the entire CCW will not be able to be constructed. The interop log file will contain more information specifying the exact method containing the issue.
With regard to COM interop support, it may be useful to understand both the rules around using and building native COM objects, as well as how COM is integrated and supported in the .Net runtime. I like the following sources:1) Inside COM, by Dale Rogerson (red book)2) .Net and COM, by Adam Nathan (blue book)
All applications should be tested with Interop Logging turned on, even if there are no known issues. This will produce a log file that should then be searched for 'ERROR' and 'WARNING'. The log file will contain more information about the cause of managed exceptions, subtle errors that do not manifest themselves as managed exceptions, as well as assist in debugging interop issues involving native code for which the source is unavailable.http://blogs.msdn.com/netcfteam/archive/2005/07/24/442609.aspx
There are interop related guidelines and tips that can drastically improve your application's performance: http://blogs.msdn.com/netcfteam/archive/2005/05/04/414820.aspx
Default marshaling for various types:Arrays: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondefaultmarshalingforarrays.aspBoolean: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondefaultmarshalingforbooleans.aspClasses: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondefaultmarshalingforclasses.aspValue Types / Structs: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondefaultmarshalingforvaluetypes.aspStrings: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondefaultmarshalingforstrings.aspObjects: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondefaultmarshalingforobjects.asp
[Author: Katie Blanch]
Disclaimers:This posting is provided "AS IS" with no warranties, and confers no rights.Some of the information contained within this post may be in relation to beta software. Any and all details are subject to change.