Software Contracts, Part 3 - Sometimes implicit contracts are subtle

I was planning on discussing this later on in the series, but "Dave" asked a question that really should be answered in a complete post (I did say I was doing this ad-hoc, it shows).

 

Let's go back to the PlaySound API, and let's ask two different questions that can be answered by looking at the APIs software contract (the first one is Dave's question):

I am happy to fulfill my contractual obligations but I need to know what they are. If you don't tell them, how is the caller to know that you need their memory until the sound finishes playing?

If I call PlaySound with the SND_ASYNC flag set, how can I know if the sound's been played.

As I implied, both of these questions can be answered by carefully reading the APIs contract (and by doing a bit of thinking about the implications of the contract).

Let's take question 2 first.

The explicit contract for the PlaySound API states that it returns TRUE if successful and FALSE otherwise.  If you specify the SND_ASYNC, what does that TRUE/FALSE return mean though?  Well, that's not a part of the explicit contract, it must be a part of the impicit contract.

Remember that the PlaySound API only has three parameters (the sound name, a module handle and a set of flags).  All of these parameters are INPUT parameters - there's no way to return the final status in the async case.  Since there's no way for the AP to return whether or not the sound successfully played, the only way that the return from the API contained an indication of the success/failure of playing the sound implies that the SND_ASYNC flag didn't actually do anything.  And that violates the principle of least surprise - if the SND_ASYNC flag was a NOP, it would be a surprise.

And in fact all the call to PlaySound does is to queue the request to a worker thread and return - the success/failure code refers to whether or not the request was successfully queued to the worker thread, not to whether or not the sound actually played.

 

No for Dave's question...

First off: One critical part of interpreting software contracts is:  If you have a question about whether or not a function behaves in a specific manner, if it's not specified in the explicit contract, assume the answer is 'no' unless otherwise specified.

Since the contract for PlaySound is currently silent about the use of memory in combination with the SND_ASYNC flag, you should always make the most conservative assumptions about the behavior of PlaySound.  Since the API documentation doesn't say explicitly that the memory can be freed while the sound is playing, you should assume that it shouldn't.  And that means that the memory handed to the PlaySound call must remain valid until the call to PlaySound has completed playing the sound.

 

But even without that, with a bit of digging, you can come to the same answer.

Here's how my logic works. Both of the givens below are either explicit or implicit in the contract.

  1. You own the memory handed to PlaySound - you are responsible for allocating and freeing it. You know this because PlaySound is mute about what is done with the memory, thus it has no expectations about what happens to the memory it uses (this is an implicit part of the contract).
  2. The default behavior for PlaySound is synchronous (you know this because the documentation states that the SND_SYNC flag is the default behavior) (this is an explicit part of the contract).

 

You can also assume that the SND_ASYNC flag is implemented by dispatching some parts of the call PlaySound to a background thread.  This is pretty obvious given the fact that something has to execute the code to open the file, load it into memory, and play it.  You can verify this trivially by using your favorite debugger and looking at the threads after calling PlaySound with the SND_ASYNC flag.  In addition, there are no asynchronous playback calls in Windows, so again, it's highly unlikely the playback is done using some kind of interrupt time processing (it's possible, but highly unlikely - remember that PlaySound was written for Windows 3.1).  I actually went back to the Windows 3.1 source code for PlaySound and checked how it did it's work (there were no threads in Windows 3.1) - on Windows 3.1, if you specified the SND_ASYNC flag, it created a hidden window and played the sound from that windows wndproc.

But even given this, we're not done.  After all, it's possible that the PlaySound code makes a private copy of the memory passed into PlaySound before returning from the original call.  So the decision about whether or not the memory passed into the PlaySound API can be freed when specifying SND_ASYNC really boils down to this: If PlaySound makes a private copy of the memory, then the memory can be freed immediately on return, if it doesn't, you can't.

This is where you need to step back and make some assumptions.  Up until now, pretty much everything that's been discussed has been a direct consequence of how the API must work - SND_ASYNC MUST be implemented on a background thread, you DO own the memory for the API, etc.

So let's consider the kind of data that appears in the memory for which the PlaySound API is called.

Remember that most WAV files shipped with Windows (before Vista) were authored as 22kHz, 16 bit sample, mono files (for Vista, the samples are all stereo).  That means that each second of audio takes up 44K of RAM.  That means that all non trivial WAV files are likely to be more than 64K in size (this is important).  Again, consider that the PlaySound API was written for Windows 3.1 where memory was at a premium, especially huge blocks of memory (any block larger than 64K of RAM had to be kept in "huge" memory allowing the blocks to be contiguous. 

If Windows were to take a copy of the memory, it would require allocating another block the size of the original block.  And on a resource constrained OS like Windows 3.1 (or Windows 95) that would be a big deal.

Also remember my 2nd point above - the defaut behavior for PlaySound is synchronous.  That means that the PlaySound call assumes that it's going to be called synchronously. 

Given the fact that PlaySound was originally written for Windows 3.1 and given that the default for PlaySound is synchronous, and given the size of the WAV files involved, it thus makes sense that the PlaySound API would not allocate a new copy of the memory for the .WAV file and instead would use the samples that were already in memory - why take the time to allocate a new block and copy its contents over when it was already available.

Now this is a big assumption to make - it might not even be right.  But it's likely to be a reasonable assumption.

So you should assume that PlaySound doesn't take a copy of the memory being rendered, and thus you need to ensure that the memory is valid across the life of the call.

 

Btw, I just was told by the doc writers that they're planning on making this part of the contract explicit at some point in the future.

 

Tomorrow: Let's look at some explicit contracts.