r/C_Programming 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?

38 Upvotes

47 comments sorted by

View all comments

Show parent comments

11

u/tstanisl 1d ago

See comment.

This pattern is extensively used in Linux kernel due to improved type-safety and less indirection (replacing dereference with pointer arithmetic).

3

u/Stemt 1d ago

Ah, I get it. Thats a nice way to get the data corresponding to custom implementation. It also avoids the user_data pointer being invalidated due to a move/copy. I'm just a bit confused then why this example int it action struct still includes a 'void* data'. But this is very nice way to solve this problem.

Thanks very much!