Practical Design Patterns: Opaque Pointers and Objects in C | Interrupt

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?


This is a companion discussion topic for the original entry at https://interrupt.memfault.com/blog/opaque-pointers

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++ :slight_smile:

1 Like

There a fatal bug in this code :

ringbuffer_t ringbuffer_create(uint32_t capacity) {
    ringbuffer_t inst = calloc(1, sizeof(struct ringbuffer_instance_t));
    inst->data = calloc(inst->capacity, sizeof(uint8_t));
    inst->capacity = capacity;
    inst->wr_pos = 0;
    inst->rd_pos = 0;
    return inst;
}

inst->capacity is used uninitialised in calloc() function !
Lines 3 and 4 have to be swapped.

@DrPi Good catch! I’ll get that fixed.

It should just be using capacity in the calloc call. I think I rearranged the lines, but forgot to update.

It’s reminder for me to always compile my sample code! Although, now that I think about it, the compiler would not have caught this.

@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 :woman_shrugging:” 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.

Our APIs were trampoline functions hardcoded to specific address. In effect it looked like this:

static const __attribute__((sections(".api_table"))) uintptr_t 
api_funcs[] = {
  &api_one,
  &api_two,
  &api_three,
};

void api_one(void) {
  _real_api_one();
}
...

Then we’d set things up in the linker script so that api_table was at a known offset.

Interesting. So somewhere in user-land was something akin to void (*api_one)(void) = api_funcs[1]; ?

Basically, with some syntactic sugar to make it nicer to use.

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!

Very interesting @nathancharlesjones, I just cloned your repo. looks like @ncmiller was the original author, but it appears like ‘discobot’ wrote it.

1 Like