Some might think my work is boring. Today I was editing a video voiceover script written in English by a non-native English speaker (which can be a real pain). I stumbled on a tech term I didn’t know: “heavy VB client.”
I googled it and it only appeared once, which suggested to me that it’s not something English speakers say a lot. So I’m digging around online to find out how we do talk about VB clients, whatever the hell those are, and I came across this magnificent poem. It was masquerading as tech support. But I saw it for what it really was.
Everything is art.
Why does my VB client keep crashing
when compiled
and not in the IDE
when I use an ActiveX Control
with a worker thread?
You probably fire events
from the worker thread
in your control.
Since all ActiveX Controls
live in single-threaded apartments,
the event sink
your VB client supplies
lives in that STA too.
VB operates in apartment model only,
hence the pointer for the event sink
is in fact
a direct pointer
to the object in VB.
Hence you are able to call
through this pointer.
Unfortunately, by doing so
you violate
the COM threading rules –
every interface pointer
is valid
only
within the apartment
it is obtained in.
Since VB is not thread-safe –
you experience
the crash.
Solutions.
There are three possible solutions
(described in ATL terms,
but they are appropriate
for straight C++ COM coding too):
1. The easiest solution is to create
a hidden window
upon the object’s construction
in FinalConstruct
(it is not a good idea
to put such code
in the constructor).
Then whenever you need to raise an event,
you post a message
to that window
instead.
The message handler then
fires the event.
The drawback is
that you have to package
any arguments
and unpackage them
in the message handler.
An additional benefit is
that unlike the other approaches,
this way the worker thread
is immediately
ready to continue
with the next task –
asynchronous notifications.
This approach is made possible
by the rule
that all STA threads
must have
a message loop
(in this case
implemented by VB).
2. Rewrite the implementation of
IConnectionPointImpl::Advise
and Unadvise
and forward the call
to the worker thread
somehow.
The implementation uses
CoMarshalInterThreadInterfaceInStream
and the worker thread uses
CoGetInterfaceAndReleaseStream
to marshal the interface pointer
to the worker thread.
Then the marshaled pointer
is stored in the map
instead of the direct pointer
from the client.
The worker thread
must enter an apartment
(MTA or create new STA).
The advantage of this approach is that
the code generated
by the ATL connection points wizard
doesn’t change.
The drawback is that
the events must be fired
from the worker thread’s apartment
only
(so if you have multiple worker threads
you better enter the MTA
in all of them).
The worker thread
is suspended
for the duration
of the call.
3. This one is a generalized version
of the second.
Instead of explicitly
marshaling
the interface pointer
for a specific apartment
up front,
the code in the Advise
method
is modified
to use the Global Interface Table
to store the interface
and a cookie
is stored
in the map instead.
Whenever any thread
wants to fire
an event,
the cookie is used
to temporarily obtain
an interface pointer
for the current apartment.
Then the event is fired
and the interface pointer
is released.
The drawback
(with ATL in mind)
is that in addition to
the IConnectionPointImpl
code,
you have to modify
the code
for the proxy
generated by the ATL wizard.
The advantage is that
events can be fired
from any apartment.
All threads which fire events
must enter an apartment.
The thread firing the event
is blocked
for the duration
of the call.