See the question and my original answer on StackOverflow

In its most simple form, COM is only the vtable binary contract plus the mother of all interfaces: IUnknown. COM is a way to reuse code without source, with components, it's some kind of a dynamic casting mechanism. Provided I know the coclasses you support (their CLSID), the interfaces they expose (their IID), and what are these interfaces' methods layout, their parameters, order, type, etc., I can use your COM server.

But to ease "communication" between your COM clients and your COM server, you can/should use some standard mechanisms/documentation and add tooling so plumbing stuff like marshaling (=serialization) will be taken care w/o any effort. This is crucial in the out-of-process case, less important with in-process (I will elude the "apartment" concept here...)

So, lots of things you'll find in COM (like registration, tooling, IDL, typelibs, etc.) are in fact optional, but also very useful (so they kinda become mandatory in the end). The purpose of things like idl (for "interface language definition") is to define and expose to your COM clients what your COM server supports so tooling can generate a lot of code automatically for you and your clients (.c, .h, .tlb). Note that nothing prevents you from implementing interfaces or coclasses without defining them in idl. Nothing obliges you to provide your .idl or your .tlb. In this case, I will only be able to use them if I known their IID, method layout etc.

Then, on top of IUnknown, Microsoft created a universal interface called IDispatch (this is also known as "Automation", or "Late binding" as opposed to "Early binding" for IUnknown), at that time targeting VB/VBA clients (before even VBScript, JScript, and lots of other COM clients, .NET supports IUnknown and IDispatch). IDispatch, if you go that route, could be the last interface you'll ever have to implement, because its semantics allows full discovery and invocation of any method, provided it supports a finite set of defined data types, the "Automation types": BSTR, VARIANT, etc.

So, if you support IDispatch, provide a TLB (typelibs) and restrict all types to Automation types, then you don't need to handle marshaling, you don't need proxies and stubs, you can forget about all this, even in out-of-process scenarios, because Microsoft implements that automatically. Back in the days, we used to call "oleaut32.dll" the "universal marshaler".

Dual interfaces are interfaces that support both IUnknown and derivates and IDispatch at the same time. They mostly exist to support C/C++ clients and Automation clients at the same time. Using Automation (BSTR, VARIANT, etc.) is a bit painful in C/C++ because they were not intended originally to be used by C/C++ clients... Note Microsoft proposes C++ smart wrappers classes: CComBSTR and CComVARIANT with ATL, or _variant_t and _bstr_t with the Windows SDK.