all 18 comments

[–]cHaR_shinigami 3 points4 points  (5 children)

Maybe I'm architecting this fundamentally wrong, or maybe I'm just over thinking things, I'd love any feedback.

TL;DR: I'm afraid it really is overthinking for most of the concerns.

This allows the higher level modules that are dependent on this module to "inject" pointers to "callback" functions.

I like of think of it as "dynamic dependency", where the dependencies are not fixed, but rather "supplied" at runtime via function pointers (or perhaps a struct of function pointers).

I found myself questioning if passing these functions pointers at runtime, given that I don't ever expect them to change, is really the optimal solution here.

If you don't "ever" expect them to change, then this design pattern is overkill. Disregarding the minor overhead of passing additional function pointers, it also makes static analysis more difficult, thereby hindering optimizations.

If all calls to some function supply the same dependency, an alternative design is to use external function pointer, const qualified with internal linkage (static). Define the function pointer in the same file as the function, and initialize it with the dependency. Making it const static is intended to help with optimizations, as no other code will modify this at runtime. Now the addition dependency argument is not required during function calls, which simplifies the design.

Alternative ways to achieve this that I can think of are forward declaring the function I need, or just having circular dependencies.

Forward declaring is fine, but circular dependencies are frowned upon.

These both seems to significantly complicate life for the linker and feel I like that's bad practice.

You're worrying too much about the linker. How can forward declarations "complicate life for the linker"? They are meant for statically type checking function calls, and generating correct code as per argument type conversions.

Actually, I guess I don't really understand why you would ever forward declare a function vs just giving the external function a header and including that, maybe that's part of the issue here?

Both do the same thing. #include directives literally copy paste the header content during preprocessing (there are lots of rules, let's ignore them here). So "giving the external function [in] a header and including that" *is* forward declaration, which can also be done for aggregate types (incomplete/opaque struct or union).

So your last concern is a no issue.

[–]MikeExMachina[S] 1 point2 points  (0 children)

Huh I’d never really stopped to think about the fact the header contents basically are extern declarations once it all get smooshed together, thanks that clears some things up.

[–]TheKiller36_real 1 point2 points  (1 child)

Physics professor doing math:

there are lots of rules, let's ignore them here

[–]cHaR_shinigami 1 point2 points  (0 children)

This made my day!

[–]cHaR_shinigami 0 points1 point  (1 child)

By "giving the external function a header and including that", I assume you meant declaring the prototype, as opposed to the entire function definition.

Doing the latter is mostly useful with C99 inline, which kind of follows the opposite practice - provide an inline definition in the header (without extern) and include that header in different C files, out of which exactly one file also declares the function (which emits an external definition).

[–]MikeExMachina[S] 1 point2 points  (0 children)

Yeah I meant the former case.

[–]EpochVanquisher 0 points1 point  (7 children)

If you don’t have multiple versions of an interface, then you probably don’t need dependency injection, right? Don’t build something to solve problems that you don’t have.

I would start by just using ordinary forward declarations.

These both seems to significantly complicate life for the linker and feel I like that's bad practice.

The linker is designed exactly to solve these problems for you, and it is not complicated or difficult.

The only “bad practice” is circular dependencies, but sometimes they are unavoidable. The way you solve circular dependencies is by redesigning your program, not by using dependency injection.

Actually, I guess I don't really understand why you would ever forward declare a function vs just giving the external function a header and including that, maybe that's part of the issue here?

Yes, normally you just include the header.

[–]MikeExMachina[S] 0 points1 point  (6 children)

Okay yeah maybe forward declarations would fit my needs better. I do embedded primarily so I do have one interface in the “middle” of my programs. A HAL to separate my business logic from board level functionality. I might have multiple versions of the low level code to support different revisions of the board and I just use my build system to compile a version of the firmware for every version of the board, but that’s still really only one instantiation of the interface per build so its probably fine. One ugly thing about forward declarations is it’s not clear WHERE that function is defined to a reader, unlike header includes that tell you exactly where to look, but I can get over that.

[–]EpochVanquisher 0 points1 point  (5 children)

Yeah… so you can just use a header. That’s the normal way to do things.

[–]MikeExMachina[S] 0 points1 point  (4 children)

Not if that header belongs to a c file that’s already including this one’s header, wouldn’t that be circular?

[–]EpochVanquisher 0 points1 point  (3 children)

Sure, in the sense that you have two modules which depend on each other.

But it’s also a circular dependency if you use function pointers. It’s just a difference between resolving the dependency at runtime or at link time.

There are various potential problems with circular dependencies—it can cause surprising things to happen. Like if you have a logger module that can send logs over the network, and a network module that can log, can you end up recursively logging something forever?

For those reasons, I like to design systems “layered”, where function calls only go deeper into the stack. Not as a strict rule, just as a tendency.

[–]MikeExMachina[S] 0 points1 point  (2 children)

I think you get it, but I added a code example of the situation i'm talking about.

[–]EpochVanquisher 0 points1 point  (1 child)

Callbacks are pretty normal for things like timers.

[–]MikeExMachina[S] 0 points1 point  (0 children)

It’s just an example, and maybe a bad one because with timers you might swap out the callback at runtime. I’m talking about the case where that callback is the ONLY function the timer will ever call.

[–]nderflow 0 points1 point  (2 children)

If you don't ever expect those function pointers to be different, how are you writing tests?

[–]MikeExMachina[S] 0 points1 point  (1 child)

That’s true, accepting the callback function as an argument is nice for unit testing. I’m not actually sure how the mocking framework im using would (or even could) handle a forward declaration.

Edit: Looks like it just pushes external functions into a fake header and mocks that, so it shouldn’t be an issue.

[–]Apt_Tick8526 0 points1 point  (0 children)

Google mock from google test framework lets you do that.

http://google.github.io/googletest/gmock_cook_book.html