Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

Can my STA object create worker threads?

Can my STA object create worker threads?

  • Comments 9

For some reason, a bunch of COM related stuff's been coming onto my radar lately, so for some reason, I've been dealing with a lot of COM related issues.  Go figure this one out.

The other day, I received a message from a co-worker asking me (roughly) "I have an apartment threaded COM component.  Does this mean that I can't use threads in the object?"

It's a really good question, and I had to think about it for a while before answering.

CAVEAT: I'm not a COM person, so it's possible the answer is more complicated than this, but here goes.

The simple answer is that by declaring your component as being apartment threading is an indicator to the CREATOR of your object that it's only safe to call the methods on your object from the thread that initially created the object.  The contract says NOTHING about how you internally implement your component.  So you are free to use whatever internal mechanisms you want in your component.

However, as always, there is a huge caveat.

By declaring your COM object as apartment threaded, you've told COM that the methods on the interfaces to your object can only be called from the thread that created the object.  That means that if COM ever drops one of the standard marshallers between your other threads and the interfaces (which shouldn't happen, but might in some COM interop scenarios, or if you're subclassing the standard marshaller, or if your COM object is an inproc handler), then COM's going to enforce the threading model you declared for your object.  Which means that COM might start returning RPC_E_WRONGTHREAD to your method calls if they go through the marshaller. 

The thing is that you might get away with this for years (since none of the COM marshallers will be involved in most in-proc scenarios), until some point in the future when your component is called in a scenario you don't expect and all of a sudden, the COM marshaller gets in the way and all of a sudden things stop working.

 

  • If you only call into your own methods using pointers you created in native code - not going through COM APIs - you're probably OK. You do need to watch out for calling out of your component, e.g. if you need to raise an event through a Connection Point. When the component's consumer registered a connection point (or otherwise handed you an interface pointer), COM may have instead handed you a pointer to a proxy. You need to co-operate with this proxy.

    You should be able to do so with CoMarshalInterThreadInterfaceInStream and the corresponding cleanup function CoGetInterfaceAndReleaseStream. If the client and component have compatible threading models, COM may hand you a pointer to the actual object. Otherwise it will give you the lightest-weight proxy it can.
  • How anout explain Com to non C programmers? I spent years trying to find out what is was at it's core. I read meaningless articles on monikers (apparantly it's a name) and IUnknown. Then one day I came across one that mentioned VTables in passing. That made it obvious it was a function library, just like a dll. Yet nowhere before or since have I seen it said it's a function library. I don't know what marchalling is (in VB we just compile a class library I think - it's been a few years -- no Iunknown - vb does all that for you).
  • Btw, Mike, thanks for the additional info, it's great.

    David, Let me see what I can do.
  • David: A COM component is a function library, it's true. The difference between a COM component and a regular DLL is that the COM component provides a table of functions, upon request, that match a known set of semantics.

    Marshalling is the process of transporting the arguments for a function call from one context to another. COM adds marshalling (a PROXY object in the caller's context and a STUB in the component's) when the caller's context is incompatible with the component's context - when a simple function call won't work. This can happen between processes, between machines, between an apartment-model caller and a free-thread-model component, between a free-thread caller and apartment component, between apartment-model caller and an apartment-model component created on a different thread, and even between components running on the same thread if they're using incompatible COM+ contexts.

    Marshalling simply consists of writing the function call arguments to some location that both caller and component can read from, then somehow alerting the component about the incoming call. It's complicated by arguments that can be pointers, which can in turn point to structures containing other pointers, all of which needs to be copied from the caller to the component.

    In the full-blown COM model, you provide the code for the proxy and stub. However, there are a number of simplifications. Firstly there's the table-based implementations built by MIDL.exe from interface declarations. Secondly, Windows provides the Automation marshaller, which builds proxies and stubs from Automation type libraries. Visual Basic uses this feature - it registers the type library when performing self-registration.

    Actually getting VB6 to keep binary compatibility between versions of a component is a bit on the tricky side - you have to be very careful about the changes you make. You can only add methods, not change or delete them. You can't change enumerated values, you can only add new values. If you haven't specifically assigned values, you can only add them at the end. Structures can't change. VB only ever (IIRC) generates dual interfaces; the vtable always contains IUnknown's and IDispatch's methods.

    Monikers are really only used in OLE for persistent document links. They're not used much elsewhere.

    Dale Rogerson's book Inside COM (which shouldn't really be in MS Press's "Inside" series, which are generally deep technical books) is a good resource.
  • If you follow COM rules with regard to passing interface pointers between threads, then you should be fine.

    However there is another problem with creating worker threads from a DLL (not necessarily an STA COM server - any DLL). It has to do with terminating the threads when the DLL is unloaded.

    Clearly, you can't wait for the threads to shutdown in you DllMain(PROCESS_DETACH) since that would deadlock. You could require clients to call a Shutdown() method, or even prevent the DLL from being unloaded by doing LoadLibrary on yourself.

    In the COM case it might be tempting to shutdown the threads when your last reference goes away (the moment you start returning TRUE from DllCanUnloadNow). This however is not safe - if the client calls CoUninitialize and this call destroys the COM apartment where your object lives, COM will unload you without even asking DllCanUnloadNow.

    A better approach is to give each thread its own reference to the DLL. Before creating the thread, you call LoadLibrary on yourself. When the thread terminates, it calls FreeLibraryAndExitThread.
  • "Actually getting VB6 to keep binary compatibility between versions of a component is a bit on the tricky side - you have to be very careful about the changes you make. You can only add methods, not change or delete them. You can't change enumerated values, you can only add new values. If you haven't specifically assigned values, you can only add them at the end. Structures can't change."

    Hold on a second. Are you talking about a COM interface or some other VB6-specific thing?

    Once you've published a COM interface, you can't add *any* methods to it. If you want to add a new method, you have to create a new interface. This is why you see interfaces like IHTMLElement, IHTMLElement2, IHTMLElement3, and IHTMLElement4.

    Adding a new method at the end of an existing interface would break binary compatibility. An application that was built using the old definition would work with the new interface, but what about an application that's built using your *new* definition of the interface and finds itself actually using the old version? It will try to call one of your new methods, but the old version of the interface doesn't have it. Boom!

    It's not like Windows where you can call GetVersionEx and say, "Oh, I'm running on Win95. I'd better not call ReadDirectoryChangesW because it's not there." COM interfaces don't have versions. Instead, you can create a brand new interface while continuing to support your old one.
  • As far as I know, VB6 components are all IDispatch-based; the "interfaces" you create with it aren't strictly COM interfaces, just (comparatively) loose specifications of which methods are available and can be dispatched.

    I could be wrong, it's been a while.
  • "VB6 components are all IDispatch-based"

    D'oh! I was thinking of vtbl interfaces, of course. Never mind... :-)
  • PingBack from http://punkouter.wordpress.com/2007/05/01/on-com/

Page 1 of 1 (9 items)