Archives

Archives / 2009 / March
  • More on COM Safe Arrays and .NET Interop

    A few years ago I wrote about P/Invoke and how it can provide benefits even when C++ Interop is available on the consuming end. Recently I’ve had to do a bit of work consuming COM safe arrays from C#. I’ve seen safe arrays trip up developers time and again so I thought I’d share some thoughts and tips on the subject.

    This discussion focuses on the use of two-dimensional safe arrays. Single-dimensional safe arrays are marshalled automatically by the CLR (in most cases) but it is two-dimensional safe arrays that are often used to communicate key-value pairs such as for a map or dictionary of some kind. The trouble is that a two-dimensional safe array is really just a table of values. You can think of it as having an X axis and a Y axis. Storing key-value pairs in the table then requires a decision to be made regarding whether the keys will be stored down the Y axis as rows or along the X axis as columns. You could argue about which is better but at the end of the day you’re likely going to be at the mercy of whatever native API you’re calling.

    If keys are stored down the Y axis then a particular key is indexed with the safe array’s first dimension. The first dimension will thus have as many elements as there are pairs. The second dimension will have exactly two elements with the first storing the key and the second storing the value.

    On the other hand if keys are stored along the X axis then a particular key is indexed not by the first dimension but by the second. The second dimension will thus have as many elements as there are pairs. The first dimension will have exactly two elements with the first storing all the keys and the second storing all the values.

    Of course not everyone naturally thinks in terms of the same coordinate space and you may need to apply a transform before it makes sense to you. Apologies to graphics developers everywhere.  :)

    Let’s make this a bit more concrete with an example. Here is a native container of string-double pairs implemented by ATL’s CAtlMap class:

    typedef CAtlMap<CString, double> Map;
    Map m_map;


    The following function creates and returns a safe array keyed along the X axis using ATL’s safe array wrapper classes:

    #define HR(expr) { hr = expr; if (FAILED(hr)) return hr; }

    HRESULT __stdcall GetDictionaryX(SAFEARRAY** dictionary)
    {
        HRESULT hr;

        if (0 == dictionary)
        {
            return E_POINTER;
        }

        CComSafeArrayBound bounds[2] =
        {
            CComSafeArrayBound(2),
            CComSafeArrayBound(m_map.GetCount())
        };

        CComSafeArray<VARIANT> safeArray;
        HR(safeArray.Create(bounds, _countof(bounds)));

        LONG indices[2] = { 0 };
        POSITION position = m_map.GetStartPosition();

        for (; 0 != position; ++indices[1])
        {
            const Map::CPair* pair = m_map.GetNext(position);

            indices[0] = 0;
            HR(safeArray.MultiDimSetAt(indices, CComVariant(pair->m_key)));

            indices[0] = 1;
            HR(safeArray.MultiDimSetAt(indices, CComVariant(pair->m_value)));
        }

        *dictionary = safeArray.Detach();
        return S_OK;
    }


    Briefly, the bounds define the dimensions of the safe array and the indices are used to set the values in the table.

    The following function does the same thing but returns a safe array keyed along the Y axis:

    HRESULT __stdcall GetDictionaryY(SAFEARRAY** dictionary)
    {
        HRESULT hr;

        if (0 == dictionary)
        {
            return E_POINTER;
        }

        CComSafeArrayBound bounds[2] =
        {
            CComSafeArrayBound(m_map.GetCount()),
            CComSafeArrayBound(2)
        };

        CComSafeArray<VARIANT> safeArray;
        HR(safeArray.Create(bounds, _countof(bounds)));

        LONG indices[2] = { 0 };
        POSITION position = m_map.GetStartPosition();

        for (; 0 != position; ++indices[0])
        {
            const Map::CPair* pair = m_map.GetNext(position);

            indices[1] = 0;
            HR(safeArray.MultiDimSetAt(indices, CComVariant(pair->m_key)));

            indices[1] = 1;
            HR(safeArray.MultiDimSetAt(indices, CComVariant(pair->m_value)));
        }

        *dictionary = safeArray.Detach();
        return S_OK;
    }

    With the native code out of the way let’s see how we can call these functions from C#. For simplicity lets assume they’re just exported functions and not COM interface methods. The same principals apply but it’s just less sample code for me to write.

    The first step is to import the functions as follows:

    [DllImport("Server.dll", EntryPoint = "GetDictionaryY", PreserveSig = false)]
    [return: MarshalAs(UnmanagedType.SafeArray)]
    static extern object[,] NativeGetDictionaryY();

    [DllImport("Server.dll", EntryPoint = "GetDictionaryX", PreserveSig = false)]
    [return: MarshalAs(UnmanagedType.SafeArray)]
    static extern object[,] NativeGetDictionaryX();

    This just adds some metadata needed by the CLR to call the LoadLibrary and GetProcAddress functions on your behalf, how to marshal the parameters and return value, and what to do with the HRESULT.

    The next step is to determine how many key-value pairs the two-dimensional array consists of. The array’s GetLength method comes in handy as you can specify the dimension whose length you are interested in:

    object[,] array = NativeGetDictionaryX();
    int count = array.GetLength(1);


    Given the count you can now enumerate the key-value pairs using a for loop and a bit of casting:

    for (int i = 0; i < count; ++i)
    {
        string key = (string)array[0, i];
        double value = (double)array[1, i];

        Console.WriteLine(key + "=" + value);
    }

    Needless to say this is quite error prone. It would be more desirable to use a foreach loop but the enumerator provided by the multi-dimensional array doesn’t know anything about the key-value pairs so it just returns it in a flat list. The good news is that it’s pretty easy to write your own enumerator or to be precise let the C# compiler generate one for you.

    Here’s an implementation of the generic IEnumerable<TKey, TValue> interface that does just what you want for arrays keyed along the X axis:

    class SafeArrayEnumerableX<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
    {
        private readonly object[,] m_array;

        public SafeArrayEnumerableX(object[,] array)
        {
            Debug.Assert(null != array);
            Debug.Assert(2 == array.GetLength(0));

            m_array = array;
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            int count = m_array.GetLength(1);

            for (int i = 0; i < count; ++i)
            {
                yield return new KeyValuePair<TKey, TValue>((TKey)m_array[0, i], (TValue)m_array[1, i]);
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }


    Notice that it’s effectively doing the same looping and casting as the hand-rolled approach but in such a way that you end up with a reusable enumerator that hides all the nasty indexing and casting. With the enumerator providing a sequence of generic KeyValuePair<TKey, TValue> structures, enumerating over the pairs now becomes much more elegant:

    static SafeArrayEnumerableX<string, double> GetDictionaryX()
    {
        return new SafeArrayEnumerableX<string, double>(NativeGetDictionaryX());
    }

    foreach (var pair in GetDictionaryX())
    {
        string key = pair.Key;
        double value = pair.Value;

        Console.WriteLine(key + "=" + value);
    }


    And that’s all there is to it. For completeness, here’s the equivalent enumerable implementation for arrays keyed along the Y axis:

    class SafeArrayEnumerableY<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
    {
        private readonly object[,] m_array;

        public SafeArrayEnumerableY(object[,] array)
        {
            Debug.Assert(null != array);
            Debug.Assert(2 == array.GetLength(1));

            m_array = array;
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            int count = m_array.GetLength(0);

            for (int i = 0; i < count; ++i)
            {
                yield return new KeyValuePair<TKey, TValue>((TKey)m_array[i, 0], (TValue)m_array[i, 1]);
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }


    I hope that helps.