VSJ
Wrox for Visual Studio 2010 - we've got it covered - click for details
The independent source for software developers
Home
Email Newswire
.NET Zone
Java Zone
XML & Web Services Zone
Database Development Zone
Architecture Zone
BlackBerry Zone
News
Articles
Free Downloads
Training Courses
Books
Institution of Analysts & Programmers
Code Bin
DevWeek & SQL Server DevCon
About VSJ
Advertising Information
Contacts
Follow VSJ on Twitter
Articles
Into the IUnknown

Mike James explores the DIY approach to using COM objects in .NET

By Mike James

Published: 31 May 2006

Yes you can cue the spooky music, because using COM objects from .NET can be a scary subject. The reason is not just that it is poorly documented, but the range of implementation styles of COM objects is so great that it’s confusing. You might think at this stage: “if it’s so difficult why bother?” The answer is that despite the fact that .NET makes COM obsolete it is deeply embedded in the existing Windows technology. First generation Windows API technology was based on the DLL as function library. Second generation Windows used the same basic idea, but collected the functions together into COM Interfaces. Microsoft has provided .NET classes that wrap much of the API, but there is still a great deal that is simply untouched. Then of course there are the legacy COM objects that you and I created in an effort to take advantage of object-oriented code reuse. It looks as if COM and the humble DLL is going to be with us for a while yet.

IJW or It Just Works is perhaps the aspect of COM Interop that Microsoft likes to emphasis. The idea is that you add a reference to a COM object and the system imports it for you by automatically creating a class wrapper for it. It has to be said that this is the ideal, and you should always try importing a COM object by adding a reference to the file it is stored in before resorting to the techniques described in this article. The big problem with IJW is that if It Doesn’t Just Work (i.e. IDJW) then you really are dumped into a great unknown territory, and as the key COM interface is named IUnknown perhaps that should be “great IUnknown territory”.

The current supplied documentation on manually creating wrappers is terrible. What has been provided is spread out under a range of different topic headings and seems to have a strange reluctance to simply tell you what it is all about. If you study the few supplied examples and try to match them to the documentation it is still very difficult to discover exactly what is going on. To really understand, you need to know a little about how COM works and some of the variations on the basic theme that you encounter in real life.

Just enough COM

The idea of a COM object is very simple. It’s essentially a C++ class with some public virtual methods that you can call. In theory it doesn’t have to be implemented in C++, but that’s the language that the technology was based on. A virtual method is simply a function that you can access via a pointer, and a COM object organises these pointers to functions into an array called a vtable. At a slightly higher level we can view this vtable as an Interface, i.e. a collection of functions of defined type and signature. It’s important to notice that the type/signature aspects of the Interface aren’t part of its runtime implementation – it really is just an array of function pointers, and you could use it to call one of the functions with any parameters you care to supply. Of course if you don’t supply the parameters it expects, nothing works and the whole thing crashes.

COM formalises the Interface idea to allow you to write code that “discovers” what interfaces an object supports at runtime. The idea is that all COM objects offer a basic interface called IUnknown, which has just three methods: QueryInterface, AddRef and Release. AddRef and Release just increment and decrement a use counter so that the COM object knows when it still has users, and you can mostly ignore them. The QueryInterface method is much more useful in that it will return a pointer to any other Interfaces the object supports. Rather than using a name to specify the Interface you want to find, you specify an Interface Id or Iid – the Guid (Globally Unique Identifier) that the COM object stored in the registry when it was installed. Indeed the COM object itself is identified by another Guid in the registry known as a clsid or CLaSs ID.

Thus far the story is simple. To use a COM object you first create an instance of it using the standard API function CoCreateInstance. You pass it the clsid of the COM object, and it passes back to you a pointer to the object’s IUnknown interface. You then pass the IUnknown QueryInterface the Iid of the Interface you really want to use and it passes back a pointer to it. You also need to know that every Interface “inherits” from IUnknown and so every vtable starts off with a pointer to QueryInterface, AddRef and Release. Interfaces can also “inherit” from other interfaces but this really only means that the vtable starts off with the entire vtable for the base Interface before the pointers to any new functions.

Interfaces based on IUnknown are, as I keep repeating, very simple. In fact they are so simple that COM needed more infrastructure to cope with the real world. There is a wide range of additional technologies that have been added to the basic IUnknown mechanism to make using COM more “automatic”. The two that you need to know about are IDispatch Interfaces and IDL.

