//
you're reading...
Programming Issues/Tips

Specifying Arrays in UDTs

1. Introduction.

1.1 Although COM User-Defined Types (UDTs) commonly use BSTRs to hold strings (i.e. character arrays) and SAFEARRAYs to hold arrays of other types, it is possible to define character arrays and arrays of other types inside a UDT.

1.2 This blog will demonstrate the use of arrays inside a UDT with an emphasis on the use of character arrays to represent C-style NULL-terminated strings.

1.3 I shall also be using C# code to write the COM client. The use of managed code provides several interesting insights into the workings of the interop marshaler. I shall also briefly demonstrate the modification of the interop assembly in order to influence the managed representation of the UDT.

2. Some Important Notes Concerning UDTs.

2.1 The IDL definition of a UDT is language-neutral. It is a template for a representation of a structure to be shared among COM components.

2.2 When a UDT defintiion has been compiled into a type library, it can be imported into the project of various COM-aware language IDEs. For example, for Visual Basic 6.0 and Visual C#, this is done by adding a reference. For Visual C++, this is done via the #import statement.

2.3 It is the job of the type library importer (of the specific language IDE) to interpret the type library contents and produce the language-specific code constructs that represent the types defined in the type library.

2.4 Hence, a UDT defined in a type library will be interpreted by the Visual Basic 6.0 IDE to produce a Visual Basic-specific Type statement that represents that UDT in that language. For Visual C++, it will be interpreted to produce a typedef struct statement contained inside a .TLH (type library header) file. For Visual C#, it will be interpreted and stored in the interop assembly file which can be referenced by the compiler.

2.5 At runtime, such a structure must be represented in memory of course. Furthermore, it must have a single memory layout that is used across components produced by all COM-aware languages.

2.6 Individual components (e.g. COM-aware .NET components) are free to produce at runtime any other representation of the UDT. But when exchanged with another COM component, the UDT must be a common binary standard one which is defined by COM and is primarily based on the C-style structure.

2.7 For .NET components, e.g. those written in C#, have another, managed, representation of a UDT. Why is it necessary to have a managed version of the same type as defined in a type library ?  This is because managed code can only work with managed types.

2.8 At runtime, a managed component works with its managed structure, and then, when required to be exchanged with unmanaged code (e.g. a COM method), the field values of the managed structure is copied to its unmanaged COM UDT counterpart.

3. Sample UDT and COM Code.

3.1 For the purpose of our study of arrays in UDTs, I have prepared a test COM server in which a sample UDT is defined. A test COM interface plus implementation is also provided. The following is the IDL listing :

// TestCOMServer01.idl : IDL source for TestCOMServer01
//

// This file will be processed by the MIDL tool to
// produce the type library (TestCOMServer01.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
	uuid(0E7EC305-147B-4578-8DF9-942169BBC3E5),
	version(1.0),
	helpstring("TestCOMServer01 1.0 Type Library")
]
library TestCOMServer01Lib
{
	importlib("stdole2.tlb");

	typedef [uuid(B4F78A9C-AA12-42a0-A979-AC2FBE74791A), version(1.0)]
	struct TestStructure01
	{
		char	m_szStr[256];
		int	m_IntArray[10];
		wchar_t	m_wszStr_01[256];
		wchar_t m_wszStr_02[256];
		BSTR	m_bstr;
	} TestStructure01;	

	[
		object,
		uuid(1AA0B498-2BA5-4EB2-974A-798C15E3EED8),
		dual,
		nonextensible,
		helpstring("ITestObject01 Interface"),
		pointer_default(unique)
	]
	interface ITestObject01 : IDispatch
	{
		[id(1), helpstring("method TestMethod01")] HRESULT TestMethod01([in] VARIANT varTestStructure01);
	};

	[
		uuid(4F424D07-A34C-4086-AD26-B534D633EFAC),
		helpstring("TestObject01 Class")
	]
	coclass TestObject01
	{
		[default] interface ITestObject01;
	};
};

