VB6 Types (Structs) with Fixed Length Strings

24. February 2013 12:06 by Mrojas in C#, COM Interop  //  Tags: , , , , , , , , ,   //   Comments (0)

 

In VB6 the communication with backend services usually involves the definition of Types (or Structs) using fixed length strings.

VB6 provided language support for defining these data types. 

For example:
Public Type HostData     
	UserName       As String * 8
	PassWord       As String * 8
	FullName       As String * 50
End Type
Figure 1 Example of VB6 Type with Fixed Length Strings
There are some ways to model this structures in .NET using the FixedLengthString defined in Microsoft.VisualBasic.Compatibity. Here I will present another approach. This approach uses character arrays (char[]) to model this structures. Ok. Let’s get down to business. To model a vb6 type like the one in Figure 1, we will use this approach:
struct HostData
    {
        [DebuggerDisplay("{s(UserName)}")]
        [MarshalAs(UnmanagedType.ByValArray, SizeConst=8)]
        public char[] UserName;

        [DebuggerDisplay("{s(PassWord)}")]
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        char[] PassWord;

        [DebuggerDisplay("{s(FullName)}")]
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)]
        public char[] FullName;
        /// This method is used to provide a better display of
	 /// character arrays as strings on the debugger.
        string s(char[] array) { return new string(array); }

        public byte[] toByteArray() {    
           return StructsHelper.StructToByteArray(this);
        }

 public static explicit operator HostData(byte[] array)
        {
            return (HostData)StructsHelper.ByteArrayToStructure(array,typeof(HostData));
        }

        /// <summary>
        /// Constructor to initialize the char arrays that are used as fixed length strings
        /// Struct constructors must have at least one parameter.
        /// </summary>
        /// <param name="initFixedLengthStrings">if true will automatically init all fixed length char arrays according to the SizeConst property of the MarshalAs attribute</param>
        public HostData (bool initFixedLengthStrings=false)
        {
            UserName = null; 
            PassWord = null;
            FullName = null;
            if (initFixedLengthStrings)
            {
                StructsHelper.InitFixedStrings(GetType(), __makeref(this));
            }
        }
    }
Figure 2: Code of Example 1 in C#
 
So several tricks are used here, I will describe them:
 

First

All fixed length strings are declared as char[]. A MarshalAs attribute is applied to each field. Like this:
[MarshalAs(UnmanagedType.ByValArray, SizeConst=n)]
Where n is the number of characters in the fixed length strings. Note that character arrays must be initialized. However structs do not allow field initializers. So they will need to be initialized on a constructor.
 

Second

A DebuggerDisplay attribute 
[DebuggerDisplay("{s(<AttributeName>)}")]
is added to each field, just to make the developer experience. That makes that instead of showing this field as a character array it will be shown as a string.
This attribute uses a small helper function used s that just converts the character array to string.
 

Third

A constructor is added. Structs do not accept parameter-less constructors.
This struct receives a Boolean indicating whether you want to initialize the character array fields.
As a requirement character arrays fields should at least be initialized to null. Character arrays could have been initialized here but I opted to create a helper function. Why? Well I think it is better if this arrays are initialized using the SizeConst attribute. So if I want to change their size I do not have to update both the SizeConst and the constructor.
public static void InitFixedStrings(Type type,TypedReference reference)  
        {
            if (type.IsValueType && !type.IsPrimitive && !type.Namespace.StartsWith("System") && !type.IsEnum)
            {//This should be an struct
                foreach (var field in
                    type.GetFields(System.Reflection.BindingFlags.Instance |
                    System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic))
                {
                    
                    if (field.FieldType.IsArray && field.FieldType == typeof(char[]))
                    {
                        var attr = field.GetCustomAttributes(typeof(MarshalAsAttribute),false);
                        if (attr != null && attr.Length > 0)
                        {
                            MarshalAsAttribute maa = (MarshalAsAttribute)attr[0];
                            var constSize = maa.SizeConst;
                            if (constSize != -1)
                            {
                                var newValue = new char[constSize];
                                field.SetValueDirect(reference, newValue);
                            }
                        }
                    }
                }
            }
        }
 

Forth>

In VB6 the common approach is to use the StrConv and CopyMemory functions to copy memory to and from structs and send them as strings or event to copy data between structs of different types. To solve that utility methods have been created:
/// <summary>
        /// Takes a bytearray and uses it to create a struct of the given type
        /// and populate it with the data of the byte array.
        /// NOTE: this method only works withs Structs which have a fixed size
        /// </summary>
        /// <param name="bytearray"> The data that will be used to initialize the struct</param>
        /// <param name="type">The type of the expected struct</param>
        /// <returns>A new struct instance with its fields initialized with the bytes from bytearray</returns>
        public static object ByteArrayToStructure(byte[] bytearray, Type type)
        {
            int len = Marshal.SizeOf(type);
            IntPtr i = Marshal.AllocHGlobal(len);
            Marshal.Copy(bytearray, 0, i, len);
            var obj = Marshal.PtrToStructure(i,type);
            Marshal.FreeHGlobal(i);
            return obj;
        }

/// <summary>
        /// Returns the contents of an struct as a byte array.
        /// It only works with fixed length structs.
        /// </summary>
        /// <param name="obj">the struct that holds the data that will be returned in the byte array</param>
        /// <returns>A byte array with the contents of the struct</returns>
        public static byte[] StructToByteArray(this object obj)
        {

            int len = Marshal.SizeOf(obj);
            byte[] arr = new byte[len];
            IntPtr ptr = Marshal.AllocHGlobal(len);
            Marshal.StructureToPtr(obj, ptr, true);
            Marshal.Copy(ptr, arr, 0, len);
            Marshal.FreeHGlobal(ptr);
            return arr;

        }
With these utility methods you can then use your structs like this:
var hostData = new HostData (true);
var byteArray = UnicodeEncoding.Unicode.GetBytes(new String(' ', Marshal.SizeOf(typeof(HostData))));
hostData = (HostData)byteArray;

var size = Marshal.SizeOf(HostData);
var test = "helloworld";
test = test.PadRight(size, '*');
            
byteArray = UnicodeEncoding.Unicode.GetBytes(test);
hostData = (HostData)byteArray;
 

Fifth

And finally how to you easily get/set data from these structs? Very easy. We will add an extension method:
const string IF_VALUE_NOT_PROVIDED_THEN_RETURN_VALUE = "\0\0internal";
        /// <summary>
        /// This method is used to get/set the values of a char array as an string.
        /// It has been implemented in a way similar to that used in the jquery .val function.
        /// If called without parameters it will return the character array value as an string.
        /// If called with parameters will use the given string to set the character array value.
        /// If the given string is bigger that the character string the value is truncated
        /// </summary>
        /// <param name="array"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static string val(this char[] array, String value = IF_VALUE_NOT_PROVIDED_THEN_RETURN_VALUE)
        {
            if (value == IF_VALUE_NOT_PROVIDED_THEN_RETURN_VALUE)
                return new string(array);
            else
            {
                var source = value.ToCharArray();
                Array.Copy(source, array, Math.Min(source.Length, array.Length));
                return value;
            }
        }
With that if you want to set a field you will do something like:
hostData.UserName.val(“Mauricio”)
And if you want to get the contents of the field you will do something like:
String username = hostData.UserName.val();
Well that’s all. Hope this helps

StructsHelpers.cs (5.93 kb)