Now, I'm aware that if the library is in .NET, it's a little pointless to access it via COM, however, I am a bit perplexed because if I were to ask someone to write a library and expose it via COM, that person should be free to do so with any language.
It shouldn't matter to me which language that COM library is written in, so why does it matter?
For reference, this is what happens when you use tlbimp on a .tlb file generated from a .NET library:
C:\dev>tlbimp test.tlb
Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.
TlbImp : error TI1029 : Type library 'test' was exported from
a CLR assembly and cannot be re-imported as a CLR assembly.
Additionally, my test COM library uses IUnknown, supporting only early-bound COM interop.
tlbimp is only the beginning of the problem.
Every COM-visible .NET object implements IManagedObject. Every time you try to call an object through a COM interface, .NET does a QueryInterface for IManagedObject, and if it succeeds the object is unwrapped and accessed directly instead. Eliminating the interop call in this way is considered a performance optimization.
This is a serious issue in COM-based interop scenarios where multiple components may be implemented in .NET languages. If each .NET component implements its own tlbimp-generated version of the COM interface, they will be unable to communicate with each other using that interface. Even though each tlbimp'ed copy of the interface has the same COM GUID, .NET considers them separate interfaces. The "correct" solution to this problem is to define the COM interface in a primary interop assembly, and have every .NET-implemented component use that same copy of the interface, but if there's no coordination between component developers that's pretty unlikely to happen.
This problem was once highlighted by a Microsoft developer at http://blogs.msdn.com/b/jmstall/archive/2009/07/09/icustomqueryinterface-and-clr-v4.aspx along with a solution for .NET 4, but it was based on a pre-release version and doesn't work in the final. I'm not aware of anywhere else it's been acknowledged by Microsoft.
One solution is to forego the interface in the .NET-to-.NET scenario and use reflection instead. This may work in many cases but of course isn't very performant. Possibly the best solution is to use unmanaged code to aggregate each of your managed components and reject attempts to QueryInterface for IManagedObject. This is similar to what's described in the above blog entry, but doesn't rely on .NET features that no longer work as described in that blog.
The .NET interop behavior is, IMO, pretty terrible, and clearly violates COM rules (same IID == same interface, and shouldn't have to care what language the object is implemented in). But .NET has behaved this way from the beginning and no amount of feedback from developers bitten by this behavior has changed the minds of anyone on the .NET team.
Tlbimp can see from the type library that it was produced from a .NET assembly. It just balks at what you are trying to do. Which doesn't make sense, if you want to use a .NET assembly then just add a reference to it.
You can fool the machine by using late-bound COM, most easily done with the dynamic keyword in C#. That still doesn't fool the CLR, it can see that it interops with a managed assembly and won't actually create the COM-callable wrapper. You most typically do this to write a test program to test your COM server. The implication is that your test doesn't actually test the way your COM server is going to be used in practice. Very basic things like converting variants to objects and back just won't be tested at all. And may well give you very unpleasant surprises when you use the assembly in production.