An IDispatch Interface is a special type of IUnknown Interface that includes methods that allow methods to be called by name at runtime. To call a method in a simple IUnknown Interface all you need is to know where in the vtable the function pointer is. So for example to call MyMethod you would need to know that it is the 6th method defined in MyInterface and so its pointer is in vtable[5] (given that the first function pointer is in vtable[0] and points at QueryInterface.) Using IDispatch you can call the function by name. You simply use the GetIDsOfNames method and then the Invoke method of the IDispatch interface. You can think of this as equivalent to the .NET reflection mechanism that allows late binding. In the case of COM the IDispatch Interface was mostly used by other software tools to allow the programmer to access Interfaces without the bother of learning COM. IDispatch is what allows you to use an ActiveX object or Automation facility from script without worrying about COM. The important point here is that this is still what .NET uses when it constructs a wrapper for a COM object. Indeed most of the COM interop IJW facilities only “Just Work” when the COM object has an IDispatch Interface – and there are great many that only support the pure IUnknown interface.

We also need to know a little about IDL – Interface Definition Language – which is used to describe the type of an Interface. It allows the COM designer to specify Guids for classes and Interfaces, Interface names, method names, method signatures, how parameters are passed and even general data types to be used in conjunction with the COM object. You can think of IDL as a general purpose data type description language. Most COM objects have an IDL file associated with them – but they don’t have to. You can find the IDL files corresponding to the COM objects that implement parts of the Windows API in the Platform SDK which you can download from the Microsoft website. IDL files aren’t actually used directly by a programming language but are converted into either C/C++ header files or into “type libraries” by the IDL complier – MIDL. A type library is what you need if you are going to try to use a COM object in IJW mode. This is what the .NET system uses to create class wrappers for the COM object but as already explained it only works if the object supports IDispatch. In practice even an IDispatch Interface isn’t a guarantee that automatic wrapping will be possible – it has to be OLE Automation compatible and use nothing out of the ordinary for it to be 100% successful.

As you can see, the basic COM mechanism is simple, but it has been elaborated to a point where is can seem very complicated. When you add to this the fact that COM programmers often found “inventive” ways of creating COM objects and Interfaces, then things become even more difficult.

There is also the fact that COM programmers use different styles to write their IDL files and to implement their COM Interfaces. Reading though some IDL files quickly raises the question “how many ways does a COM programmer need to define a 32-bit word?” – DWORD, HRESULT, ULONG, HWND, Reserved, Flag and so on… all just mean Int32. There are also generations of Windows to take into account. You will find FARPOINTER being used as a data type in the days when pointers were mostly 16-bit, and a 32-bit pointer was the exception. You also have the problem of ANSI 8-bit versus UNICODE 16-bit character data, and the large number of different ways there are of representing sophisticated data types such as strings, arrays and structures. It all adds up to a very threatening mess for any programmer about to tackle an IUnknown, and it’s a good example of how an initially good idea was allowed to run wild. What you need to keep in mind is that the IUnknown interface is essentially simple, and if you proceed step-by-step you can get it all working.

Creating COM objects

It’s time to put theory into practice, and we might as well start at the beginning and look at how we can create a COM object. As an example, let’s use the Windows TaskScheduler, which has a fairly complicated COM-based API. You can find a header file MsTask.h and an IDL file MsTask.idl for it in the Platform SDK (in the Include directory). Reading either file will supply all the information you need about the Guids, data types and Interfaces that the COM object is associated with.

If the COM object is playing by the rules, there are three basic ways to bring an instance into existence using .NET:

1. Use the COM API function CoCreateInstance as if you were a C++ programmer.
You can do this in C# using the standard Pinvoke mechanisms. For example, to create an instance of the TaskScheduler we first need to define CoCreateInstance.

[DllImport("ole32.Dll")]
static public extern uint
	CoCreateInstance(
	ref Guid clsid,
	[MarshalAs(UnmanagedType.IUnknown)]
	object inner,
	uint context,
	ref Guid uuid,
	[MarshalAs(UnmanagedType.IUnknown)]
	out object rReturnedComObject);
