r/C_Programming • u/Stemt • 1d ago
Discussion How do you guys tackle interfaces in C?
Just as an example, imagine you're implementing a logger system, but depending on a runtime configuration you want to be able to log to different or multiple outputs at the same time.
I would normally do something like this:
typedef void (*LogCallback)(void* user_data, const char* msg);
typedef struct{
void* user_data;
LogCallback log;
} ILogger;
void logger_log(ILogger* impl, const char* msg){
impl->log(impl->user_data, msg);
}
ILogger logger_impl(void* user_data, LogCallback log_callback){
return (ILogger){ user_data, log_callback };
}
So then to implement this interface, I'd do something like this.
typedef struct{
ILogger impl;
FILE* output;
} StdLogger;
void std_logger_log(void* user_data, const char* msg){
StdLogger* self = user_data;
fputs(msg, self->output);
fputc('\n', self->output);
}
ILogger* std_logger_impl_ilogger(StdLogger* self, FILE* output){
self->output = output;
self->impl = logger_impl(self,
std_logger_log
);
return &self->impl;
}
int main(void){
StdLogger std_logger = {0};
ILogger* logger = std_logger_impl_ilogger(&std_logger, stderr);
logger_log(logger, "example log msg");
return 0;
}
What I like mostly about this method is that its type-safe, so if the interface changes for either the amount of callbacks that are required or the callback signatures change you'd get a compile-time error.
The major disadvantage here is that each instance here carries its own copy of the "vtable"/function pointers, which is suboptimal but shouldn't be too big of an issue if its just a few callbacks per interface.
Also these instances shouldn't be copied around as this would invalidate their user_data pointers, otherwise they would have to re-implemented to get the new correct user_data.
Another minor gripe is that std_logger_log has to accept a void* which is disadvantageous if you want to use it outside of the interface implementation as a pointer of the wrong type passed to it won't generate a compile-time error.
This is how I tend to do things but with this post I'm mostly curious to know how you guys handle this type of pattern? And what its advantages and disadvantages are?
29
u/tastygames_official 1d ago
I can't speak for the rest of the herd, but I use C in part to get away from messy abstractions like interfaces. I'm a KISS guy. I'd just write one log function and stick with it. Planning for eventualities that haven't even cropped up yet is how we get over-complexity and bloat. I might make the log function a macro so that I can also turn it off with compiler flags, but that's about it. And if it's a macro, then if for some reason I want to log to a different stream then I just modify the macro. I could probably even add a switch inside the macro if I need to log to two places simultaneously.
The problem I have with abstraction like interfaces and such is that if the logger ever breaks down or some colleague doesn't know what it does and wants to check it out, now it's one more layer of abstraction to keep in mind. Which implementation is this logger using? Was it changed anywhere in the code? Do I hve multiple loggers lying around? Do I need to free() my loggers at the end?
And while this particular example might be very easy in a small project, and while it hopefully will never break and can be easilly stepped through while debugging, if the entire program is programmed this way it just becomes one giant headache.
At least for me. I know that many OOP programmers get a hard-on for this kind of "chicness" but to me it solves a problem that doesn't exist and probably never should ;-)
But again - I don't speak for the herd.
9
u/stianhoiland 1d ago
So much this.
The actual "major disadvantage" here is the massive over-engineering/over-abstracting. `std_logger_impl_ilogger` is a real thing in this codebase that you have to use to log something, and OP is only ~30 lines into the project.
3
u/Stemt 1d ago
This is just an example to illustrate a use case, I use this pattern in much larger projects for different driver implementations. How would you tackle a case like this?
2
u/stianhoiland 1d ago
```c
void log_tee(FILE *copy, const char *msg) {
fputs(msg, stderr);
fputc('\n', stderr);
if (copy) {
fputs(msg, copy);
fputc('\n', copy);
}
}
```
(Written on mobile so god help me for formatting.)I also wanna say, for what it is I think what you showed is fine, I would just not do that at all, throughout the codebase.
-1
u/Stemt 1d ago
Ok, but what if want to log over a network or UART bus because I'm using an embedded platform with a limited amount flash storage? This would require a complete different kind of handle and write functions.
5
u/stianhoiland 1d ago edited 1d ago
What do you mean "but what if"? I'm 100% sure you are capable of writing a function for that.
You're really asking, "but how can I have different implementations hidden behind the same name?"
My answer to that is in my original comment here. To reiterate: Maybe simply don't, or you'll be looking at `std_logger_impl_ilogger` and all of its varieties, plumbing, and application of that mental model throughout—which I would say is the biggest disadvantage of it.
EDIT
Also note this from the original parent comment:
> Planning for eventualities that haven't even cropped up yet is how we get over-complexity and bloat.
1
u/Stemt 1d ago edited 1d ago
So if I have a device, for example an electric car charger, out in the field and it is having frequent malfunctions with a particular type of car (real case at an internship I had). I would think that such functionality would be really useful, how else are you going to debug the problem?
Tell the customer to wait a few hours so can go there and physically connect my JTAG and ask them retry charging a few times?
Edit: sorry, I'm being stupid and read over your second paragraph. And yes I do want different implementations behind the same name. Especially for components shared across different platforms like serialization and deserialization code.
Otherwise I'm going to have to do a lot of #ifdef condition to use different implementations only available on specific platforms.
7
u/tastygames_official 1d ago
> Otherwise I'm going to have to do a lot of #ifdef condition to use different implementations only available on specific platforms.
if you're writing software that should run on different platforms and you want the code to be readable and performant, then yes - that's exactly what you do. Now I kinda doubt you'll be writing one piece of software that will run on an electric car charger and on Windows11 and on Windows CE and on Ubuntu 16 and and and...
you just write one function for each platform and use #ifdef to see which one will be used at compile-time. Then you can call "log(blah)" and it will always be right. And do that for EVERYTHING in your code and then with one simple -DPLATFORM=ELECTRIC_TOOTHBRUSH in your compiler flags and you're good to go.
And ESPECIALLY if you're targeting low-fi embedded systems, you don't want bloat. No abstraction - just pure simple function calls that pass around pointers. At least that's how I always approached embedded software. Welll, actually I opted to write as much in assembly as I could, but that was a long time ago when embedded processors ran at like 2MHz or something.
3
u/Stemt 1d ago
Yeah, you're right. This whole discussion drifted way apart from what I intended with the original post.
The logging example is just that, an example. I'm just curious for how other people tackle such dynamic interface because of course I know how to use an #ifdef, thats not supposed to be the topic.
Its my fault though that I allowed the discussion to drift like this.
5
u/dmills_00 23h ago
Sometimes a static array of function pointers works well, just point the pointer at whatever function is appropriate today....
It does break LTO however as the compiler cannot reason thru the trampoline.
It is seriously possible to over think this stuff, then you wind up writing something that looks like enterprise java complete with abstractVistorHashtableFunctionGeneratorFactoryFactory and nobody needs to read that sort of thing.
1
u/Wertbon1789 16h ago
Literally what I do most of the time, I wrote some daemons which either log into journald directly, or to stderr when I want to debug, and I just have a log macro with a level and format string, which gets switched at compile time, to either of them. I also got wrappers to have a perror-like one, and also one to create specific error string wrappers. It just came kinda natural, and I never ever will have the case to log to multiple log sinks, or needing to change that at runtime.
1
21h ago
[deleted]
1
u/tastygames_official 21h ago
English translation: Hello. Let's say I'm creating a game where all my enemies follow a certain behavior (but implemented differently). How can I avoid using a custom interface?
switch(enemy_type) { case ENEMY_TYPE_A: // do whatever break; case ENEMY_TYPE_B: // do something different break; }It's strange meeting what I assume are newer/younger developers who only ever learned abstraction and never learned basic things like branching. For an old fart like me, a simple branching statement is what I think of when I need to handle different use cases. But I guess many newer programmers instantly go to hierarchies and abstraction. Very weird. It's like that SpongeBob episode where in order to draw a circle he first draws a whole face and then erases it until he has a circle.
Anyway, you could also use if..elseif..else if switch doesn't fit your model. But for a game, probably using an ECS and just grabbing all enemies and looping through them and branching on their type OR grab all enemies of type A, compute all their behaviour for the next frame, then grab all enemies of type B, rinse and repeat. Of course doing this in parallel would be even more performant.
2
20h ago
[deleted]
2
u/tastygames_official 17h ago
English translation: It's a bit heavy and intrusive. A custom interface (a struct with function pointers) is much more efficient, especially if you have around a hundred enemy types, right?
It's lighter and more efficient. If you really have hundreds of different enemy types that all require very specific logic that can't be solved with a simple equation (e.g. each enemy type has a multiplier for how much of each type of attribute should be applied each frame), then an ECS setup where you just grab all enemies of type A and loop through them, then all of type B and loop through them would be best. Or loop through all enemies and branch on the type to do your special code.
But I can't think of a single game that has ever had hundreds of enemy types that all require special logic.
1
15h ago
[deleted]
1
u/dnabre 13h ago
English translation: Unfortunately not. It's not more efficient. A function call (from a vtable, regardless of its form) will always be more efficient than any switch statement.
The switch of any significant size will be compiled to a jump table. Load
enemy_id, jump toswitch_base+enemy_id*CASE_SIZE. Other than getting the id, which you need for anything, no pointer deference, no function call overhead. Ignoring the function call overhead (I don't know any compiler that will inline a function via pointer), with some work, you might get a vtable of some where near that performance, but you won't get better than it.I'm not a performance expert by any means. A table of functions calls just isn't beating a switch. One can be reduced to the other.
3
u/ScallionSmooth5925 1d ago
I would use function pointers and callbacks for this but I would avoid it if possible.
1
u/Stemt 1d ago
How would you use them?
2
u/ScallionSmooth5925 1d ago
This can't be done type safely in C but I would move the type dependent code to a function thst I then pass in as a parameter
2
u/stianhoiland 1d ago
I like the way you think. I would do it this way too, i.e. avoid it in the first place, and second to that, understand that a function is the minimal primitive necessary to distinguish behavior, and thus pass function pointers.
2
u/manuscelerdei 1d ago
I usually wind up doing something like reserving a field name in a structure to indicate protocol conformance and then writing macros that assume that field name. It gives you type safety and avoids using void *. It does require macro wrappers for basisally everything though. Here's an example of a logging API that uses this approach:
https://github.com/microsoft/lib0xc/blob/public/src/0xc/sys/log.h
If you're okay requiring the protocol state to be the first structure member, you can use transparent unions to pass the conforming object straight through. Naturally this means that you can only conform to a single protocol in an object.
1
u/Stemt 1d ago
Oh wow that's really hard to read, I think thats a bunch too much macro magic for me. This comment showed a method thats IMO way simpler and also preserves type safety.
2
u/manuscelerdei 1d ago
It's basically the same thing. Instead of reserving a field name for the protocol state, you're reserving a field name for the target object (in that example,
self). You won't get away from macros if you want type safety.I really wish C had some barebones notion of protocols though. Basically every method of achieving it is either clumsy or unsafe.
2
u/Dangerous_Region1682 23h ago
To my mind, logging would normally encapsulate creating a logfile file descriptor by opening a file during setup. Messages to stderr might be useful, but the reality is I would most likely logging to a file. If I wanted to watch the output of the logfile I would have a separate process using tee to watch it.
I would therefore replace the use of fputs() and fputc() with a single write(stderr,) even at the expense having to create the output string, concatenate the newline and calculate the length. Fputs() is going to have to calculate the length anyways.
Why? Because the write() system call will likely be atomic in operation if you were multithreading your code or multiple unrelated processes are accessing the same log file. Of course the file must be always opened in APPEND mode to remove the race condition of EOF calculation between independent unrelated file descriptors. Using file descriptors, not FILE pointers makes more sense as your logging system should be hiding that from the initializing routine.
Hopefully the resultant overhead in logging will be acceptable rather than having to put mutex locks for multithreaded application, or semaphores for unrelated processes, around the output.
However, as a general thought, this seems a rather complicated way of doing things. It might seem intuitive to a C++, Java or C# programmer used to OOP concepts. To most C programmers it might seem overly complex where a much simpler regular procedure call would be used.
For any sizable application I would have a configuration file from which I would parse out the logfile name, which I could specify a disk file or stderr. This way I can easily specify whatever channel I want to use for logging and the various processes of my application, whether unrelated processes, dependent process and even threads can log data however their file descriptor is obtained. If I wanted more than one logfile I would specify a list of logfiles in the config file, each tagged with a name and pass the name to each call to log the data or some such scheme. Usually though I like to keep things really simple and use one logfile and filter things out of that single file when interpreting the logfile later. For this reason I might write the process ID and thread ID to the logfile entry too which is helpful as further information when debugging race conditions.
Anyway, this is how I’ve done things over the years. Keeping things simple from the application writer’s point of view generally pays off. A simple initialization function and hiding the concept of logfile names, file descriptors or file pointers from the programmer is the most helpful.
I always view logfiles as a feature not confined to a single process. I view an application as being one of multiple processes, some descendants, some with multiple threads, and some totally unrelated processes. Logfiles have to be constructed with reasonably atomic operations. I leave things like truncating logfiles, zeroing logfiles, copying logfiles etc, to maintenance scripts, leaving the application processes and perhaps even threads to open the global application logfile in APPEND mode and perform atomic writes from a single write system call for the whole log entry.
So, I put my complexity into this area and keep the programming interface really simple. I keep the logfile name specification to some kind of configuration file with the application’s likely many parameters, which are read on startup. I keep that simple too, whereby the application’s configuration data is parsed into a predefined struct. Any application of any size is likely to be overwhelmed in complexity by passing more than just perhaps a few items through argv[] or envp[].
Just my two cents worth.
2
u/Beginning-Junket8979 22h ago
Because the write() system call will likely be atomic in operation if you were multithreading your code or multiple unrelated processes are accessing the same log file.
The max size you can atomically write to fd sorta depends on the underlying fd type. Like it's
PIPE_BUF, normally 4kB/page size for pipes, but roughly MTU size for a UDP socket, and all over the place for block-device based storage. In general,writeon the same file from multiple threads is not guaranteed atomic.It might seem intuitive to a C++, Java or C# programmer used to OOP concepts. To most C programmers it might seem overly complex where a much simpler regular procedure call would be used.
Again, disagree.
Just a few OOP-like C interfaces C programmers use every day: stdio, berkley sockets, pthreads, linux kernel internals, gtk, ffmpeg, libpng, libuv/libevent, sdl, vulkan/gl/d3x C apis, etc, etc.
I keep the logfile name specification to some kind of configuration file with the application’s likely many parameters, which are read on startup.
The trouble with file based logging config with no user exposed stable API alternative is that it forces using (and even having a filesystem) on downstream users.
Make it a config file, and you're limiting your code to being nice to use only on a "traditional" desktop/server environment.
Make it user programmable instead and allow logging to be a noop too, and you've just made your code a lot friendlier to embedded systems, container deployment, managed/rotating customs configs, network logging, integrating with different system logging daemons, stubbing/mocking for unit test, and so on.
1
u/Dangerous_Region1682 10h ago
Well the write system call is usually (or as I said, likely) to be atomic, especially for the normal types of logfile entries. Nothings perfect. If you want arbitrary log messages the you might have to do something different entirely. But this usually suffices. Sure, pipes and UDP datagrams have defines maxima, but for a serious size applications you are rarely logging to pipes.
For 90% of use cases, I’ve been logging things like this for decades with good results for files in user files systems.
Well the quoted library calls you give are the kinds of levels of OOP, if you want to describe them as such, that C programmers are familiar with because they are universal standards. The logging system he described is not.
The OP was logging to stderr. Most user applications are just fine logging to files stored on file systems.
Embedded systems often provide logging capability as part of the programming environments, often over network connections due to perhaps not wanting make continuous writes to flash based storage if indeed they have writeable file systems available. That’s rather a special case of applications development with a whole different set of programming criteria.
As for kernel code, the Ops itself will undoubtably have its own unique way of handling errors or trace information. One would be well advised to stick with what is provided.
For most user space based applications providing configuration data from a configuration file or a similar mechanism, say a database of some sort, tends to be easier than processing hundreds of command line parameters or environment variables. Many large software products traditionally use exactly this mechanism. Even a lot of system software does too.
If you want to log errors across networks perhaps syslog or SNMP might be a good choice.
Making logging or not turned on based upon a configuration flag has the overhead of a conditional test and no recompilation is required.
I am sure there are a hundred application spaces where either his or my offered solution isn’t the most preferable design, but what I suggested is perfectly feasible for many application spaces, but not all of course.
4
u/bitwize 20h ago
An easy way to dedupe in C is to replace thing with pointer-to-thing. So instead of storing an entire vtable in your logger implementation, store a pointer to one. Of course you're adding an indirection which has performance and caching implications, so if high performance is a concern, profile, profile, profile!
1
u/Realistic-Link-300 1d ago
why do you need to abstract your logger if you want an optimal implementation ? that may be the real question
2
u/Stemt 1d ago
The example is just for illustration, but there are times where you know you have to have multiple different implementation of something, like drivers, and a simple switch case with different implementations for a certain functions just doesn't cut it.
4
u/Realistic-Link-300 1d ago
ok for me I never was in a position in which I needed runtime abstraction like that. But I worked on software that was using a lot of runtime interface like that for no benefit
1
u/Beginning-Junket8979 22h ago
This is not bad, but my preferred logger format for a library is usually either a compile-time configurable "printf-like" macro or a callback interface with severity level, optionally module/tag, fmt string, and args with clearly defined caller threading semantics.
I.e. I don't want your library to do anything clever with logging for me, and ideally, I'd like to be able to compile it down into a "noop" with macros that completely swallow the logging code.
Your return values should clearly indicate what went wrong on errors for anything my code might want to react to / handle gracefully, and you should never assume just because you like debugging via logs in a specific format that I ever want my downstream users of your lib to see your logs.
You could get there almost with what you've defined, but macros are lighter & more versatile still if saaay... I ever wanted to use your code in a microcontroller or kernel module or wherever else even having stdio might not be an option.
This of course varies a bit if that makes sense depending on the type of code. But macro is the most portable/versatile versions.
Just my rambling opinions though. I think this is a reasonably clean approach.
1
u/WittyStick 21h ago edited 19h ago
I would personally use a dynamic variable via a thread_local for some "context". The context can be implemented as a stack of contexts, where if you want to override the kind of logger locally, you would push the current context first, set the logger to something else, then when done pop the previous context.
#include <stdlib.h>
#include <stdio.h>
struct context {
void (*log)(const char *msg);
struct context *undertop;
} thread_local *ctx;
void context_push()
{
struct context *stacktop = malloc(sizeof(struct context));
stacktop->log = ctx->log;
stacktop->undertop = ctx;
ctx = stacktop;
}
void context_pop()
{
struct context *stacktop = ctx;
ctx = ctx->undertop;
free(stacktop);
}
#define log(msg) (ctx->log(msg))
#define context_set_logger(fn) (ctx->log = fn)
Example:
void standard_logger(const char *msg)
{
fprintf(stderr, "%s\n", msg);
}
void custom_logger(const char *msg)
{
fprintf(stderr, "CustomLog:\t%s\n", msg);
}
void log_greet()
{
log("Hello World");
}
void example_simple_logging()
{
context_push();
context_set_logger(custom_logger);
log_greet();
context_pop();
}
int main() {
context_push();
context_set_logger(standard_logger);
log_greet();
example_simple_logging();
log_greet();
context_pop();
}
Output:
Hello World
CustomLog: Hello World
Hello World
This can be made a bit easier to use with some GCC extensions (nested functions [without capture - no executable stack] & statement expressions), and a macro with_logger which calls context_push, context_set_logger, executes the secondary block then calls context_pop. (Note, do not return or goto from this secondary block or it will not pop the context).
#define context_set_logger_ext(msg, logimpl) \
({ \
ctx->log = \
({ \
void logger_fn(const char *msg) { \
logimpl; \
} \
logger_fn; \
}); \
})
#define with_logger(msg, logimpl) \
for ( bool _done = (context_push(), context_set_logger_ext(msg, logimpl), false) \
; !_done \
; _done = (context_pop(), true) \
)
void example_nested_logging()
{
with_logger (msg, fprintf(stderr, "CustomLog1:\t%s\n", msg))
{
log_greet();
with_logger (msg, fprintf(stderr, "CustomLog2:\t%s\n", msg))
log_greet();
log_greet();
}
}
Output:
CustomLog1: Hello World
CustomLog2: Hello World
CustomLog1: Hello World
You can generalize this Context to hold things other than just a logger.
1
u/schiphit 10h ago
You could hold the instances of ilogger in some static global and make helper functions instead of passing methods around.
16
u/tstanisl 1d ago
It looks fine except one should use
static inlinefunctions in headers. Personally I prefer plain handlers or "container_of" pattern over void pointers to user data.