The following are pertinent points concerning the above IDL code :

  • TestStructure01 contains 4 arrays and one BSTR. The int array is used to demonstrate that we are free to use such arrays (viz SAFEARRAY of ints) inside a UDT.
  • The single-byte character array m_szStr is used to show how such strings are treated in managed code. A managed code character is Unicode and single-byte characters are treated as sbyte (a signed 8-bit integer).
  • There are 2 wide-character arrays m_wszStr_01 and m_wszStr_02. These are used to demonstrate 2 ways by which managed strings may be converted to unmanaged wide character arrays.
  • The BSTR is used for comparison with the 3 character arrays.
  • The ITestObject01 interface and a provided implementation is defined for test calling purposes.

3.2 The implementation for the ITestObject01::TestMethod01() is provided below :

STDMETHODIMP CTestObject01::TestMethod01(VARIANT varTestStructure01)
{
	// TODO: Add your implementation code here
	if (V_VT(&varTestStructure01) != VT_RECORD)
	{
		return S_OK;
	}

	// Get the IRecordInfo object associated with the structure.
	IRecordInfoPtr spIRecordInfo = V_RECORDINFO(&varTestStructure01);

	if (spIRecordInfo == NULL)
	{
		return S_OK;
	}

	// Check to see what is the GUID associated with the
	// IRecordInfo. It should be B4F78A9C-AA12-42a0-A979-AC2FBE74791A.
	GUID guid;
	spIRecordInfo -> GetGuid(&guid);

	TestStructure01* ptest_structure_01 = (TestStructure01*)V_RECORD(&varTestStructure01);

	int i = 0;

	printf ("ptest_structure_01 -> m_szStr : [%s].\r\n", ptest_structure_01 -> m_szStr);

	for (i = 0; i < 10; i++)
	{
	  printf ("ptest_structure_01 -> m_IntArray[%d] : [%d].\r\n", i, ptest_structure_01 -> m_IntArray[i]);
	}

	printf ("ptest_structure_01 -> m_wszStr_01 : [%S].\r\n", ptest_structure_01 -> m_wszStr_01);
	printf ("ptest_structure_01 -> m_wszStr_02 : [%S].\r\n", ptest_structure_01 -> m_wszStr_02);
	printf ("ptest_structure_01 -> m_bstr      : [%S].\r\n", ptest_structure_01 -> m_bstr);

	return S_OK;
}

It essentially displays field values of the TestStructure01 structure contained inside the input varTestStructure01 VARIANT. The implementation for ITestObject01 compiles into a DLL named TestCOMServer01.dll.

3.3 The more important code lies in the managed client. We shall begin to examine this in more detail starting from the next section. I shall present 2 managed client code, both written in C# :

  • The first one simply imports the type library of the COM server and uses the default managed representation of the UDT as defined by the type library importer.
  • The second one will use a manually created interop assembly that customizes the managed representation of the UDT that allows for easier coding.

4. Managed Client 1.

4.1 When the type library is imported into the C# code project, the TestStructure01 UDT (which is contained inside the type library) is interpreted by the type library importer to produce the following default managed representation :

[Guid("B4F78A9C-AA12-42A0-A979-AC2FBE74791A")]
public struct TestStructure01
{
  public sbyte[] m_szStr;
  public int[] m_IntArray;
  public ushort[] m_wszStr_01;
  public ushort[] m_wszStr_02;
  public string m_bstr;
}

Note how the types of individual fields are transformed from IDL types to managed types :

  • The m_szStr field has been interpreted from an array of 256 characters into ant array of sbyte’s. This is because an unmanaged character type is the signed 8-bit integer (sbyte) in managed code.
  • The m_IntArray field remained as an array of integers.
  • m_wszStr_01 and m_wszStr_02 are reinterpreted as arrays of ushort’s. This is because an IDL wchar_t is actually an unsigned short (2 bytes) and its direct managed equivalent is the ushort.
  • The m_bstr field is reinterpreted as a managed string. At runtime, the managed string will be transformed by the interop marshaler into an unmanaged BSTR.

4.2 The full source codes for the managed client is listed below :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Remoting;
using TestCOMServer01Lib;

