views:

419

answers:

4

I'm using an extensive existing COM API (could be Outlook, but it's not) in .NET (C#). I've done this by adding a "COM Reference" in Visual Studio so all the "magic" is done behind the scenes (i.e., I don't have to manually run tlbimp).

While the COM API can now be "easily" used from .NET, it is not very .NET friendly. For example, there are no generics, events are strange, oddities like IPicture, etc. So, I'd like to create a native .NET API that is implemented using the existing COM API.

A simple first pass might be

namespace Company.Product {
   class ComObject {
       public readonly global::Product.ComObject Handle; // the "native" COM object
       public ComObject(global::Product.ComObject handle) {
          if (handle == null) throw new ArgumentNullException("handle");
          Handle = handle;
       }

       // EDIT: suggestions from nobugz
       public override int GetHashCode() {
          return Handle.GetHashCode();
       }
       public override bool Equals(object obj) {
          return Handle.Equals(obj);
       }
   }
}

One immediate problem with this approach is that you can easily end up with multiple instances of ComObject for the same underlying "native COM" object. For example, when doing an enumeration:

IEnumerable<Company.Product.Item> Items {
   get {
      foreach (global::Item item in Handle.Items)
         yield return new Company.Product.Item(item);
   }
}

This would probably be unexpected in most situations. Fixing this problem might look like

namespace Company.Product {
   class ComObject {
       public readonly global::Product.ComObject Handle; // the "native" COM object
       static Dictionary<global::Product.ComObject, ComObject> m_handleMap = new Dictionary<global::Product.ComObject, ComObject>();
       private ComObject(global::Product.ComObject handle) {
          Handle = handle;
          handleMap[Handle] = this;
       }
       public ComObject Create(global::Product.ComObject handle) {
          if (handle == null) throw new ArgumentNullException("handle");

          ComObject retval;
          if (!handleMap.TryGetValue(handle, out retval))
              retval = new ComObject(handle);
          return retval;             
       }
   }
}

That looks better. The enumerator changes to call Company.Product.Item.Create(item). But now the problem is the Dictionary<> will keep both objects "alive" so they will never be garbage collected; this is likely bad for the COM object. And things start getting messy now...

It looks like part of the solution is using a WeakReference in some way. There are also suggestions about using IDisposable but it doesn't seem very .NET-friendly at all to have to deal with Dispose() on every single object. And then there's the various discussions of when/if ReleaseComObject should be called. There is also code over on http://codeproject.com that uses late binding, but I'm happy with a version-dependent API.

So, at this point I'm not really sure what is the best way to proceed. I'd like my native .NET API to be as ".NET-like" as possible (maybe even embedding the Interop assembly with .NET 4.0) and w/o having to employ heuristics like the "two dots" rule.

One thing I thought of trying is to create an ATL project, compile with the /clr flag and use the C++'s compiler COM support (Product::ComObjectPtr created by #import) rather than .NET RCWs. Of course, I'd generally rather code in C# than C++/CLI...

+1  A: 

You are making it unnecessarily difficult. It is not a "handle", there's no need for reference counting. A __ComObject is a regular .NET class and subject to normal garbage collection rules.

Hans Passant
So you're saying the second bit of code I've shown is right (enough) for all "normal" situations? What name would you like besides "Handle" (I just borrowed this from WinForms to avoid inventing something new...).
Dan
No, the first snippet is good enough. The garbage collector knows when to clean up the reference. I can't suggest a better name without knowing what the real coclass name is. Not "ComObject".
Hans Passant
Say I'm iterating through a collection of COM objects, with the first snippet I'm going to get different .NET objects each time--that doesn't seem right at all.
Dan
You lost me there. Why would a collection of different COM objects not map to a collection of different .NET objects? I suspect you ought to consider a class factory.
Hans Passant
I want this to be a very .NET-friendly API, so instead of an *Items* property returning *Product.ItemCollection* of *Product.Item* COM objects, instead I want *Items* to return *IEnumerable<Company.Product.Item>*. That means I'm creating an *Item* wraper each and every time I do the iteration. (See recent edits).
Dan
I don't get the problem, why don't you wrap right away, when the COM object gets created? One .NET wrapper object for one COM object. No point in wrapping later.
Hans Passant
How do you make the iterator (see updated sample) work "as expected" (no new .NET objects each time through) w/o using a `Dictionary<>`?
Dan
Okay, I'm beginning to see the problem: Items is a property of the COM object. WeakReference is too ugly, create a new wrapper. Be sure to override GetHashCode() and Equals() so that identity is based on the COM object instead of the wrapper instance.
Hans Passant
I've edited with your suggestions...if that's really the best solution, than C++/CLI is the only way: need help from the compiler (templates) or pre-processor (macros) to write all of that boilerplate code.
Dan
We are definitely not on the same page with this. C++/CLI doesn't solve anything, you'd have the exact same wrapper issue. The CLR's COM interop layer does take care of the __ComObject identity mapping but that's not accessible to C++/CLI either. Again, I'm thinking you are making this way more difficult then it needs to be. Good luck with it!
Hans Passant
C++/CLI gives me templates and/or the C pre-processor which can make it easier to reduce the amount of boilerplate code *I* have to write.I really don't want to make it any more difficult than it has to be--which is why I'm asking here on SO.And if it really is as easy as you indicate it is (and I'm not saying it isn't), then why aren't there more .NET-friendly wrappers out there; it doesn't seem like it would take more than a week or two to wrap Outlook/Word/Excel/etc.
Dan
You started this quest with the conclusion that the COM interface you were dealing with was crappy and needed to be fixed. I don't doubt that. Are the Office interfaces just as crappy to require a wrappers? Personally, I think it is astonishing that they managed to keep it consistent and working well through, what, eight major releases. Good design, up front. If the COM interfaces you have to deal with suck, fix the COM interfaces. I certainly sounds like you know how to do that!
Hans Passant
A: 

It sounds like you are looking to create a simpler .NET API around your complex COM API. As nobugz said, your ComObject classes are real, native .NET objects that internally contain the unmanaged references to your actual com objects. You don't need to do anything funky to manage them...just use them like they are normal .NET objects.

Now, in regards to presenting a "prettier face" to your .NET consumers. There is an existing design pattern for this, and its called a Facade. I am going to assume that you only really need a part of the functionality that these COM objects provide. If that is the case, then create a facade layer around your com interop objects. This layer should contain the necessary classes, methods, and support types that provide the neccesary functionality to all of the .NET clients with a friendlier API than the com objects have themselves. The facade would also be responsible for simplifying oddities like converting whatever the com objects do for events with normal .NET events, data marshaling, simplification of com object creation, setup, and teardown, etc.

While in general, abstractions should be avoided, as they tend to add work and complexity. However sometimes they are necessary, and in some cases can greatly simplify things. If a simpler API can improve the productivity of other team members who need to consume some of the functionality provided by a very complex com object system, then an abstraction provides tangible value.

jrista
If you "... don't need to do anything funky ..." then why are there so many questions/problems related to COM interop? You mention yourself that the "facade would be responsible for ... simplification of com object creation, setup, and teardown, etc." So my question is exactly how write such a facade so that client code has absolutely no idea it is even using a COM object?
Dan
Well, as the Facade design pattern states, it acts like a simplification buffer between one system and another. Assuming you know what functionality the consumers of this facade need, you would design a whole new API that provided a clean, simple, .NET interface to that functionality. A properly designed facade is wholly and entirely self-contained and independent. There should be no leakage of the system it wraps to the system it buffers. That means you need new classes, enums, methods etc. designed specifically for the facade, which are mapped to any com types and methods.
jrista
As I mentioned in my final paragraph, abstractions are a necessary evil. They are evil because they add work and complexity in one sense. They are necessary because they reduce work and complexity in another sense. The benefit of a facade in your situation is multi-faceted: you simplify a complex system, you buffer your consumers from legacy complexity, you hide COM oddities behind .NET normalcy, etc. You could choose to require everyone consume the COM object wrappers directly...however, this creates a negative coupling that could have dire consequences in the future...
jrista
+3  A: 

You, yourself aren't dealing with COM objects. You are already dealing with a facade that was created the moment you added a reference to the COM binary to your project. (.NET) will generate a facade for you, therefore simplifying the task of using COM objects to simply using regular .NET classes. If you do not like the interface that's generated for you, you should probably create a facade to the existing facade. You don't have to worry about COM intricacies, because that's already been done for you (there may be some things you do need to worry about, but I think they are few and far between). Just use the class as a regular .net class because that's exactly what it is, and deal with any problems as they arise.

EDIT: One of the problems you might experience is nondeterministic COM object destruction. The reference counting that's taking place behind the scenes relies on garbage collection so you can't be sure when your objects will be destroyed. Depending on your application you may need more deterministic destruction of your COM objects. To do this you would use Marshal.ReleaseComObject. If this is the case, then you should be aware of this gotcha.

Sorry, I would post more links, but apparently I can't post more than 1 without first getting 10 reputation.

KarlW
Well, I'd like to know what those "any problems" may be know so that I can be sure I'm completely hiding any COM-related intricacies from clients. Given the numerous .NET/COM topics on SO and elsewhere, I’m finding it difficult to believe that things really are that simple.
Dan
[ after your edit ]: yes, these are all the little hiccups from which I'd like to completely insulate clients of my classes.
Dan
+2  A: 

The biggest problem I've found with bringing COM objects into .NET is the fact that the garbage collector runs on a different thread and the final release of the COM object will often (always?) be called from that thread.

Microsoft deliberately broke the COM threading model rules here which state that with apartment threaded objects, all methods must be called from the same thread.

For some COM libraries this is not a big deal, but for others it's a huge problem - particularly for libraries that need to release resources in their destructors.

Something to be aware of...

cantabilesoftware