Tuesday 12 November 2013

DDE XTYP_EXECUTE Command Corruption

A few months back I had a request to add support for the XTYP_EXECUTE transaction type to my COM based DDE client. The question came from someone who was using the component via VBScript to automate a TCL script which in turn was automating some other processes! My underlying C++ based DDE library already had support for this, along with my DDECmd tool so I thought it was going to be a simple job. What I didn’t expect was to get into the murky depths of what happens when you mix-and-match ANSI [1] and Unicode build DDE clients and servers...

Adding XTYP_EXECUTE Support

Due to the nature of COM the API to my COMDDEClient component is essentially Unicode. When I went into my code to check how the command strings were handled in my C++ library I discovered (after spelunking the last 10 years of my VSS repo) that when I ported the library to allow it to be dual build (ANSI & Unicode) I just took the command string as is (either char or wchar_t) and pushed it out directly as the value for the XTYP_EXECUTE transaction data.

The XTYP_EXECUTE DDE transaction type is an oddity in that the “format” argument to the DdeClientTransaction() function is ignored [2]. The format of this command is documented as being a string, but the exact semantics around whether it’s ANSI or Unicode appear left unsaid. I therefore decided to correct what I thought was my naive implementation and allow the format to be specified by the caller along with the value - this felt more like a belt-and-braces approach.

Naturally when I came to implement my new ExecuteTextCommand() method on the COM DDE server IDDEConversation interface, I followed the same pattern I had used in the rest of the COM server and defaulted to CF_TEXT as the wire format [3]. I tested it using the new support I’d added to my DDECmd tool via the --format switch and thought everything was great.

Impedance Mismatch

Quite unexpectedly I quickly got a reply from the original poster to say that it wasn’t working. After doing the obvious checks that the build I provided worked as expected (still using my own DDECmd tool as the test harness), I took a crash course in TCL. I put together a simple TCL script that acted as a DDE server and printed the command string send to it. When I tried it out I noticed I didn’t get what I expected, the string appeared to be empty, huh?

Naturally I Googled “TCL XTYP_EXECUTE” looking for someone who’s had the same issue in the past. Nothing. But I did find something better - the source code to TCL. It took seconds to track down “win/tclWinDde.c” which contains the implementation of the TCL DDE support and it shows that TCL does some shenanigans to try and guess whether the command string is ANSI or Unicode text (quite a recent change). The implementation makes sense given the fact that the uFmt parameter is documented as being ignored. What then occurred to me as I was reading the TCL source code (which is written in C) was that it was ANSI specific. I was actually looking at slightly older code it later transpired, but that was enough for the light bulb in head to go on.

A consequence of me using my own tools to test my XTYP_EXECUTE support was that I had actually only tested matched build pairs of the DDE client and server, i.e. ANSI <-> ANSI and Unicode <-> Unicode. What I had not tested was mixing-and-matching the two. I quickly discovered that one permutation always worked (ANSI client to Unicode server) but not the other way (a Unicode client sending CF_TEXT to an ANSI server failed).

Guess what, my DDECOMClient component was a Unicode build DDE client sending CF_TEXT command strings to a TCL DDE server that was an ANSI build. Here is a little table showing the results of my mix-and-match tests:-

Client Sending Server Receives Works?
ANSI CF_TEXT Unicode CF_UNICODETEXT Yes
ANSI CF_UNICODETEXT Unicode CF_UNICODETEXT Yes
Unicode CF_TEXT ANSI ??? No
Unicode CF_UNICODETEXT ANSI CF_TEXT Yes

Forward Compatible Only

I looked at the command string buffer received by the DDE server in the debugger and as far as I can tell DDE will attempt to convert the command string based on the target build of the DDE server, which it knows based on whether you called the ANSI or Unicode version of DdeInitialize() [4]. That means it will automatically convert between CF_TEXT and CF_UNICODETEXT format command strings to ensure interoperability of direct ports of DDE components from ANSI to Unicode.

In fact it does even better than that because it will also correctly convert a CF_TEXT format command sent from a Unicode build DDE client to a Unicode build DDE server. The scenario where it does fail though, and the one that I created by accident through trying to be flexible, is sending a CF_TEXT format command string from a Unicode build DDE client to an ANSI build DDE server.

Once I discovered that DDE was already doing The Right Thing I just backed out all my DDE library changes and went back to the implementation I had before! And hey presto, it now works.

 

[1] For “ANSI” you should probably read “Multi-byte Character Set” (MBCS), but that’s awkward to type and so I’m going to stick with the much simpler (and much abused) term ANSI. This means I’m now as guilty as many others about using this term incorrectly. Sorry Raymond.

[2] Not only is this documented but I also observed it myself whilst investigating my later changes on Windows XP - the uFormat argument always seemed to reach the DDE Server callback proc as 0 no matter what I set it to in the client.

[3] Given the uses for DDE that I know of (mostly financial and simple automation) CF_TEXT is more than adequate and is the lowest common denominator. Nobody has written to me yet complaining that it needs to support requests in CF_UNICODETEXT format.

[4] It is not obvious from looking at the DdeInitialize() signature that the function even has an ANSI and Unicode variant because there are no direct string parameters.

6 comments:

  1. So does this only apply to your COM component and not ddecmd?

    I had a customer complaint and he was using ddecmd to try and drive some DDE commands. My application is DDE aware and is MBCS. When I debugged and looked at the DDE commands coming over into my DDE proc, the strings were gibberish--"?????????????", etc. Looked like some strings that happen when using WideCharToMultiByte() with the wrong charset specified.

    I went and recompiled ddecmd myself with MBCS instead of Unicode, and then the strings started coming over the wire correctly. When I ran ddecmd (Unicode version) under the debugger, it looked to be converting the wide strings to MBCS before the ultimate call to DdeClientTransaction() in CDDECltConv::Execute().

    ReplyDelete
  2. I've just reviewed the code on GitHub to see what exactly is in the v1.5 release of DDECmd and sadly it still contains the --format switch (with a poor choice of default which makes the problem more apparent with an ANSI DDE server).

    I have opened an issue on GitHub to document the problem - https://github.com/chrisoldwood/DDECmd/issues/1

    ReplyDelete
    Replies
    1. I believe there is a workaround for the v1.5 release which would be to explicitly specify the command be sent as Unicode, i.e. use execute with this switch:

      --format CF_UNICODETEXT

      Delete
  3. Oh man I just spend hours of debugging related to this. I am writing code to communicate with the old ClipSrv service but what ever I've tried, it failed XTYP_EXECUTE with not processed. But my application is UNICODE, and the MS ClipSrv is ANSI, so that the command strings at the server where not the onces I send seemed logical. And indeed, specifying CF_UNICODETEXT (instead of zero) and sending an UNICODE command string to the ClipSrv works. Thanks for the eye opener!

    ReplyDelete
    Replies
    1. This behaviour is completely undocumented and unexpected, as one assumes the data is send to the server as-is without conversions. I was expecting any binary data could be sent, but if I read the docs better it talks about "command strings"

      Delete
    2. I think we both need some sort of "badge of honour" we can stick on our machines to show our pain at stumbling into this :o).

      Delete