As a kid, I wanted to make games, and just found the win32 API inscrutable. I too lost a couple years.
As a kid, I wanted to make games, and just found the win32 API inscrutable. I too lost a couple years.
Also of course that’s Microsoft of the late nineties: e.g. COM/OLE2 originally had a Macintosh port and an obscure commercial-Unix one, VC++ 4 shipped with a Macintosh cross environment complete with an MFC port, etc. Don’t know if anybody ever seriously used those except the Mac ports of Microsoft software (Office, IE).
The Mac OS from the 90's had zero POSIX, and was less capable than Windows, hence Copland, which by the way also had zero compatibility with UNIX and was going to be a microkernel OS with C++ userspace.
Some of the key problems that makes Win32 unpleasant to use:
1. Preferring to be consistent with past mistakes rather than fix them. Core Win32 isn't namespaced in any way, and is full of generic function names like "StartTrace" that can easily conflict with user code. 30 years later they are still adding new Win32 APIs that also aren't namespaced in any way. They still use the "Ex" convention and so on.
2. A pervasive assumption that the APIs shouldn't have any opinion about memory allocators or heaps. You get this in old UNIX APIs as well. Why does the ETW API mocked in this article require so much pointer arithmetic and memory munging, well, because Microsoft don't want to have an opinion on where memory should be held. They see malloc as a utility convenience, so nothing in the API will malloc memory for you, or if it does, then it's done using its own API specific wrappers around malloc/free. By implication Win32 generally doesn't have functions that construct objects for you, instead it's always up to you to allocate the memory and pass it in. That means the API might be called with allocations of various different sizes depending on when the app was compiled, so, then the caller is typically expected to place the structure size in the structure itself as a crude form of versioning. It's all a lot of tedious boilerplate that's easy to get wrong.
3. An inconsistent and ad-hoc (API specific) approach to versioning and backwards compatibility. There just doesn't seem to have ever been much planning ahead. Win32 is full of places where they left room to extend the API yet never used them, and places where they didn't and then needed to introduce an Ex or 2/3 call. Again COM introduced better ways to manage this problem, but they didn't use that consistently either.
4. Frequent use of random GUIDs to namespace things. The article has examples of this. Nowadays we take for granted internet based namespaces, like how Java/macOS use reverse DNS names to identify code. Win32 originates in a pre-internet era, more or less, so a lot of APIs are built on the assumption that nobody can communicate with anyone else and there are no central naming registries that work. Giant random numbers are a reasonable solution to this, but again, Microsoft stuck with it for way too long after the concept became obsolete.
COM fixes some of these things but is used inconsistently and it was never retrofitted onto the core API properly until WinRT. For example, if you want to work with Direct3D you need to use COM and instantiate some COM objects, but if you want to then connect that Direct3D surface to a window, there's no COM object to represent a window, that's all done with legacy C APIs. Why did they never switch entirely to COM? Probably internal politics - developer experience became seen as some other department's job (VC++), and Bill Gates became obsessed with Big Chunky Features he could easily understand without doing Windows programming as his day job (like WinFS).
Charles Simonyi comes to mind.
Instead they're the result of a whole bureaucracy of people each of whom have to inject their own particular requirements, coding preferences and "what if in the future..." guesses.
my verdict is that Simonyi didn't communicate the idea - good otherwise - well enough
As for Apps Hungarian, I was just discussing this the other day on a forum devoted to John Ousterhout's A Philosophy of Software Design. In Chapter 14, "Choosing Names" Ousterhout discusses a case where a variable named 'block' was used in a place where there were both physical and logical blocks, both integer types. His suggestion, which isn't wrong, was to use different variable names for different kinds of blocks, making it possible for a programmer to tell if a logical block variable was being used in some physical block handling code. This is similar to Joel's example of using 'rw' and 'col' prefixes for variables that are both integer types.
When I see a convention like this, I ask, "why can't they be different types?". In modern languages you can define new types easily, and define different interfaces or APIs to operate on the types. With 'physicalBlock' and 'logicalBlock' types, or 'row' and 'col' types, programmers can lean on the compiler or interpreter to prevent many errors. Beyond looking wrong to the programmer, the code is wrong, and won't compile or interpret correctly, provided there is a reasonable type system for the language.
The obvious concern is a proliferation of types, and yes that can happen when taken to extremes. One area where Joel's example makes a very strong case for types is 'us' and 's'. Strings are handy, but also the wrong abstraction. A 'SafeString' type would very clearly communicate the abstraction at the proper level: the thing in question is not a string, it's a representation of encoded user input. An attempt to assign a SafeString to a plain old string would be an error. Passing a string to a function that declares a SafeString would be an error.
There's even a name for using ints and strings when different types would be better: Primitive Obsession. A sign of this obsession would be passing around a user ID and password as strings, and a corrective would be a Credential type that encapsulates both ID and password types, and includes the necessary parsing and validation on creation. This eliminates an entire class of errors much more strongly than a convention where every string that contains a password has a 'pw' prefix, and userID strings are prefixed with 'userid' or just 'id'.
Of course, if the language doesn't provide useful typing, then naming conventions are essential. In languages with good type systems, always be asking, "does the language's primitive type really represent the abstraction in use here?"