Day 7 or 37, who knows: .Net CF and Marshalling ANSI strings in structures

Consider the following code:

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

namespace SmartDeviceProject5
{
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo();
try
{
SomeMethod(foo); // KABOOM! NotSupportedException
}
catch (Exception e)
{
}
try
{
AnotherMethod("hello"); // KABOOM! NotSupportedException
}
catch (Exception e)
{
}
}

[DllImport("SomeDLL")]
extern static void SomeMethod(Foo foo);

[DllImport("SomeDLL")]
extern static void AnotherMethod([MarshalAs(UnmanagedType.LPStr)] string someString);
}

public struct Foo
{
public string Bar;
public string Borked;
}

}

When attempting to run this program, it will throw an exception upon calling the SomeMethod PInvoke. Attempting to call the "AnotherMethod" function will fail just as well.
This happens because C# automatically marshals all strings in structures as LPTStr (which for some reason is LPStr in Windows Mobile). However, in methods, strings are marshalled as LPWStr. I don't get it. Anyways, .Net Compact Framework does not support marshalling as LPStr in structures or method calls for whatever reason, even though the code to make it happen is all there (as I will show you).
Changing the code by adding the MarshalAs attribute to explicitly Marshal them as LPWStr would make it work:



    [DllImport("SomeDLL")]
extern static void AnotherMethod([MarshalAs(UnmanagedType.LPWStr)] string someString);
}

public struct Foo
{
[MarshalAs(UnmanagedType.LPWStr)]
public string Bar;
[MarshalAs(UnmanagedType.LPWStr)]
public string Borked;
}


Although LPWStr (Unicode) is more or less the standard now, but if you really really want/need to Marshal it as a LPStr, tough cookies, you get a NotSupportedException.
Generally, if you need to marshal a structure with strings, it's highly unlikely that you are going to need to mix LPWStr and LPStr in the same structure. To that end, I wrote a custom MarshalAnsi class that marshals a structure and all its contents; any strings found are marshalled as LPStr:


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

namespace System.Runtime.InteropServices
{
    /// <summary>
    /// .NET Compact framework does not support marshalling strings to ascii.
    /// Need to do it manually.
    /// </summary>
    static class MarshalAnsi
    {
        /// <summary>
        /// This Dictionary maintains all the strings allocated by an IntPtr that a structure
        /// was Marshalled to.
        /// </summary>
        static Dictionary<IntPtr, List<IntPtr>> myStringsForObject = new Dictionary<IntPtr,List<IntPtr>>();

        public static IntPtr StructureToPtr(object structure)
        {
            Type type = structure.GetType();
            var fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.GetField | BindingFlags.Public | BindingFlags.NonPublic);
            
            // determine the total size of the structure. Need to special case strings and bools
            int totalSize = 0;
            foreach (FieldInfo field in fieldInfos)
            {
                totalSize += field.FieldType == typeof(string) ? Marshal.SizeOf(typeof(IntPtr)) :
                              field.FieldType == typeof(bool) ? Marshal.SizeOf(typeof(int)) : Marshal.SizeOf(field.FieldType);
            }

            // allocate the pointer, and create it's list of allocated strings
            IntPtr ret = Marshal.AllocHGlobal(totalSize);
            List<IntPtr> strings = new List<IntPtr>();
            myStringsForObject.Add(ret, strings);
            // structure pointer offset, which is incremented as we write to the structure
            int ofs = 0;
            foreach (FieldInfo field in fieldInfos)
            {
                object toWrite = null;

                if (field.FieldType == typeof(string))
                {
                    // allocate memory for the string if need be, and add it to the 
                    // pointers string allocation list
                    string str = field.GetValue(structure) as string;
                    IntPtr strPtr;
                    if (str == null)
                        strPtr = IntPtr.Zero;
                    else
                    {
                        byte[] bytes = Encoding.ASCII.GetBytes(str);
                        strPtr = Marshal.AllocHGlobal(bytes.Length + 2);
                        strings.Add(strPtr);
                        Marshal.Copy(bytes, 0, strPtr, bytes.Length);
                        Marshal.WriteInt16(strPtr, bytes.Length, 0);
                    }
                    toWrite = strPtr;
                }
                else if (field.FieldType == typeof(bool))
                {
                    // need to write this as an int, not a bool. 
                    // BOOL in C/C++ is really an int, which is of size 4.
                    toWrite = (bool)field.GetValue(structure) ? 1 : 0;
                }
                else
                {
                    // just do the default behavior
                    toWrite = field.GetValue(structure);
                }
                Marshal.StructureToPtr(toWrite, (IntPtr)((int)ret + ofs), false);
                // increment the structure pointer offset
                ofs += Marshal.SizeOf(toWrite);
            }

            return ret;
        }

        /// <summary>
        /// Destroy the memory allocated by a structure, and all strings as well.
        /// </summary>
        /// <param name="ptr"></param>
        public static void DestroyStructure(IntPtr ptr)
        {
            List<IntPtr> strings = myStringsForObject[ptr];
            myStringsForObject.Remove(ptr);
            foreach (IntPtr strPtr in strings)
            {
                Marshal.FreeHGlobal(strPtr);
            }
            Marshal.FreeHGlobal(ptr);
        }
    }
}

Usage:
Simply pass a structure into this class and it will return a pointer with the marshalled data. All strings are marshalled as Ansi strings.
Remember to call MarshalAnsi.DestroyStructure with the pointer returned from MarshalAnsi.StructureToPtr when it is no longer in use.

2 comments:

Anonymous said...

You saved my day, thank you sooo much.

Anonymous said...

It seems like Marshal.PtrToStructure is not supported in .Net CF as well. Do you have a code snippet for the same ?
thx
Krishna