namespace CSTestClient01
{
    class Program
    {
        private static void UseTestStructure01()
        {
            ITestObject01 pITestObject01 = null;
            TestStructure01 test_structure_01 = new TestStructure01();
            int i = 0;

            // We create an instance of a COM object which
            // implements the ITestObject01 interface.
            // The COM object has prog ID "TestCOMServer01.TestObject01".
            pITestObject01 = (ITestObject01)Activator.CreateInstance(Type.GetTypeFromProgID("TestCOMServer01.TestObject01"));

            // Insert values into the fields of test_structure_01.
            // The m_IntArray field is an array of 10 integers.
            // We simply instantiate the array field and then
            // fill the array with values.
            test_structure_01.m_IntArray = new int[10];
            for (i = 0; i < 10; i++)
            {
                test_structure_01.m_IntArray[i] = i;
            }

            // strSource is a reference string which is used
            // to fill in the m_szStr, m_wszStr_01, m_wszStr_02
            // and m_bstr fields of test_structure_01.
            string strSource = "Hello World";
            sbyte[] sbyte_array = new sbyte[256];
            ushort[] ushort_array = new ushort[256];

            // For the m_szStr m_wszStr_01 and m_wszStr_02 fields,
            // their values must be set array element by array element.
            for (i = 0; i < strSource.Length; i++)
            {
                sbyte_array[i] = Convert.ToSByte(strSource[i]);
                ushort_array[i] = Convert.ToUInt16(strSource[i]);
            }

            test_structure_01.m_szStr = sbyte_array;
            test_structure_01.m_wszStr_01 = ushort_array;
            test_structure_01.m_wszStr_02 = ushort_array;

            // For the m_bstr field, its value may be set
            // directly by being assigned a string.
            test_structure_01.m_bstr = strSource;

            pITestObject01.TestMethod01(test_structure_01);

        }

        static void Main(string[] args)
        {
            UseTestStructure01();
        }
    }
}

Note the following pertinents points about the code above :

  • The m_IntArray array field member is assigned value rather straight-forwardly. But note that this field must be specifically declared as an array of 10 integers :
test_structure_01.m_IntArray = new int[10];

This is because there is no stipulation in the managed version of the TestStructure01 structure that the m_IntArray field be of any specific size. We must set it to 10 to match the size expected by the unmanaged UDT.

If less than the expected number of elements were declared, we will be greeted with the following exception message when we call TestMethod01() :

“An unhandled exception of type ‘System.ArgumentException’ occurred in CSTestClient01.exe

Additional information: Type could not be marshaled because the length of an embedded array instance does not match the declared length in the layout.”

If greater than the expected number of elements were declared, no exception will occur but only the expected number of elements will be copied to the counterpart field. In the case of our example, only the first 10 elements will be copied.

  • The m_bstr field is also assigned easily since it is a managed string in the managed representation of the UDT.
  • More work needs to be done for the m_szStr, m_wszStr_01 and m_wszStr_02 fields. Their values must be set array element by array element. Just like the m_IntArray field, these 3 character arrays must have their individual arrays initialized properly with adequate number of elements that matches their unmanaged counterparts.

4.3 Once all field values have been set, the TestMethod01() method is called. The managed TestStructure01 is transformed by the interop marshaler into its unmanaged COM equivalent based on information in the interop assembly. It is the unmanaged COM UDT that gets passed to the TestMethod01() method.

The program output will be as follows :

ptest_structure_01 -> m_szStr : [Hello World].
ptest_structure_01 -> m_IntArray[0] : [0].
ptest_structure_01 -> m_IntArray[1] : [1].
ptest_structure_01 -> m_IntArray[2] : [2].
ptest_structure_01 -> m_IntArray[3] : [3].
ptest_structure_01 -> m_IntArray[4] : [4].
ptest_structure_01 -> m_IntArray[5] : [5].
ptest_structure_01 -> m_IntArray[6] : [6].
ptest_structure_01 -> m_IntArray[7] : [7].
ptest_structure_01 -> m_IntArray[8] : [8].
ptest_structure_01 -> m_IntArray[9] : [9].
ptest_structure_01 -> m_wszStr_01 : [Hello World].
ptest_structure_01 -> m_wszStr_02 : [Hello World].
ptest_structure_01 -> m_bstr      : [Hello World].

