//
you're reading...
Interop Marshaling

Custom Marshaling – Comments and an Example Use of the Marshal Cookie

1. Introduction.

1.1 This article and several to follow are extensions to the “Understanding Custom Marshaling” series of articles designed to help readers understand the rudiments of custom marshaling in .NET.

1.2 In this article, I discuss some general observations about the custom marshaling mechanism that I hope the reader had the opportunity to make while working through the example codes of the articles.

1.3 Note well that the comments I make below are anecdotal and are based on observation rather than rigorous research. I welcome all comments (especially corrections) from readers.

1.4 I also present in this article a bonus enhanced version of StringMarshaler that will demonstrate the use of the MarshalCookie.

2. Comment 1 : Using the Custom Marshaler Across Threads.

2.1 First note that whenever the CLR wants to instantiate a custom marshaler, it calls the static GetInstance() method of the custom marshaler’s class.

2.2 Through working with the test codes, I noticed that the CLR may at times instantiate a new custom marshaler to accommodate additional threads (i.e. this means that the static ICustomMarshaler::GetInstance() method may be called multiple times).

2.3 This seems to be the case whenever there is a possibility that 2 or more threads end up simultaneously using one single instance of a custom marshaler.

2.4 It appears that the CLR is able to anticipate this and will prevent it from happening by instantiating a new custom marshaler to service separate threads.

2.5 However it is also possible that even in the presence of multiple threads, only one single custom marshaler is used throughout. This happens as long as the single instance custom marshaler is never called simultaneously across threads.

2.6 This shows that we should not make any assumptions about the instantiation of a custom marshaler class. This is certainly another reason why we should either limit or avoid altogether any state information stored inside a custom marshaler.

3. Comment  2 : GetNativeDataSize() Never Gets Called.

3.1 Readers who have worked through the source codes of the “Understanding Custom Marshaling” series of articles may have noticed that the GetNativeDataSize() method never seems to get called.

3.2 The GetNativeDataSize() method is meant to be used in situations where the managed data to be marshaled is a value type (e.g. int, long, byte) viz a reference type (string, arrays).

3.3 If the data to be marshaled is a reference type (e.g. a string), GetNativeDataSize() must return -1.

3.4 However, custom marshaling of value types appear to be unsupported since .NET Framework version 1.0. Currently, to custom marshal a value type, it needs to be boxed inside an object type.

3.5 As such, it appears that at present, custom marshaling still applies only to reference types. Now, whenever reference types are marshaled, they are always passed by address. That is, a pointer is passed on the stack, and the pointer contains the address of the unmanaged equivalent of the managed data to be marshaled.

3.6 This is why the MarshalManagedToNative() method returns an IntPtr and the size and allocation of the native data to be marshaled is all done inside MarshalManagedToNative() independent of the CLR.

3.7 This leaves GetNativeDataSize() redundant and currently appear to be uncalled.

4. An Example Use of the Marshal Cookie.

4.1 In this section, I present an enhanced version of the StringMarshaler class that we have been using from the “Understanding Custom Marshaling” series of articles.

