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:
RETURN VALUE
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... */
backout_nicely();
exit(1);
}
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!