4.4 I have mentioned several times the fact that the interop assembly is used by the interop marshaler to obtain information on the TestStructure01 UDT. What exactly is in the interop assembly that provides this information ?

4.5 In the next section, where a second example managed client is presented, I shall expound on this in more detail.

5. Managed Client 2.

5.1 This time, we do not directly reference the type library of the COM server. Rather. we manually create an interop assembly from the type library. This can be easily done with the help of TLBIMP.EXE :

tlbimp TestCOMServer01.dll /out:interop.TestCOMServer01.dll

5.2 Thereafter, using ILDASM.EXE, we perform a disassembly of the interop assembly to produce an IL (intermediate language) text file :

ildasm /TEXT interop.TestCOMServer01.dll /out:interop.TestCOMServer01.il

5.3 We then open the interop.TestCOMServer01.il file which is a text file and search for the declaration of the TestStructure01 structure :

.class public sequential ansi sealed beforefieldinit interop.TestCOMServer01.TestStructure01
       extends [mscorlib]System.ValueType
{
  .pack 4
  .size 0
  .custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string)
  = ( 01 00 24 42 34 46 37 38 41 39 43 2D 41 41 31 32   // ..$B4F78A9C-AA12
      2D 34 32 41 30 2D 41 39 37 39 2D 41 43 32 46 42   // -42A0-A979-AC2FB
      45 37 34 37 39 31 41 00 00 )                      // E74791A..
  .field public  marshal( fixed array [256]) int8[] m_szStr
  .field public  marshal( fixed array [10]) int32[] m_IntArray
  .field public  marshal( fixed array [256]) uint16[] m_wszStr_01
  .field public  marshal( fixed array [256]) uint16[] m_wszStr_02
  .field public  marshal( bstr) string m_bstr
} // end of class interop.TestCOMServer01.TestStructure01

The above intermediate language declaration for TestStructure01 is a declaration of the structure to be used in managed code. The marshal(…) declarations indicate how the individual fields are to be marshaled across unmanaged code. Each marshal(…) declaration maps directly to a .NET MarshalAsAttribute (see point 5.4 below). Another way to look at it is : each marshal(…) declaration indicate the counterpart data type on the unmanaged side.

The IL declaration for TestStructure01 shows clearly how it is used by the interop marshaler to convert a managed TestStructure01 structure into its managed equivalent :

  • m_szStr, delared as an array of int8 (equivalent to an sbyte) elements, is to be marshaled across to its unmanaged equivalent field as a fixed array of 256 elements of the same type (i.e. int8, or a signed character in unmanaged code).
  • m_IntArray, declared as an array of int32 (32-bit integers) elements, is to be marshaled across to its unmanaged equivalent field as a fixed array of 10 elements of the same type (i.e. integers in unmanaged code).
  • m_wszStr_01 and m_wszStr_02 are to be marshaled as fixed arrays of 256 ushorts (unsigned 2-byte short integers) which is also equivalent to the unmanaged wchar_t.
  • Finally, m_bstr is to be marshaled across to its unmanaged equivalent field as a BSTR.

5.4 As mentioned, each marshal(…) declaration maps directly to a .NET MarshalAsAttribute. Here is a C# langauge declaration for TestStructure01 which shows how each marshal(…) declaration maps to its MarshalAsAttribute equivalent :

[Guid("B4F78A9C-AA12-42A0-A979-AC2FBE74791A")]
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct TestStructure01
{
  [MarshalAs(UnmanagedType.ByValArray, SizeConst=256)]
  public sbyte[] m_szStr;
  [MarshalAs(UnmanagedType.ByValArray, SizeConst=10)]
  public int[] m_IntArray;
  [MarshalAs(UnmanagedType.ByValArray, SizeConst=256)]
  public ushort[] m_wszStr_01;
  [MarshalAs(UnmanagedType.ByValArray, SizeConst=256)]
  public ushort[] m_wszStr_02;
  [MarshalAs(UnmanagedType.BStr)]
  public string m_bstr;
}

