I’ve written a lot of C++ in my career, but I still prefer to design in
C for most embedded projects (“why” is the subject of a much longer, rant-filled post).
I’m not a big proponent of OOP in general,
but I do think having an “instance” of something which contains stateful data is a generally
useful thing for embedded software. For example, you may want to have several
instances of a ring buffer (aka circular FIFO queue) on your system.
Each instance contains stateful data,
like the current position of read and write pointers. What’s the best way
to model this in C?
First, on a technical note, if you were to move the data pointer to the end of the struct and specify it as a flexible array member, you could make a macro to statically allocate instances.
More broadly, however, I just don’t see the benefit of opacity. After having written and used many ‘user protective’ APIs, I find that they hinder developer understanding and debugging far more than they protect from erroneous usage. Providing an API with a well-documented public interface provides all the encapsulation benefits. Users that wish to venture beyond knowingly do so at their own risk, but sometimes that risk is justified. Unless you’re linking against a compiled binary, I think the opacity just gets in the way.
Thanks for the explainer! I’ve used many libraries using this pattern, and finally there is a clear explanation of it. BTW I’m looking forward to that “much longer, rant-filled post” about C++
@riggs Thanks for mentioning that about flexible array members. I’ve never used them before, but I can see the value.
More broadly, however, I just don’t see the benefit of opacity. After having written and used many ‘user protective’ APIs, I find that they hinder developer understanding and debugging far more than they protect from erroneous usage.
Yeah, I think it really depends. Making it a regular old non-opaque pointer is just fine in some situations.
If the design goes in that direction, I’d say there are a few things to keep in mind.
Pros:
Gives more power and control to the user
Good for debugging
Cons:
Increases the surface area of the API
Might need more error checking in the module
Module code can not make as many guarantees about how the struct data is manipulated, since it can be changed from anywhere. Can result in more complex code.
I think there’s a difference in philosophy here, because I disagree that the cons you’ve listed exist. If a library clearly documents what the public API is, any use outside of that is caveat emptor. I don’t think the library should have increased error-checking that other code might have manipulated values. If a user of the library does that and breaks library functionality by going beyond the public API, it seems clear to me that it’s their responsibility to correct the functionality, not the library developer’s.
Part of this difference may stem from organizational influence: I’ve always only been developing for small teams or open source, both instances where it was politically feasible to do declare usage of non-public APIs to be not-my-problem.
This is a nice sentiment, but it doesn’t pan out in practice. No matter how many warnings and caveats you put in your code comments and documentation, people end up using those APIs and they get mad at you when you break them. Wrapping things up in opaque structs is worth it, if only to spare yourself from the abuse.
The main issue we ran into at Pebble is people allocating a struct by simply declaring a variable of that type, e.g.
foo_t my_foo;
Then we’d ship a new version of our OS, and all those apps would break until they were recompiled. This is why every SDK from version 2.0 on used opaque pointers and forward declared structs. Can’t allocate something you don’t know the size of :-).
e.g. the Layer API still documented courtesy of the Rebble folks.
I can see where they would be valuable if the culture doesn’t support the “public API didn’t change ” approach.
Now I’m curious how apps were calling into the OS such that you could upgrade the OS without recompiling (or at least re-linking) the apps in the first place.
I heard once an argument against the use of goto which said, “You might be able to use gotos correctly, but you can’t guarantee that the developer who’s going to take your place can use gotos correctly. If you want your code to be long-lived, design it simply, without gotos, so that even less-skilled programmers can work on it and improve it.” (I want to say it came from “Timeless Laws of Software Development” by Jerry Fitzpatrick, but I can’t seem to find the quote now).
I feel like there’s a similarity here: there’s nothing inherently wrong with goto/transparent structs, except for the fact that if the code is to be used or worked on by another individual, you have the highest chance that your code survives if it’s written in a way the prevents that individual from using it recklessly (like @francois said has happened to them). Opaque structs are like safety belts.
Great rundown! The concept of OOP techniques in C has also interested me recently. I just finished a small project where I explored and compared all of the ways (that I could find) to implement OOP-like concepts using C. All I did was group together various techniques listed by James Grenning, Miro Samek, and Axel Schreiner, but I also tried to organize it by complexity, so folks could easily see which OOP techniques were the easiest to implement and locate the one that they needed for their projects. Maybe someone out there will find it useful!