Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I wrap a COM object in a native .NET class?

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...

like image 806
Ðаn Avatar asked Feb 13 '10 20:02

Ðаn


People also ask

What is wrapper C#?

A wrapper class is any class which "wraps" or "encapsulates" the functionality of. another class or component. These are useful by providing a level of abstraction from the. implementation of the underlying class or component; Not much clear , Right ?

Which wrapper is useful when .NET client talks to COM?

The COM Callable Wrapper (CCW) will be used to wrap the . NET components and used to interact with the COM clients. CCW will be created by the . NET utility RegAsm.exe.

What is COM callable wrapper in C#?

COM callable wrappers are invisible to other classes running within the . NET runtime. Their primary purpose is to marshal calls between managed and unmanaged code; however, CCWs also manage the object identity and object lifetime of the managed objects they wrap.


2 Answers

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.

like image 107
KarlW Avatar answered Oct 18 '22 18:10

KarlW


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...

like image 33
Brad Robinson Avatar answered Oct 18 '22 17:10

Brad Robinson