Solved "Options" objects in C

zirias@

Developer
I'm currently designing a library that will have some "classes" which offer "complex" configuration, so a constructor function with a long argument list won't fit the bill. An idiomatic solution to that problem is to have a separate "options" object which is passed to the constructor.

Now, doing that in C, you could just publicly define a struct for the options, like
C:
typedef struct FooOpts {
    int bar;
    const char *baz;
} FooOpts;
and use it like this:
C:
FooOpts opts = {
    .bar = 42,
    .baz = "helloworld"
};
Foo *foo = Foo_create(&opts);

That's nice and clean, but has a bad drawback for a library interface: Whenever you need to add more options, you will break the ABI. :(

Now, you could of course make FooOpts opaque as well to solve that problem, then using it would look something like this:
C:
FooOpts *opts = FooOpts_create();
FooOpts_setBar(opts, 42);
FooOpts_setBaz(opts, "helloworld");
Foo *foo = Foo_create(opts);
FooOpts_destroy(opts);

This will make for an evolvable library API/ABI, but comes with some overhead, both in "boilerplate" code and at runtime (yet another heap allocation and lots of function calls).

Does anyone have a better solution in C?
 
Thanks yuripv79, but it's not immediately obvious to me how this would help here? Besides, I should have mentioned this, the planned library won't be FreeBSD-specific, the goal is portability to any POSIXy system 😉 (I just ask here because the FreeBSD community is full of capable coders, hehe).

What I want to achieve is an interface that doesn't need an ABI change just to add a new (optional) feature. And now I wonder whether I really have to pay the "full price" for that as outlined above, by opaquing even "options" objects.... :-/
 
Ah, I see, so it would solve the aspect of "boilerplate code" :cool:

Well, I'll pass on it, for portability. And the conceptual issue stays the same anyways. :( My gut feeling is: opaquing will be the only way to achieve a "stable" ABI here. But hey, doesn't hurt to ask 😉
 
Can you sidestep the issue and do some sort of lazy loading setup and avoid the need of complex constructors (or constructor lists) entirely?

Code:
Foo *foo = Foo_create();
Foo_setBar(foo, 42);
Foo_setBam(foo, 43);
Foo_setBag(foo, 44);
Foo_bringHappyness(foo);
Foo_setBof(foo, 45);
Foo_bringHappyness(foo);
Foo_destroy(foo);

Foo_setBof (and all of the setters), also flag a "dirty" boolean which alerts the object that it needs to resync during the next Foo_bringHappyness. I tend to do this for OpenGL vertex buffers that need "resyncing" to the GPU. Each time I add a new i.e Vec3, it sets that dirty flag to resync upon the bind.

It will simplify the API (albeit perhaps not the code underneath), people rarely like construction objects (dangerously close to factories) and in practice, the construction functions tend to not want to do too much processing within them anyway.
 
Can you sidestep the issue and do some sort of lazy loading setup and avoid the need of complex constructors (or constructor lists) entirely?
Not without a major redesign (and I'm not sure whether that would make sense) :(

Practical example: One of my classes needing configuration is my Server class, which is an abstraction for a listening socket (or even a set of them, when there's more than one resolvable address for a given name, or the user wants to listen on multiple addresses).

Right now, Server_create(ServerOpts *opts) does everything from name lookup to establishing all required listening sockets. I think that's the natural expected interface. To do what you suggest, I would have to split that, so that Server_create() creates a "half baked" object that's then configured with other methods and finally I would need something like a Server_start() method (which could of course fail as well). IMHO, that's a bit counter-intuitive. :(
 
creates a "half baked" object that's then configured with other methods and finally I would need something like a Server_start() method (which could of course fail as well). IMHO, that's a bit counter-intuitive. :(
I see. If an object *can* do something atomically during creation, then it probably should. Probably my suggested approach more suited to something that acts a bit more like a container.

How about the Var-args approach. I.e XtVaCreateManagedWidget?

Code:
Server_createVa(foo, "localhost", bar, 32, baz, &iface, NULL);

This works well if parameters are limited to pointers (incl strings), integers and basically anything smaller than a pointer. You can probably get creative with MACROs too in order to prevent a loss of type-safety.
 
Server_createVa(bar, 24, baz, 32, boz, 39, NULL);
Hm, that could be something to provide additionally. I guess for some cases, it would make sense (maybe the configuration of my ThreadPool, I could imagine that would often be hard-coded in practice). I have a doubt and a concern about it:

The doubt: How often can this interface be used in practice? Getting back to my example of the Server class, many configuration options for it will probably be passed down from user configuration in practice. And then, a varargs interface won't help.

The concern: This style can't be checked statically (the compiler won't be able to find errors).

BTW, regarding factories: They're not bad per-se. They're just massively over-used (especially in "enterprisy" software 😏). But my approach here is more or less the counterpart: Instead of delegating the actual construction to a separate object, you just use a "dumb" separate object to hold all the options but still leave construction to the class itself.

I guess what I really deal with here is a limitation in the C language design. The only way to really hide information is by using some opaque pointer, and that requires "dynamic" allocations :(
 
The concern: This style can't be checked statically (the compiler won't be able to find errors).
I think with some MACROs some of the compile-time checking can be reinstated. i.e:

Code:
Server_createVa(ADDRESS("localhost"), PORT(32), INTERFACE(&ifaces), NULL);

Where i.e ADDRESS is a MACRO like:

Code:
#define ADDRESS(A) \
  "address", check_string(A)

If you don't care about C99 or older, you could use VAARGS in MACROs to avoid that last "NULL".

Is it naff? Yes.

I was in a similar position to you with a renderer a while back at work. The difficulty that floats were quite common so the var-args approach didn't fit. I just went with RenderOptions struct like your first example, something like this.

Code:
struct RenderOptions opts = ReDefaultOpts();
opts.frame_buffer = some_render_texture;
opts.model = ReMat4Perspective(...);
opts.mesh = some_mesh;
opts.backface_cull = 0;
opts.depth_test = 1;

ReRender(&opts);

I wasn't massively happy with it so if you do find a better solution during your travels, please do share. :)
 
If you don't care about C99 or older, you could use VAARGS in MACROs to avoid that last "NULL".
For some time, I tried to stick to C89/C90, because even C99 wasn't fully supported by MSVC. That's long ago. No idea what MSVC supports nowadays, but I just don't care, you can build your stuff for Windows with GCC or CLANG (using e.g. MSYS2). Long story short, all my current C code just requires C11 :cool:

I wasn't massively happy with it so if you do find a better solution during your travels, please do share. :)
I think this model is ALMOST perfect, it's certainly simple and clean. The only issue is when you use that at a library boundary and can't guarantee the options object will never be extended, then you'll break ABI :(

I guess I will indeed go with the "full opaquing" approach, although it is not pretty ....
 
The only issue is when you use that at a library boundary and can't guarantee the options object will never be extended, then you'll break ABI :(

Indeed. You can always go with classic padding. Consider boo being a late addition.

Code:
struct ServerOpts
{
  int foo;
  float bar;
  char baz;
  double boo;
  char unused[4096 - sizeof(boo)];
};

Maybe? relevant,
I don't believe this will help solve the OP issues. That said I have spoken to Daniel Holden (the developer of libcello) a few times. I believe he works at Epic these days. Very interested in his work from my own projects involving MACRO infused madness. He does provide a disclaimer that this probably shouldn't be used in library projects because it is all a little bit weird. Looking through it in the past, perhaps only the garbage collector is the only non-portable / undefined-behaviour part.
 
kpedersen, what an UGLY hack, but it looks like it could work, at least as long as a representation of zero is always the neutral/default element. Of course, that one will "waste" stack space 🙈 – but let me think it through ... :cool:

edit: slightly improved:
C:
typedef union ServerOpts {
    struct {
        int foo;
        char baz;
    };
    char so___placeholder[4096];
} ServerOpts;
 
One more idea:

Code:
ServerOpts opts = DefaultOpts(); /* DefaultOpts() is MACRO, not externally linked */

struct ServerOpts
{
  int apiVersion;
  int foo;
  int bar;
  int baz;
  int boo;
};

Now you can check apiVersion within your library to know if accessing boo is safe or not.
 
kpedersen, now that's an idea I had myself. I have the "gut feeling" it is fishy (incompatible types, UB, ...) (but then, maybe the union reserving space suffers from the same, although this could be solved by having ALL the versions in the union ...).

I don't know exactly how it could go wrong in some real-life implementation, but it isn't covered by the language standard. It reminds me of something Microsoft did in their win32 API though, many structs there have their own size as the first field 🙈
 
In the book "Clean Code" what-his-name talks about this and how operating systems handle changing arguments. I don't have time to rummage through the book but perhaps someone else does.
 
Thinking for a while again ... my thoughts now are
  • I won't finish this today anyways. Maybe soon time to get some sleep 🙈
  • I now tend to go directly for the "full opaquing" solution. Just because it's clearly well-defined in terms of the C standard. Boilerplate code? Ok, I can live with that. Runtime penalty? Well, how often do you need to construct such "complex" objects that need structured options? Probably not that often. So, no reason to "optimize" that.
  • Still, very interesting ideas discussed here, and I'm not entirely sure yet whether to change my mind ... thanks for that!
 
In my code i always used object-like stuff in C.

C:
typedef struct Apple Apple;

enum AppleColor {
    RED,
    GREEN,
    YELLOW
};

struct Apple {
    char* name;
    int color;
};

Apple AppleInit(char*, int);
void AppleDestroy(Apple*);

Apple AppleInit(char *name, int color) {
    Apple ap = {
        name, color
    };
    
    return ap;
};

And later usage:
C:
Apple myapple = AppleInit("baldwin", AppleColor.RED);
    
AppleDestroy(&myapple); // if malloc is used

Sorry if there is any suntax error, i am typing on phone.
 
I guess I finally have an idea how to avoid some of the drawbacks when "opaquing" these options objects. It hit me today while half through refactoring my code 🙈:

Why would you ever need more than a single instance of such a configuration object? I came up with: Either because you're constructing objects on multiple threads, or because some object must store its configuration for its lifetime.

But then, there's a much simpler solution: Have exactly one static and thread-local instance of every options object. Offer an "init" method to reset it to default values, and individual accessors to configure it. Once it's used for construction, just copy its contents into the newly constructed object.

So, I hope usage could e.g. look something like this:

C:
TcpClientOpts_init("www.example.com", 443);
TcpClientOpts_enableTls(0, 0); // no client certificate needed
TcpClientOpts_setProto(P_IPv6);
Connection *client = Connection_createTcpClient();
 
IFAIK Windows is handling ABI backwards-compatibility with a size member in structures:
Code:
struct Opts {
    unsigned size;
    ....
};
struct Opts opts;
memset(&opts, 0, sizeof(opts));
opts.size = sizeof(opts);
...
Later, in code, you can handle compatibility gracefully. Or you can go setsockopt(2) or ioctl(2) (possibly similar to what curl does)? Or you can use shared library versioning (isn't this what it is for?)
 
Back
Top