This is simply a matter of looking up its definition in the documentation and translating it to a Pinvoke style declaration. The only unusual part is the use of UnmanagedType.IUnknown type which Marshals the returned pointer to IUnknown into a object wrapper of type System._ComObject. To use this to create an instance of the TaskSchduler we first need its Clsid (read from the IDL file) and the Iid of the IUnknown interface (which is standard):
Guid CLSID_CTask=new Guid(
"{148BD52A-A2AB-11CE-B11F-
	00AA00530503}");
Guid IID_IUnknown = new Guid(
	"00000000-0000-0000-C000-
	000000000046");
With the addition of a constant and an object to store the returned Interface pointer we can now call CoCreateInstance:
object OSched;
const uint CLSCTX_INPROC_SERVER = 1;
CoCreateInstance(
	ref CLSID_CTask,
	null,
	CLSCTX_INPROC_SERVER,
	ref IID_IUnknown,
	out OSched);
The OSched object is now a reference to the COM class and its IUnknown Interface. How this is used is described later.

2. Use .Net reflection facilities.
The second way of creating a COM object is to use the .NET reflection facilities to create a Type from a Guid and then an object from a type:

Type TSched = Type.GetTypeFromCLSID(
	CLSID_CTask);
OSched = Activator.CreateInstance(
	TSched);
As before, OSched is now an object that wraps the COM class and its IUnknown interface.

3. Use .NET COM interop facilities
The final method is to make direct use of the .NET COM interop facilities to define a class that wraps the COM object:

[ComImport, Guid("
	148BD52A-A2AB-11CE-B11F-
	00AA00530503")]
	class CTaskScheduler
	{
	};
Now we have a .NET class that wraps the COM object and IUnknown, and we can create an instance in the usual way:
CTaskScheduler OSched =
	new CTaskScheduler();
In this case the .NET framework does all of the initialisation and calls to CoCreateInstance etc., and looks after the lifetime of the object.

Which of the three methods is best? The answer is that using the ComImport attribute has to be the easiest, but it sometimes doesn’t work, or sometimes doesn’t provide you with the control you need over how or when the class is created. In such cases try one of the others.

Raw IUnknown

At this point we have the COM object and, implicitly, its IUnknown Interface. However, .NET doesn’t expect you to want to use the object’s IUnknown Interface, and therefore doesn’t provide an easy way to do it. For other Interfaces the COM Interop makes it very easy – in fact all you have to do is cast the object to a suitable Interface type, and COM Interop looks after calling QueryInterface and wrapping the returned Interface pointer. This is certainly the way that you should use COM in .NET, but as a demonstration of how you can use a COM Interface directly let’s explore IUnknown.

The first thing to say is that .NET does provide methods that you can use to work with IUnknown, but they aren’t entirely satisfactory. Again it’s important to note that this isn’t the way that you would use a typical COM Interface. The Marshal static object has a great many methods that can be used in the conversion of managed and unmanaged types. In this case there are four methods of direct relevance:

  • GetIUnknownForObject – which returns a pointer to a pointer of the object’s IUnknown Interface
  • QueryInterface – which returns a pointer to a pointer for a specified Interface
  • AddRef – which increments the use count
  • Release – which decrements the use count
All of these functions use the IntPtr type to specify and return pointers to Interface pointers. All will become clear with a simple example. First get a pointer to the IUnknown Interface pointer for the scheduler:
IntPtr pIUnk=
	Marshal.GetIUnknownForObject(
	OSched);
Now that we have the pointer we can use AddRef and Release as a demonstration that it works:
Int32 usecount1 =
	Marshal.AddRef(pIUnk);
Int32 usecount2 =
	Marshal.Release(pIUnk);
Finally to show how a pointer to another interface would be retrieved we use QueryInterface to get a pointer to the ITaskScheduler Interface (Iid obtained from the IDL file):
IntPtr pISched;
Guid IidSched=new Guid(
	"148BD527-A2AB-11CE-B11F-
	00AA00530503");
Int32 result= Marshal.QueryInterface(
	pIUnk, ref IidSched, out pISched);
When this method returns we have a pointer to the ITaskScheduler Interface pointer.

This is all very well, but how do we call the methods in an Interface given that all we have is a pointer to the Interface pointer? There might well be a way to cast such a pointer into an Interface wrapper type, but I can’t find one! One possible solution is to access the vtable directly as a pointer array, and then cast each pointer to a function to a suitable delegate. Despite my best efforts I’ve only had limited success with this approach in C# – it’s probably something to do with the calling convention used as it tends to fail when there are parameters.

Casting Interfaces

The examples given above of using the IUnknown Interface are perhaps a little too low-level for most applications. Once you have created the COM object and its IUnknown Interface, you don’t really need to access it, because the .NET Framework knows how to use it on your behalf. It you want to call methods defined in MyInterface, all you have to do is define a C# Interface with the correct number of functions, in the same order as the vtable and with the correct parameters. You then simply cast the COM object to this Interface type and the Framework automatically calls the QueryInterface method and wraps the returned pointer to an Interface pointer in a new class. You can then use all of the methods of the new class as if nothing unusual was afoot.

Of course there are a few fine details to consider, and the main problem, as with all Interop tasks, is sorting out how parameters should be handled. In principle this is no different from parameter handling when calling a DLL function, but there are a few new features. Let’s look at a real example and implement the definition of the ITaskScheduler Interface. The IDL file defines this as:

interface ITaskScheduler : IUnknown
{
// Methods:
	HRESULT SetTargetComputer(
		[in] LPCWSTR pwszComputer);
	HRESULT GetTargetComputer(
		[out] LPWSTR * ppwszComputer);
	HRESULT Enum(
		[out] IEnumWorkItems **
			ppEnumWorkItems);
	HRESULT Activate(
		[in] LPCWSTR pwszName,
		[in] REFIID riid,
		[out] IUnknown ** ppUnk);
	HRESULT Delete(
		[in] LPCWSTR pwszName);
	HRESULT NewWorkItem(
		[in] LPCWSTR pwszTaskName,
		[in] REFCLSID rclsid,
		[in] REFIID riid,
		[out] IUnknown ** ppUnk);
	HRESULT AddWorkItem(
		[in] LPCWSTR pwszTaskName,
		[in] IScheduledWorkItem *
			pWorkItem);
	HRESULT IsOfType(
		[in] LPCWSTR pwszName,
		[in] REFIID riid);
}
You can generally copy and paste the IDL or the header file specification of the Interface into your C# or VB program and then edit it to create a working definition. It is helpful to realise that only the order of the functions matters in relating them to the vtable. So for example an absolutely minimum specification for this Interface would be:
[Guid("148BD527-A2AB-11CE-B11F-
	00AA00530503"),
	InterfaceType(
ComInterfaceType.InterfaceIsIUnknown)]

private interface ITaskScheduler
{
	void SetTargetComputer();
	void GetTargetComputer();
	void Enum();
	void Activate();
	void Delete();
	void NewWorkItem();
	void AddWorkItem();
	void IsOfType() ;
}
This definition defines where each function is in the vtable, but without parameter information. Of course if any function really doesn’t have any parameters you could use this definition to call it without further refining the definition of the Interface. This allows us to work incrementally when developing an Interface definition. A simple way to work is to paste the IDL or header file definition into the C# program and initially comment out all of the parameters.

The Guid quoted at the start is the Idd of the Interface obtained from the IDL file and this is used by the Framework when it calls QueryInterface. Notice that the ComInterfaceType doesn’t mean that this is an IUnknown interface but that it should be treated by the Framework as if it was derived from IUnknown. Also notice that the Framework saves you the trouble of having to define the QueryInterface, AddRef and Release functions as they are part of every IUnknown Interface. You start the definition from the first new function added to the basic IUnknown.

To try out an Interface definition it’s usually a good idea to pick the simplest function defined and attempt to define its parameters. If this works then you know that you have the Interface you want and it is functioning correctly. Defining the rest of the Interface is then just a matter of crafting the parameter definitions. Let’s start with GetTargetComputer:

HRESULT GetTargetComputer(
	[out] LPWSTR * ppwszComputer);
This translates to:
void GetTargetComputer(
	[Out, MarshalAs(
	UnmanagedType.LPWStr)]
	out StringBuilder ppwszComputer);
There are a number of traps waiting for the eager programmer in this definition. The first is that you do need to use LPWStr because this COM Interface uses Unicode. You also need the out modifier because this is a pointer to a LPWStr – most strings are passed as pointers to strings not, as here, pointers to pointers to strings. There is also the small puzzle as why the return type is void and not Int32 or similar? The answer is that the COM Interop system converts all HRESULT error codes into exceptions which you should handle.

As a result we don’t get a HRESULT and so the function should be defined as void.

With this just this change to the Interface definition we can test it. To get the new Interface we simply cast the original object:

ITaskScheduler ITaskSchd =
		OSched as ITaskScheduler;
Now we can use it but be careful to only use those methods that you have actually defined, i.e. GetTargetComputer in this case:
StringBuilder name = new
	StringBuilder(50);
ITaskSchd.GetTargetComputer(out name);
MessageBox.Show(name.ToString());
Now that we have this much working we can move on to more interesting methods. As methods within an Interface tend to use the same range of parameters, Interface definition tends to become easier as you progress though the list. So for example, only a small modification is required to define SetTargetComputer:
void SetTargetComputer([In,
	MarshalAs(UnmanagedType.LPWStr)]
	String pwszComputer);
Notice that in this case a String is easier to use and this parameter is a pointer to a string.

Interfaces as parameters

In the simple COM model the only way Interfaces are returned is via QueryInterface and a suitable Iid. COM programmers didn’t keep to this simple model as they found it more or less essential to write other Interface functions that returned Interface pointers – sometimes with the help of an Iid but mostly without one. Consider for a moment NewWorkItem:
HRESULT NewWorkItem(
	[in] LPCWSTR pwszTaskName,
	[in] REFCLSID rclsid,
	[in] REFIID riid,
	[out] IUnknown ** ppUnk);
This takes a clsid and an Iid and returns a pointer to an IUnknown Interface pointer to the Interface specified. This appears to be horribly complicated and probably not worth trying to translate, but in fact the .NET Framework can handle it with ease. The translation is:
void NewWorkItem([In, 
	MarshalAs(UnmanagedType.LPWStr)]
	String pwszTaskName,
	[In]
		ref Guid rclsid,
	[In]
		ref Guid rIid,
	[Out, MarshalAs(
		UnmanagedType.IUnknown)]
		out object ppUnk);
The string is marshalled as in earlier functions, and the Framework knows how to deal with converting a Guid to a string of bytes which the function needs. What is more surprising is that the Framework also implements the marshalling of the returned Interface into an object wrapper. To use this function we simply need the Clsid of the Task object:
Guid CTask = new Guid(0x148BD520,
	0xA2AB, 0x11CE, 0xB1,0x1F, 0x00,
	0xAA, 0x00, 0x53, 0x05, 0x03);
…and the Iid of the ITask Interface:
Guid ITask = new Guid(0x148BD524,
	0xA2AB, 0x11CE, 0xB1, 0x1F, 0x00,
	0xAA, 0x00, 0x53, 0x05, 0x03);
Next we use these to create a new Task object and its associated Interface:
object itask;
ITaskSchd.NewWorkItem("Test",
	ref CTask, ref ITask, out itask);
To make use of the ITask Interface we have to cast it to a .NET ITask Interface type and this means that we have to define another Interface. This isn’t difficult, but in this case we can also cast it to one of the standard Interfaces that many COM objects support.

It’s also worth noting that CTask is a COM class in its own right and can be created complete with its IUnknown Interface using any of the three methods described earlier. For example:

[ComImport, Guid(
	"148BD520-A2AB-11CE-B11F-
	00AA00530503")]
	class CTask{}
After this you can create CTask objects using “new” and cast them to ITask Interfaces.

Standard COM Interfaces

As COM developed there was a need to define standard Interfaces that would do common tasks in a uniform way. For example, many COM objects need to save their state, and rather than expect the user to create a custom Save routine, the IPersist Interface and its derived Interfaces were introduced so that the user could simply call IPersistFile.Save and the COM object would save its state to disk. Of course not all COM objects implement IPersistFile or any other standard Interface, but when they do it’s worth making use of them. In this case the TaskScheduler object implements both IUnknown and IPersistFile and we can use this to save the new task that we have just created.

Most of the standard COM Interfaces are defined in:

System.Runtime.InteropServices.ComTypes;
…which is new in .NET 2.0. You can find older definitions in the UCOM namespace for .NET 1.1.

Adding a:

using
System.Runtime.InteropServices.ComTypes;
…allows us to write:
IPersistFile PFile =
	itask as IPersistFile;
PFile.Save(null, true);
That is, we cast to the predefined Interface type IPersistFile and then use its Save method to create an unscheduled task in the default machine’s Scheduler.

Inheritance and Interfaces

You can continue in this way expanding the facilities that you have access to in .NET. Mostly it’s just a matter of pasting and editing the IDL definitions. However, you have to be careful about taking account of Interface Inheritance. For example, if you plan to implement the ITask Interface its IDL definition starts:
interface ITask;
[
	local,
	object,
	uuid(
148BD524-A2AB-11CE-B11F-00AA00530503),
	pointer_default(unique)
]
interface ITask : IScheduledWorkItem
{
// Properties that correspond to
// parameters of CreateProcess:
	HRESULT SetApplicationName(
		[in] LPCWSTR
		pwszApplicationName);
…and so on.

The line “interface ITask : IScheduledWorkItem” means that ITask inherits from IScheduledWorkItem. For Interfaces inheritance is particularly simple. All that happens is that the vtable starts off with all of the items in IScheduledWorkItem and then the new functions in ITask. Thus the first function in our .NET Interface definition isn’t SetApplicationName but CreateTrigger – the first function in IScheduledWorkItem:

interface IScheduledWorkItem;
[
	local,
	object,
	uuid(
a6b952f0-a4b1-11d0-997d-00aa006887ec),
	pointer_default(unique)
]
interface IScheduledWorkItem :
	IUnknown
{
// Methods concerning scheduling:
	HRESULT CreateTrigger(
		[out] WORD * piNewTrigger,
		[out] ITaskTrigger **
			ppTrigger);
…and so on.

That is, Interfaces really do implement inheritance as concatenation of vtables. For some reason the fact that an Interface inherits from another is often overlooked, or at best well hidden, in documentation and a careful look at the IDL file is usually a good idea.

Preserve our Sig

The fact that COM Interop is clever enough to intercept the HRESULT return value, decode it and then trigger an exception if it is an error code is usually a good thing. However, sometimes methods return status information in HRESULT. They shouldn’t really, but in COM most things happen. For example, in the Outlook Express COM API – defined in msoeapi.idl and msoeapi.h which are both in the Platform SDK Includes folder – the GetFirstFolder and GetNextFolder functions in the IStoreNamespace Interface return an HRESULT of 0 if they have retrieved a sub-folder and 1 if there are no more sub-folders. To process this result we have to add the [PreserveSig] attribute which forces the COM Interop mechanism not to interfere with HRESULT:
[PreserveSig]
Int32 GetFirstSubFolder(
[In, MarshalAs(UnmanagedType.U4)]
		Int32 dwFolderId,
[Out, MarshalAs(UnmanagedType.Struct)]
		out _FOLDERPROPS pProps,
[Out, MarshalAs(UnmanagedType.U4)]
		out Int32 phEnum);

[PreserveSig]
Int32 GetNextSubFolder(
[In, MarshalAs(UnmanagedType.U4)]
		Int32 hEnum,
[Out, MarshalAs(UnmanagedType.Struct)]
		out _FOLDERPROPS pProps);
Now you can test the HRESULT return value to see if there is a folder to process, but notice that you now also need to test it for more general error conditions and throw your own exceptions to signal the problem. An interesting trap waiting for the innocent programmer is that if you leave off the [PreserveSig] then everything works, but the return value is always set to zero by COM Interop.

What we can’t do

At this point you might be feeling that COM is well and truly mastered in .NET, but there are still a few things that I cannot find a way of doing without moving to C++. The most important has already been mentioned. There seems to be no way to retrieve a vtable and convert it into an array of delegates or an equivalent .NET Interface. You might well argue that this isn’t necessary as we can always use a COM Interface via COM Interop and never need such low-level techniques. However, this is to ignore the existence of COM perversions like WAB – the Windows Address Book. This isn’t created using any of the standard COM creation methods. Instead there is a new API function which works without the use of a clsid or Iid. The function returns two pointers to Interface pointers and without the help of a Guid COM Interop fails to recognise them as COM Interfaces. The only way of dealing with them seems to be to treat them as vtables and convert each function pointer to a delegate – easy in C++, but seemingly impossible in C#.


Dr. Mike James’ programming career has spanned many languages, starting with Fortran. The author of Foundations of Programming, he has always been interested in the latest developments and the synergy between different languages.


Return to Articles

NetAdvantage Free Trial - click for details