5.5 In this second managed client sample, I will demonstrate how to modify interop.TestCOMServer01.il in order to produce a declaration for TestStructure01 with string-related fields that will be easier for C# code to work with.

5.6 Referring to the declaration for TestStructure01 in point 5.3, modify the structure to the following :

.class public sequential /*ansi*/ unicode sealed beforefieldinit interop.TestCOMServer01.TestStructure01
       extends [mscorlib]System.ValueType
{
  .pack 4
  .size 0
  .custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string)
  = ( 01 00 24 42 34 46 37 38 41 39 43 2D 41 41 31 32   // ..$B4F78A9C-AA12
      2D 34 32 41 30 2D 41 39 37 39 2D 41 43 32 46 42   // -42A0-A979-AC2FB
      45 37 34 37 39 31 41 00 00 )                      // E74791A..
  .field public  marshal( fixed array [256]) int8[] m_szStr
  .field public  marshal( fixed array [10]) int32[] m_IntArray
  //.field public marshal( fixed array [256]) uint16[] m_wszStr_01
  .field public marshal( fixed array [256]) char[] m_wszStr_01
  //.field public marshal( fixed array [256]) uint16[] m_wszStr_02
  .field public marshal( fixed sysstring [256]) string m_wszStr_02
  .field public  marshal( bstr) string m_bstr
} // end of class interop.TestCOMServer01.TestStructure01

Note the highlighted part of the text that has been emboldened. The following is a summary of the changes and their meanings :

  • The original line “.class public sequential ansi sealed beforefieldinit…” indicates that the target character set to use when transforming any string field members in the structure to their unmanaged equivalents is ANSI. By changing this to unicode as in “.class public sequential /*ansi*/ unicode sealed beforefieldinit interop.TestCOMServer01.TestStructure01…” we changed the target character set to unicode. This will have a significant impact on the m_wszStr_02 field as will be seen later (point 5.7).
  • By commenting out the line “.field public  marshal( fixed array [256]) uint16[] m_wszStr_01″ and putting in its place “.field public  marshal( fixed array [256]) char[] m_wszStr_01″, we indicate that the field m_wszStr_01 is to be a managed array of unicode characters.
  • By commenting out the line “.field public  marshal( fixed array [256]) uint16[] m_wszStr_02″ and putting in its place “.field public  marshal( fixed sysstring [256]) string m_wszStr_02″, we indicate that the field m_wszStr_02 is to be a managed string.

After making these changes, re-assemble the intermediate language code to produce a brand new interop assembly :

ilasm /DLL interop.TestCOMServer01.il /out:interop.TestCOMServer01.dll

When the new interop assembly has been imported into a managed code project, you will see, via the Object Browser, that these changes together transformed the TestStructure01 structure into the following :

[Guid("B4F78A9C-AA12-42A0-A979-AC2FBE74791A")]
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct TestStructure01
{
  [MarshalAs(UnmanagedType.ByValArray, SizeConst=256)]
  public sbyte[] m_szStr;
  [MarshalAs(UnmanagedType.ByValArray, SizeConst=10)]
  public int[] m_IntArray;
  [MarshalAs(UnmanagedType.ByValArray, SizeConst=256)]
  public char[] m_wszStr_01;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
  public string m_wszStr_02;
  [MarshalAs(UnmanagedType.BStr)]
  public string m_bstr;
}

The code presented in point 5.7 below demonstrates how these transformations helps to ease the coding for the m_wszStr_01 and m_wszStr_02 fields.

5.7 The following is a full listing for the second managed code client :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Remoting;
using interop.TestCOMServer01;

