0. It doesn't matter, everything goes, it's C, do whatever you like!
1. Who are you
really writing for?
If for the machine, do whatever you like.
If for other programmers -- give some consideration to what they will find understandable.
This includes your own future self! An issue you may've noticed already, or maybe not if you're still fairly new to this, or just very consistent in your writing style. (Which could kinda mean two things: that you're very consistent and familiar with your own dialect (or many dialects), or that you indeed format and comment things well already. Preferably, the former
is the latter.
)
2. What is the purpose of this issue?
Globals are readable and writable by any module in the system, whether they "should" be able to or not. Mind, that means semantically: C of course doesn't give a shit if you trample random memory, that's your own prerogative. It's about visibility and scoping, and how much interface is exposed between modules.
Note that, depending on compiler version and declarations, globals may be automatically shared, or not. In GCC < 8 I believe?, they were shared; after, not. Scope can be controlled by prefixing
static (local to module) or
extern (shared).
The temptation is that, with globals shared and scoped, they become part of the interface, whether intended or not, and complexity grows massively. Every outside reference to a variable, is one more reference you need to keep track of, with respect to state changes within the module (if indeed the variables declared within it are mostly/entirely used locally -- you don't have to, of course..!), the consistency thereof, and what ways the module can be interacted with.
Ultimately, what this drives towards is the matter of reusability, that a module should be largely self-contained, expose only its documented interfaces, and enforce that as such.
A recent example I used is this,
https://github.com/cwalter-at/freemodbus/tree/master/modbusIt's fairly old and mature, actively maintained (well, there are a couple PRs sitting around for a few years, but last I looked, all quite minor and easily implemented yourself), and very modular. You implement the physical/interface bits in the
port files, implement the callbacks (messages translated into read/write commands, you implement the read/writing yourself), pick the interface you want (on init), then poll in a
main() loop. The code is fairly compact, straightforward -- Modbus is a pretty thin and simple stack, but a stack it is -- and easy enough to understand. So, I think it's a good illustration of how convenient this style of design and organization is.
Which leads into:
3.
https://en.wikipedia.org/wiki/Dependency_injectionAs mentioned above, it's another design pattern that can help simplify things.
The apparent downside for embedded purposes is, all this boilerplate doesn't matter, you're only ever going to use one configuration, and you're checking memory regularly (well, maybe) and it's all flat in the end, who cares.
Well, the overhead is small; in C, the function pointers used during MBinit() for example, will always emit a load, call (register indirect) operation (or, I think so, anyway?). Distinctly slower than absolute call, let alone inlining the thing whole. But how often are you calling MBinit()? Just once? Yeah who cares about an extra five or 20 cycles. And the polling is done, whatever, say every millisecond, what's another couple dozen cycles on that, who cares?
The worst part is probably interrupts, which are often written as callbacks in these sorts of things (see also: STM32 HAL and the like), so all registers on the ABI's clobber list must be saved and restored around the call. But here still, it needn't be a problem. For Modbus at least, conventional baud rates are slow (9600, 19200...) so few interrupts can fire per second. YMMV for whatever kind of module, of course, but the point is, many things aren't performance sensitive, or nearly as much as you're apt to think in the embedded world.
In C++, more of these are likely to be optimized down, AFAIK, so, a constant function pointer at compile-time may end up inlined, say. In that case, there's literally no downside.
Exposure to newer languages probably helps out as well. Being familiar with say Java Objects (everything is an Object, no pointers, only references), or lambdas or other functional things (Factory design pattern; passing functions as parameters or objects; etc.), gives you a higher level perspective that you'd never realize deep down in the trenches of plain C. Even if you're never using the full form of those structures, knowing that they're there, and that you could express what you're doing as a reduced form of one of these constructs -- can help organizing things.
Tim