Tue, 01 Apr 2008

What If I Don't Actually Like My Users?

Here begins our descent into hell; if an interface manages to achieve negative scores on the Hard To Misuse List, your users may detect the dull red glow of malignancy rather than incompetence.

-1. Read the mailing list thread and you'll get it wrong.

If the first hit on Google when searching for the symptoms or how to use your interface leads to a convincing but incorrect answer, that puts your interface here.

-2. Read the implementation and you'll get it wrong.

This happens most often when the implementation being read is not the one you which ends up being used. Or maybe the implementation comes with test cases which all exercise the unnatural corners of the interface, which mislead instead of enlightening.

-3. Read the documentation and you'll get it wrong.

Here's my favorite (now fixed) example, from the glibc snprintf man page:

       snprintf and vsnprintf do not write more than size bytes
       (including the trailing '\0'), and return -1 if the output was
       truncated due to this limit.

I was scanning the man page for the return value on overlength snprintfs; now I'd found it I stopped reading. But here was the next sentence:

       (Thus until glibc 2.0.6. Since glibc 2.1 these functions follow
       the C99 standard and return the number of characters (exclud-
       ing the trailing '\0') which would have been written to the
       final string if enough space had been available.)
-4. Follow common convention and you'll get it wrong.

The usual example here is fputs() and similar which take the context argument at the end instead of the start:

	int fputs(const char *s, FILE *stream);

But that doesn't quite get down here: the compiler will warn if you get the argument order backwards (or, if you prefer, forwards). So again I reach to the Linux Kernel, this time for the list macros:

	void list_add(struct list_head *new, struct list_head *head);

I now have this nailed into my brain, but for a long time I expected the 'head' (ie. the list I'm adding to) to be the first argument. Of course, this wouldn't be such a problem if list heads and list entries were not exactly the same type.

-5. Do it right and it will sometimes break at runtime.

Every C programmer knows that malloc returns NULL on error:

	p = malloc(bufsize);
	if (!p) {
		/* Phew!  We can handle this... */

Except malloc may also return NULL on zero-length allocations: something you'll find out the hard way when your nice code which didn't special case 0-length allocations breaks horribly on someone else's machine.

-6. The name tells you how not to use it.

Sometimes we opt for changing behavior without changing a (now-inappropriate) name, knowing that existing users won't be broken by the new behaviour. But don't curse future users with a misleading name: if your project takes off, there will be far more of them than current users.

My example here is another Linux kernel one which bit me. I was writing a block (disk) driver: it gets passed a struct request which consists of a series of chunks. After servicing them, it calls end_request(). Only it turns out that (for historical reasons!) this only ends the first chunk. My block driver "worked", but it was doing about N^2/2 times the work it needed to do for an N-chunk request.

(I didn't find that, the maintainer reviewing my code did).

-7. The obvious use is wrong.

I've been coding in C for about 20 years, and about five years ago I spent an hour chasing a case where I'd done if (strcmp(arg, "foo")) instead of if (!strcmp(arg, "foo")). Now I religiously #define streq(a, b) (!strcmp((a),(b))) because I know I'm not as smart as I think I am.

Less "I'm obviously an idiot" is the behavior of strncpy() which truncates the destination string without adding a NUL terminator. Or char x[5] = "hello"; which the C standards committee thought would be an excellent trap for newcomers (and particularly stupid since there is a workaround if you really want an unterminated character array).

-8. The compiler will warn if you get it right.

The bind() socket library call comes to mind here: it takes a struct sockaddr but you always have to cast to use it, as you will never have a struct sockaddr, but instead a struct sockaddr_in or some other specific type. This one is almost excusable, although I'd expect better from modern code.

-9. The compiler/linker won't let you get it right.

This is hard to find in C, since the compiler will let you cast your way through almost anything. Listed here for completeness.

-10. It's impossible to get right.

Unlike the first category, this final category is neither a paragon nor unattainable. Some interfaces are so fundamentally flawed that they can't be used correctly. Perhaps it can fail in a way you have to know about but it doesn't return an error. Perhaps it returns an error but you can do nothing about it.

In the Linux kernel there used to be interfaces which assumed single-threading, and are now unsafe. Say you expose two functions called prepare() and and action() and expect the caller to do if (prepare()) action();. This is broken if action() relied on all the checks in prepare() passing, and now conditions can change between the two.

That's everything I know about interface design. Now, go and make your own mistakes so you can have wise things to say about it!

[/tech] permanent link