namespace CSTestClient02
{
    class Program
    {
        private static void UseTestStructure01()
        {
            ITestObject01 pITestObject01 = null;
            TestStructure01 test_structure_01 = new TestStructure01();
            int i = 0;

            // We create an instance of a COM object which
            // implements the ITestObject01 interface.
            // The COM object has prog ID "TestCOMServer01.TestObject01".
            pITestObject01 = (ITestObject01)Activator.CreateInstance(Type.GetTypeFromProgID("TestCOMServer01.TestObject01"));

            // Insert values into the fields of test_structure_01.
            // The m_IntArray field is an array of 10 integers.
            // We simply instantiate the array field and then
            // fill the array with values.
            test_structure_01.m_IntArray = new int[10];
            for (i = 0; i < 10; i++)
            {
                test_structure_01.m_IntArray[i] = i;
            }

            // strSource is a reference string which is used
            // to fill in the m_szStr, m_wszStr and m_bstr
            // fields of test_structure_01.
            string strSource = "ABCDEFGHIJ";

            // The value for the m_szStr field
            // must be set array element by array element.
            test_structure_01.m_szStr = new sbyte[256];
            for (i = 0; i < strSource.Length; i++)
            {
                test_structure_01.m_szStr[i] = Convert.ToSByte(strSource[i]);
            }

            test_structure_01.m_wszStr_01 = new char[256];
            // The m_wszStr_01 field may be set directly by
            // being assigned the output of the string.CopyTo()
            // function.
            strSource.CopyTo(0, test_structure_01.m_wszStr_01, 0, strSource.Length);

            // The m_wszStr_02 and m_bstr fields are strings.
            // Their values may be set directly by being assigned a string.
            // During marshaling, the various marshal directives
            // specified in the interop assmebly will indicate to
            // the interop marshaler how to convert each field.
            test_structure_01.m_wszStr_02 = test_structure_01.m_bstr = strSource;

            pITestObject01.TestMethod01(test_structure_01);
        }

        static void Main(string[] args)
        {
            UseTestStructure01();
        }
    }
}

The following are pertinent points connected with the code above :

  • While the array elements for the m_szStr field remains troublesome, the setting of values for the m_wszStr_01 and m_wszStr_02 fields have been made more natural and intuitive.
  • m_wszStr_01, now declared to be a char array, is set by using the String.CopyTo() method.
  • m_wszStr_02, now declared to be a string, is simply assigned the value of another string.

Now, recall that m_wszStr_02 is also decorated with the MarshalAsAttribute with UnmanagedType.ByValTStr and SizeConst=256 :

[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
public string m_wszStr_02;

The UnmanagedType.ByValTStr parameter indicate to the interop marshaler that when copying the character elements of m_wszStr_02 to the counterpart field of the destination unmanaged structure (i.e. the UDT), the target character set depends on the CharSet parameter of the StructLayoutAttribute applied to the managed TestStructure01 struct.

Now, because the managed TestStructure01 structure is imported as :

[Guid("B4F78A9C-AA12-42A0-A979-AC2FBE74791A")]
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct TestStructure01
{
   ...
   ...
   ...
}

The character set used by the destination UDT for the counterpart m_wszStr_02 field is thus taken to be Unicode. Hence, the COM object’s TestMethod01() method will produce the following expected output :

ptest_structure_01 -> m_szStr : [ABCDEFGHIJ].
ptest_structure_01 -> m_IntArray[0] : [0].
ptest_structure_01 -> m_IntArray[1] : [1].
ptest_structure_01 -> m_IntArray[2] : [2].
ptest_structure_01 -> m_IntArray[3] : [3].
ptest_structure_01 -> m_IntArray[4] : [4].
ptest_structure_01 -> m_IntArray[5] : [5].
ptest_structure_01 -> m_IntArray[6] : [6].
ptest_structure_01 -> m_IntArray[7] : [7].
ptest_structure_01 -> m_IntArray[8] : [8].
ptest_structure_01 -> m_IntArray[9] : [9].
ptest_structure_01 -> m_wszStr_01 : [ABCDEFGHIJ].
ptest_structure_01 -> m_wszStr_02 : [ABCDEFGHIJ].
ptest_structure_01 -> m_bstr      : [ABCDEFGHIJ].

6. In Conclusion.

6.1 It has been a great experience researching and preparing this topic.

6.2 I have set out to demonstrate that a COM UDT may include array types and ended up demonstrating many facinating insights into the workings of the interop assembly.

6.3 I hope to do more research into modifications of interop assemblies and will certainly share with readers any good findings.

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: