any1 / aml

Another Main Loop
ISC License
17 stars 7 forks source link

Use C11 _Generic instead of void * in public API #13

Open emersion opened 1 month ago

emersion commented 1 month ago

The public API allows the same functions to be used on different kinds of objects. This makes it pretty error-prone: it's easy to call a function on an invalid type without any compile-time error (either an aml type not accepted by the function in which case it's a runtime error, or a completely foreign type in which case it's just fireworks). C11 _Generic could be used to indicate which types are accepted by a function, e.g:

int _aml_get_fd(const void* obj);

#define aml_get_fd(obj) _Generic(obj, \
    const struct aml*: _aml_get_fd, \
    const struct aml_handler*: _aml_get_fd, \
)(obj)

(Note that in general I am not a fan of functions accepting multiple types of arguments, and would just recommend duplicating the functions into multiple wrappers.)

any1 commented 1 month ago

This would probably by an improvement. It could even be done without breaking ABI.

Another approach that I've considered is to approximate type traits via struct members. The interface would still be partially opaque, but the first members for the structs would be exposed. For example:

struct aml_handler {
    struct aml_obj base;
    struct aml_trait_startable startable;
    struct aml_trait_pollable  pollable;
};

int aml_get_fd(struct aml_pollable* obj);

Then you could do...

struct aml_handler handler;
struct aml aml;

...

int handler_fd = aml_get_fd(&handler->pollable);
int aml_fd = aml_get_fd(&aml->pollable);

aml_start(aml_get_default(), &handler->startable);

At least, this would be an interesting experiment in interface design.

emersion commented 1 month ago

It could even be done without breaking ABI.

Technically speaking using _Generic would be an API break, for instance this would fail compilation after the change:

struct aml_handler *handler = ...;
void *obj = handler;
return aml_get_fd(obj);

Though probably nobody does this in practice.

approximate type traits via struct members

This would be better. I can see these potential downsides:

any1 commented 1 month ago

Technically speaking using _Generic would be an API break, for instance this would fail compilation after the change

I don't mind breaking API as much as ABI.

That being said, I've been wanting to redesign the interface for a while anyway based on some observations that I've made:

If/when I do redesign it, I think I'll just do the right thing and duplicate those functions across different types. I did it this way originally because of laziness and a sudden urge to question conventional wisdom.

emersion commented 1 month ago

I agree with all of the points above.

any1 commented 1 month ago

I haven't removed the superfluous things from the API, but at least type safety is addressed: #14

emersion commented 1 month ago

Very nice, thanks for that!