I liked this article and appreciate the examples. But I was left wondering: under what conditions, if any, should I not follow the advice in this article?
I think it’s all good as rules of thumb, but I see two potential downsides.
First, there’s a key phrase that I think indicates some limitations:
It’s not a general purpose BMP library. It only supports 24-bit true color, ignoring most BMP features such as palettes.
As you start adding features, this design approach may rapidly become intractable. It’s most suitable for libraries that are minimalist in the other sense, and unfortunately, we live in an extremely complicated world where such libraries often aren’t actually useful. It’s all great ideals, but if taken as absolute gospel, I suspect it will strongly interfere with getting stuff done.
Second, for instance, I don’t like how the author has bmp_size return 0 in the case of an error. I know it’s a C-ism that’s very difficult to work around in C, but that’s a totally valid size to pass to calloc, which may return a totally valid non-NULL pointer, and bmp_init may then scribble over totally arbitrary memory. (Or it might segfault. “Might” is the operative word here. When a segfault is the best possible outcome, something has gone badly, badly wrong.) This approach forces the caller to check, which is unfriendly and error-prone. Of course, I don’t actually know a better way in C to handle it; returning a negative signed value won’t work because of conversion rules (though trying to allocate almost SIZE_MAX bytes of memory will probably fail).
Similarly,
…the library returns the power of two number of uint32_t it needs to allocate….
Why? The caller will definitely forget to 1ULL << z eventually, and then they’ll have far, far too small a table for the number of elements they want to put in it (which, by the way, the library also has no way to signal). I guess it saves the library from an “error state”, but at the cost of obfuscating its own interface and pushing the check for that error state (which, of course, didn’t actually go away) into the application where it will need to be written many times and thus have many opportunities to be done incorrectly.
…I guess I’m complaining that C isn’t Rust here. =/ But in both these cases, I wonder if a less “minimalist” interface might have been less error-prone to actually use, and (hopefully) uses of a library greatly outnumber implementations thereof, and thus should be a preferable target for minimization.
Guideline #2 (No dynamic memory allocations) can have a significant effect on error detection.
If the library allocates memory then it can use structure marking to detect use after free, uninitialized variables, and other programming errors.
Here is a quick sketch of a version that uses structure marking. I added bmp_try_bytes so that the resulting bitmap can be written to a file.
enum bmp_error_code //definition omitted for brevity
struct bmp_data; //incomplete type
bmp_error_code bmp_try_create(long width, long height, struct bmp_data **result);
bmp_error_code bmp_try_set(struct bmp_data *, long x, long y, unsigned long color);
bmp_error_code bmp_try_get(const struct bmp_data *, long x, long y, unsigned long *color);
bmp_error_code bmp_try_free(struct bmp_data **);
bmp_error_code bmp_try_get_bytes(const struct bmp_data *, size_t *bytes, void **bytes);
One could argue that the design goal of a library should be minimal features and the smallest API possible.
Software projects tend to outgrow the initial specs too fast and rarely get marked as done.
This should be the usual method though. Implement a small thing, declare it DONE featurewise. If necessary replace it with another library that does a different thing.
The inability to declare things done is why most software is terrible. This is especially bad in OSS, where people view the churn as sign of a healthy project, along with all the makework that results from API deprecations/breakage/etc.
I’m convinced a not-insignificant portion of programmers thrive on incidental complexity because it keeps their job semi-interesting.
I liked this article and appreciate the examples. But I was left wondering: under what conditions, if any, should I not follow the advice in this article?
I think it’s all good as rules of thumb, but I see two potential downsides.
First, there’s a key phrase that I think indicates some limitations:
As you start adding features, this design approach may rapidly become intractable. It’s most suitable for libraries that are minimalist in the other sense, and unfortunately, we live in an extremely complicated world where such libraries often aren’t actually useful. It’s all great ideals, but if taken as absolute gospel, I suspect it will strongly interfere with getting stuff done.
Second, for instance, I don’t like how the author has
bmp_sizereturn 0 in the case of an error. I know it’s a C-ism that’s very difficult to work around in C, but that’s a totally valid size to pass tocalloc, which may return a totally valid non-NULLpointer, andbmp_initmay then scribble over totally arbitrary memory. (Or it might segfault. “Might” is the operative word here. When a segfault is the best possible outcome, something has gone badly, badly wrong.) This approach forces the caller to check, which is unfriendly and error-prone. Of course, I don’t actually know a better way in C to handle it; returning a negative signed value won’t work because of conversion rules (though trying to allocate almostSIZE_MAXbytes of memory will probably fail).Similarly,
Why? The caller will definitely forget to
1ULL << zeventually, and then they’ll have far, far too small a table for the number of elements they want to put in it (which, by the way, the library also has no way to signal). I guess it saves the library from an “error state”, but at the cost of obfuscating its own interface and pushing the check for that error state (which, of course, didn’t actually go away) into the application where it will need to be written many times and thus have many opportunities to be done incorrectly.…I guess I’m complaining that C isn’t Rust here. =/ But in both these cases, I wonder if a less “minimalist” interface might have been less error-prone to actually use, and (hopefully) uses of a library greatly outnumber implementations thereof, and thus should be a preferable target for minimization.
Guideline #2 (No dynamic memory allocations) can have a significant effect on error detection.
If the library allocates memory then it can use structure marking to detect use after free, uninitialized variables, and other programming errors.
Here is a quick sketch of a version that uses structure marking. I added bmp_try_bytes so that the resulting bitmap can be written to a file.
Honestly, I find this article full of great advice just for writing general purpose C libraries, let alone minimalist ones.
One could argue that the design goal of a library should be minimal features and the smallest API possible. Software projects tend to outgrow the initial specs too fast and rarely get marked as done. This should be the usual method though. Implement a small thing, declare it DONE featurewise. If necessary replace it with another library that does a different thing.
The inability to declare things done is why most software is terrible. This is especially bad in OSS, where people view the churn as sign of a healthy project, along with all the makework that results from API deprecations/breakage/etc.
I’m convinced a not-insignificant portion of programmers thrive on incidental complexity because it keeps their job semi-interesting.