Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

Software Contracts, Part 3 - Sometimes implicit contracts are subtle

Software Contracts, Part 3 - Sometimes implicit contracts are subtle

  • Comments 32

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.

  • I think part of the mystery with PlaySound is that there is no documented way of knowing when the sound has finished playing.  Of course the way to handle this is to spin up your own thread, call PlaySound synchronously and then free the buffer, but that seems like a high price to pay for a very common scenario.  I ended up writing my own PlaySound built on top of DirectSound to compensate for some of these issues (among others of course) and was able to design the thing to properly manage buffers transparently.

    One more question, by your reasoning above we can only assume that if we specify SND_RESOURCE | SND_ASYNC to PlaySound that the API will manage freeing the resource (I know its just a mmap'd section of an executable image and doesn't need much freeing, but...) when playback has finished.  How's that for deductive reasoning?

  • Lonnie, there are two major cases for PlaySound.

    The first is calling PlaySound with an alias or filename - in that case, all you need to is to ensure that the memory containing the name of the alias or filename in question remains in memory.  If you use the alias IDs, then you don't even need to do that.

    The second case is when you call PlaySound with a chunk of memory.  It turns out that you can determine the length of the file from the contents of the FMT section and the contents of the DATA section.  

    Even without that, if you call PlaySound(NULL, ...), you'll stop the playback.  So just call PlaySound(NULL, ...) before freeing the memory and you'll be just fine.

  • Oh, and Lonnie, you're right - if you specify SND_RESOURCE, then PlaySound calls LoadResource etc so it guarantees that the memory is still valid during the course of playback.  Similarly, for SND_FILENAME it allocated a block of memory and frees it when done (all behind the scenes).  

    Again, this behavior can be deduced from the API and some common sense.

  • Whoa, did my RSS reader accidentally cross-link Raymond Chen's feed with yours???

    (That's meant as a compliment, BTW.  :p)

  • Thanks, Larry! I agree that a program can be much more bulletproof if you make the worst-case assumptions about the functions you call. The depressing thing about doing that is that it can significantly complicate the code and obscures its goal. So I think a lot of us tend to extend the explicit contract with our own principle of least surprise contract terms. It would be good if that documentation was upgraded as well as the code to turn implicit into explicit when possible. I know Microsoft moves slowly nowadays, but it can't be that hard to update MSDN to say "on ASYNC, you must not free the buffer until the sound has finished playing."

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

    Does PlaySound() access the memory passed in after the call completes?

    Say what? No? Oh, OK then.... ;)

    Yeah, that's facetious. But...

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

    ...is an argument *against* the actual behaviour of PlaySound().

    IMO, as far as *any* C API goes, you should *always* assume that if the caller passes a caller-allocated memory buffer to a function, any function, then the caller may be free to do what it likes with that buffer after the function has returned, *unless the documentation says otherwise*.

    I therefore contend that the MSDN documentation for PlaySound(), specifically the part describing the SND_ASYNC flag, is defective by being incomplete in this regard.

    And that you're just making excuses for it.

    You're just invalidating almost every other line of C code ever written otherwise. Hmmm.....strlen() doesn't say whether it uses the buffer I passed it after the function returns, therefore I must hang onto it and keep it unmodified for the remainder of my program in order to "always make the most conservative assumptions about [its] behavior".

    :)

  • Adam, you're right - that's why I'm having the documentation fixed.  But to answer your question: strlen doesn't also say that it works asynchronously.

    On the other hand, the ReadFile API's documentation doesn't say anything about the memory pointed to by lpbuffer being valid from the time the API is called to when it completes.  

    It's another example of an implicit API contract - even though ReadFile doesn't say it, it's contract is: you provide a buffer and ReadFile fills it in.  If the read is asynchronous, the contents of the buffer aren't valid until the read completes, but until it completes, the buffer MUST remain valid (this falls out of the fact that the buffer is effectively an OUT parameter - out parameters must remain valid from the time an API is invoked until it completes).

    The PlaySound API with the SND_ASYNC flag behaves the same as ReadFile behaves with an LPOVERLAPPED parameter - the buffer passed in must remain valid until the API completes.  There is absolutely no difference in the contracts.

    Having said that, there IS one significant difference: ReadFile provides a mechanism (the LPOVERLAPPED) that can be used to determine when the ReadFile API has completed, the PlaySound API (as has been discussed earlier in this thread) doesn't.

    On the other hand, the PlaySound API DOES provide a mechanism for canceling any outstanding asynchronous call to PlaySound - calling PlaySound with a NULL filename is documented as terminating any and all outstanding calls to PlaySound, so there IS a safe way of ensuring that the API is completed.

  • Yes, you're clever that you can use the fact that it's written for Windows 3.1 to work out it must require the memory block not to be freed, but 10 years after Windows 95, who will think of such things?

    Also, was not copying the memory a good design decision given that it makes it much harder for the caller to free the memory after the sound has been played (meaning memory is less likely to be freed after being passed to PlaySound)?

  • Tim, as I pointed out earlier: The reason you can't free the memory is that the API doesn't say that it's ok to free the memory.  And in general, memory passed to APIs must be valid until the API has completed (even when the API is asynchronous).

    But the same logic (not copying multi-hundred K blocks of memory) applies to Win9x as well.

    And yeah, 20/20 hindsight is wonderful, it would have been great if the API had been defined differently.  But as Raymond Chen likes to say: Time machines haven't been invented yet.

    This behavior has been the behavior of PlaySound since 1991 when the API was originally added to Windows 3.x.

  • If you are interested in this subject, I suggest that you take a look at "Design by Contract" (http://en.wikipedia.org/wiki/Design_by_contract) and the Eiffel language which heavily relies on it.

  • A question just popped in my head after reading your entry:

    Following the principle of "least surprise", wouldn't ASYNChronous calls, by nature, have an "implicit contract" by definition since:

    - It requires some sort of signaling the caller that the operation has ended and the allocation are no longer required (so you can do your "house cleaning")?

    - Or, to remove any need of signaling, an alternative way would be, to segregate any memory allocations in order to remove any dependencies from the calling function?

    My apologies if this is just another a dumb question.

  • jugger: If this API hadn't behaved in this manner for 15 years now, I agree - the async code should copy the memory.  But time travel hasn't been invented yet.

  • But couldn't this behaviour be "fixed" in future Windows releases? Surely if Vista had just buffered ASYNC calls to PlaySound then the issue of applications releasing memory too early would eventually go away.

  • Larry, it's true that time machine hasn't been invented yet. But what harm would it be done if we create a new version that DOES copy the memory, and run an old program that does not expect this on it? (Anyway, would someone write a program that modifies the buffer as it plays?)

    I'd say that if it'd be a non-breaking change and your teams should consider fix it this way.

  • how about a simple memlock inside PlaySound API ? Wouldnt that re-affirm the implicit contract ?

Page 1 of 3 (32 items) 123