4.2 It is named StringMarshalerEx and the following is a listing of its code :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace StringMarshalerClassLib
{
    public class StringMarshalerEx : ICustomMarshaler
    {
        public StringMarshalerEx(string cookie)
        {
            m_cookie = cookie;
        }

        public object MarshalNativeToManaged(IntPtr pNativeData)
        {
            if (m_cookie.ToUpper() == "ANSI")
            {
                return Marshal.PtrToStringAnsi(pNativeData);
            }
            else if (m_cookie.ToUpper() == "UNICODE")
            {
                return Marshal.PtrToStringUni(pNativeData);
            }
            else 
            {
                return Marshal.PtrToStringBSTR(pNativeData);
            }
        }

        public IntPtr MarshalManagedToNative(object ManagedObj)
        {
            // Managed Object must be a string type.
            if (!(ManagedObj is string))
            {
                return IntPtr.Zero;
            }

            if (m_cookie.ToUpper() == "ANSI")
            {
                return Marshal.StringToCoTaskMemAnsi((string)ManagedObj);
            }
            else if (m_cookie.ToUpper() == "UNICODE")
            {
                return Marshal.StringToCoTaskMemUni((string)ManagedObj);
            }
            else
            {
                return Marshal.StringToBSTR((string)ManagedObj);
            }
        }

        public void CleanUpNativeData(IntPtr pNativeData)
        {
            if (m_cookie.ToUpper() == "ANSI")
            {
                Marshal.FreeCoTaskMem(pNativeData);
            }
            else if (m_cookie.ToUpper() == "UNICODE")
            {
                Marshal.FreeCoTaskMem(pNativeData);
            }
            else
            {
                Marshal.FreeBSTR(pNativeData);
            }

            pNativeData = IntPtr.Zero;
        }

        public void CleanUpManagedData(object ManagedObj)
        {
            // Nothing to do
        }

        public int GetNativeDataSize()
        {
            return -1;
        }

        public static ICustomMarshaler GetInstance(string cookie)
        {
            // First check to see if the cookie argument is valid.
            if 
            (
                (cookie.ToUpper() != "ANSI")
                &&
                (cookie.ToUpper() != "UNICODE")
                &&
                (cookie.ToUpper() != "BSTR")
            )
            {
                // If not, throw exception.
                throw new ArgumentException(string.Format("Invalid argument : {0:S}", cookie));
            }

            // Always instantiate a new custom marshaler.
            // The CLR will not instantiate a new custom
            // marshaler if the same cookie has been used
            // previously.            
            return new StringMarshalerEx(cookie); 
        }

        private string m_cookie;
    }
}

The following are some important points about the StringMarshalerEx class above :

  • The main objective of StringMarshalerEx is to marshal a managed string to the unmanaged world with the target unmanaged string expressed in one of 3 possible formats : as an ANSI C-style string, as a UNICODE C-style string and as a BSTR.
  • It contains a member string m_cookie. This shows that an instance of the StringMarshalerEx class retains state. The StringMarshalerEx constructor takes a string parameter that it stores in the m_cookie member.
  • In fact, the StringMarshalerEx class anticipates more than one single instance (up to 3, one for each string format) to be created in a client application.
  • The m_cookie member is best used in the MarshalNativeToManaged(), MarshalManagedToNative() and CleanUpNativeData() methods.
  • These are the methods where string management functions of the Marshal class are used. The specific Marshal functions must be based on the current value of m_cookie.
  • The reason for this is quite obvious of course : the ANSI version of the Marshal class must be used when m_cookie is “ANSI”, the UNICODE version when m_cookie is “UNICODE” and the BSTR version when m_cookie is “BSTR”.
  • The static GetInstance() method will first check that the cookie string is a valid anticipated one and if not an ArgumentException is thrown.
  • If the cookie string is valid, a new instance of StringMarshalerEx is created.

4.3 To show the StringMarshalerEx class in action, we need to add new exported functions to our TestDLL.dll :

#include "stdafx.h"
#include "objbase.h"
#include <comutil.h>

void __stdcall SetStringA(/*[in]*/ const char* lpszString)
{
	MessageBoxA(NULL, lpszString, "SetStringA", MB_OK);
}

void __stdcall SetStringW(/*[in]*/ const wchar_t* lpwszString)
{
	MessageBoxW(NULL, lpwszString, L"SetStringW", MB_OK);
}

void __stdcall SetStringBSTR(/*[in]*/ BSTR bstrString)
{
	_bstr_t _bstString(bstrString);

	MessageBoxA(NULL, (LPCSTR)_bstString, "SetStringBSTR", MB_OK);
}

void __stdcall GetStringA(/*[out]*/ char** ppszStringReceiver)
{
	char lpszString [] = "The quick brown fox jumps over the lazy dog";

	*ppszStringReceiver = (char*)::CoTaskMemAlloc(sizeof(lpszString));

	strcpy_s(*ppszStringReceiver, sizeof(lpszString), lpszString);
}

void __stdcall GetStringW(/*[out]*/ wchar_t** ppwszStringReceiver)
{
	wchar_t lpwszString [] = L"The quick brown fox jumps over the lazy dog";
	size_t stSize = sizeof(lpwszString);
	size_t stLen = wcslen(lpwszString);

	*ppwszStringReceiver = (wchar_t*)::CoTaskMemAlloc(stSize);

	wcscpy_s(*ppwszStringReceiver, stLen + 1, lpwszString);
}

void __stdcall GetStringBSTR(/*[out]*/ BSTR* pbstrStringReceiver)
{
	*pbstrStringReceiver = ::SysAllocString(L"The quick brown fox jumps over the lazy dog");
}
  • SetStringA() is actually the same as the SetString() function that we saw earlier in previous parts.
  • GetStringA() is actually GetString() in previous parts.
  • The new functions are SetStringW(), SetStringBSTR(), GetStringW(), GetStringBSTR() which are the UNICODE and BSTR versions of SetString() and GetString().
  • The UNICODE and BSTR versions of ReturnString() and ModifyString() are left to the reader as an exercise.

4.4 Finally, I have prepared 2 sets of client code that will demonstrate the call to the SetString*() and GetString*() functions in managed code. The following lists the client C# code for using SetString*() :

[DllImport(@"TestDLL.dll", EntryPoint = "SetStringA", CallingConvention = CallingConvention.StdCall)]
private static extern void SetStringA
    ([In][MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(StringMarshalerEx), MarshalCookie = "ANSI")] string strValue);

[DllImport(@"TestDLL.dll", EntryPoint = "SetStringW", CallingConvention = CallingConvention.StdCall)]
private static extern void SetStringW
    ([In][MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(StringMarshalerEx), MarshalCookie = "UNICODE")] string strValue);

[DllImport(@"TestDLL.dll", EntryPoint = "SetStringBSTR", CallingConvention = CallingConvention.StdCall)]
private static extern void SetStringBSTR
    ([In][MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(StringMarshalerEx), MarshalCookie = "BSTR")] string strValue);

static void DoTest_SetString()
{
    string str = "My String";

    SetStringA(str);

    SetStringW(str);

    SetStringBSTR(str);

    SetStringA("Hello World");
}
  • Notice the use of the MarshalAsAttribute.MarshalCookie field.
  • This field is of type string and ends up being the argument to the static GetInstance() method.
  • In the DoTest_SetString() function at runtime, a call to each version of SetString*() invokes a call to GetInstance() with a specific cookie value.
  • However, when the same version is called a second or third time (or more), GetInstance() is not called again and the same created instance is used.
  • This is the case when SetStringA() is called a second time with string “Hello World”.

4.5 The following lists the client C# code for using GetString*() :

[DllImport(@"TestDLL.dll", EntryPoint = "GetStringA", CallingConvention = CallingConvention.StdCall)]
private static extern void GetStringA
    ([Out][MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(StringMarshalerEx), MarshalCookie = "ANSI")] out string strReceiver);

[DllImport(@"TestDLL.dll", EntryPoint = "GetStringW", CallingConvention = CallingConvention.StdCall)]
private static extern void GetStringW
    ([Out][MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(StringMarshalerEx), MarshalCookie = "UNICODE")] out string strReceiver);

[DllImport(@"TestDLL.dll", EntryPoint = "GetStringBSTR", CallingConvention = CallingConvention.StdCall)]
private static extern void GetStringBSTR
    ([Out][MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(StringMarshalerEx), MarshalCookie = "BSTR")] out string strReceiver);            

static void DoTest_GetString()
{
    string str;
    GetStringA(out str);
    Console.WriteLine(str);

    GetStringW(out str);
    Console.WriteLine(str);

    GetStringBSTR(out str);
    Console.WriteLine(str);                        
}
  • Just like the case with DoTest_SetString(), in the DoTest_GetString() function at runtime, a call to each version of GetString*() invokes a call to GetInstance() with a specific cookie value.
  • And as before with SetString*(), when the same version is called a second or third time (or more), GetInstance() is not called again and the same created instance is used.

5. In Conclusion.

5.1 I certainly hope the reader has enjoyed this article and is now anticipating the potential usefulness of the MarshalCookie.

5.2 I shall follow on with more articles expounding various example custom marshalers. I hope these will prove useful and will provide even more motivation.

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: