Core Design

The design of this library is centered around 4 things:

Starting from the beginning, it is necessary to talk about the enumeration and the structures and the extensible, ABI-resistant nature of the interface. Then, we have to address what happens if these extensible, implementation-defined attributes are not recognized by an implementation.

Extensibility and Future-Proofed API

One of the biggest issues uncovered in previous attempts to fix this problem in C (N2419) and C++ (P0320, P2019, P3072, and P3022) was the lack of extensibility and the lack of future-proofing against ABI issues. At the Minneapolis 2024 WG14 meeting, a vote was taken to allow undefined behavior for strftime and the broken-down time structures (see N3272, Option 1) due to the fact that it would be an ABI break in some places where users were directly copying structures. Our current interfaces do not have a great track record when they need to be updated, upgraded, or maintained.

Therefore, there is a need for flexibility in an interface which we expect to change over time.

One way to do this is with Windows-style Ex suffixes on functions and structures, alongside some built-in checking by having the first or last member of a structure be a cbSize member meant to be filled in by the sizeof(…) of the structure it’s contained within. However, a good portion of the C community (Linux and BSD-derivates, in particular) would likely riot over such a style of interface, even if it is common place in Win32-style APIs. Unfortunately, Linux’s and BSD’s typical approach to this problem is to simply write the direct struct and then hem & haw for decades over improvements, providing entirely new functions with new structures or just shrugging their shoulders and leaving every individual vendor and user to fend for themselves when the day comes that new interfaces are needed.

A way to resolve these concerns is to combine the slimness of the Linux API but the ability to have multiple structures like the Win32 APIs but without creating new entry points. But, that poses an additional problem: how do we keep a unified interface that can handle multiple different structures (multiple different thread attributes) now and into the future without getting caught on ABI issues that force us to make such improvements Undefined Behavior, as in N3272?

Solution: Using Tags

The solution for this is to recognize 2 different important qualities we need:

  • we need a tag to identify a structure for both a user and an implementation to recognize a given thread attribute;

  • and, we need a standards-compliant way of taking what will probably be a type-erased pointer to a structure and casting to the right thread attribute.

This would bring us to the first design, a void* plus attr_kind sort of tagging system:

 1#include <stddef.h>
 2#include <assert.h>
 3
 4typedef enum attr_kind {
 5        attr_kind_a,
 6        attr_kind_b,
 7} attr_kind;
 8
 9struct attr_a {
10        size_t stack_size;
11};
12
13struct attr_b {
14        size_t guard_pages;
15};
16
17typedef struct tag_and_attr {
18        attr_kind tag;
19        void* attribute;
20} tag_and_attr;
21
22enum {
23        do_some_success,
24        do_some_fail,
25        do_some_busy,
26        do_some_memory,
27        do_some_on_fire,
28};
29
30int do_something (size_t n, tag_and_attr attrs[]);
31
32int main () {
33        attr_a a = { 0 };
34        attr_b b = { 0 };
35        tag_and_attr attrs[] = {
36                { .tag = attr_kind_a, &attr_a },
37                { .tag = attr_kind_b, &attr_b },
38        };
39
40        int err = do_something(2, attrs);
41        assert(err == do_some_success);
42        // ...
43
44        return 0;
45}

One would then take a tag_and_attr[] – an array of such objects – to denote which actions to take. But this has obvious disadvantages native to C as a language: it is completely type unsafe. void* can point to literally anything. This isn’t helpful, and makes the interface less compelling and competitive against a C++-style implementation which uses templates to not only not pay for any extra code but can treat each attribute completely separately and properly. This is where a rule in C and C++ comes in to save us:

A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There can be unnamed padding within a structure object, but not at its beginning.

We can (slightly) improve type safety by noting that, so long as we have a attr_foo structure, if the first member is an attr_kind type, a pointer to that attr_kind will also serve as the attribute structure’s overall pointer. That means we can flatten our tag_and_attr structure to save on a very tiny bit of space, while improving type safety by putting the attribute kind into the structure itself rather than as a separate piece:

 1#include <stddef.h>
 2#include <assert.h>
 3
 4typedef enum attr_kind {
 5        attr_kind_a,
 6        attr_kind_b,
 7} attr_kind;
 8
 9struct attr_a {
10        attr_kind kind;
11        size_t stack_size;
12};
13
14struct attr_b {
15        attr_kind kind;
16        size_t guard_pages;
17};
18
19enum {
20        do_some_success,
21        do_some_fail,
22        do_some_busy,
23        do_some_memory,
24        do_some_on_fire,
25};
26
27int do_something (size_t n, attr_kind* attrs[]);
28
29int main () {
30        attr_a a = { attr_kind_a, 0 };
31        attr_b b = { attr_kind_b, 0 };
32        attr_kind* attrs[] = {
33                { &a.kind },
34                { &b.kind },
35        };
36
37        int err = do_something(2, attrs);
38        assert(err == do_some_success);
39
40        // ...
41
42        return 0;
43}

This saves us having to specify the kind with every entry and moves it instead to the initialization of the attribute itself. It also makes it so rather than taking a pointer to literally anything, it is instead focused on a narrow and specific set of values. Safety can further be increased with a set of creation functions that can’t make the mistake of providing the wrong attr_kind and hardocding it in. (C++ would solve this problem by baking it into the type’s constructor.) Because of the rule about the first element of a struct having an identical address as the whole structure, the following becomes the proper way to cast from each attr_kind to its respective attribute type:

 1int do_something (size_t n, attr_kind* attrs[]) {
 2        for (size_t i = 0; i < n; ++i) {
 3                attr_kind* attr_tag = attrs[i];
 4                if (!attr_tag) {
 5                        continue;
 6                }
 7                switch (*attr_tag) {
 8                        case attr_kind_a: {
 9                                attr_a* attr = (attr_a*)(void*)attr_tag;
10                                // use attr-> to get information about an attribute a
11                        } break;
12                        case attr_kind_b: {
13                                attr_b* attr = (attr_b*)(void*)attr_tag;
14                                // use attr-> to get information about an attribute b
15                        } break;
16                        default:
17                                break;
18                }
19        }
20
21        // ...
22        return do_some_success;
23}

This forms the basis for ztdc_thrd_create_attrs(). Unknown attributes that are not recognized by do_something (i.e., by ztdc_thrd_create_attrs()) are simply passed over. The others are processed in the function and the implementation can react to them. This allows for implementations to accept more structures in the future, so long as a new tag can be defined. The enumeration’s underlying type is int right now, and so on typical implementations this can hold about 2 billion values (if they’re all positive values). There’s no way either the standard or implementations will ever fill all 2 billion of those values and have a structure to go along with it.

“What if an attribute is unrecognized / errors?”

The second problem arises after we finally get an interface that allows for implementation-defined and standard-defined thread attributes: what happens if an implementation cannot honor an attribute? There’s 2 ways that such failures happen:

  • the thread attribute is provided by the user by the implementation does not recognize it;

  • and, the thread attribute is provided by the user, the implementation recognizes it, but it cannot honor it for one reason or another.

This is surprisingly more common than one would believe for things like thread attributes. To give a more practical example, ztdc_thrd_attr_stack_size on Win32 Thread-based platforms with a too-small size request will just round the size up to the minimum size. But a too-small request on a POSIX thread-based platform will actually error and ignore the request with that specific stack size. The difference here matters because different platforms have wildly different implementation strategies, and it is impossible to provide guaranteed minimums to a unified interface in a way that’s helpful.

Therefore, the more user-friendly thing to do is to allow a user to know which attributes are not honored by the implementation. This means that our do_something function from before needs to take a function (and a userdata parameter, as is typical for user-controlled APIs). So, let’s add that to the interface:

 1#include <stddef.h>
 2#include <assert.h>
 3#include <stdio.h>
 4
 5typedef enum attr_kind {
 6        attr_kind_a,
 7        attr_kind_b,
 8} attr_kind;
 9
10struct attr_a {
11        attr_kind kind;
12        size_t stack_size;
13};
14
15struct attr_b {
16        attr_kind kind;
17        size_t guard_pages;
18};
19
20enum {
21        do_some_success,
22        do_some_fail,
23        do_some_busy,
24        do_some_memory,
25        do_some_on_fire,
26};
27
28typedef int(do_some_err_func_t)(attr_kind* attr, int err, void* userdata);
29
30int do_something (size_t n, attr_kind* attrs[]);
31int do_something_err (size_t n, attr_kind* attrs[],
32        do_some_err_func_t* func_err, void* func_err_userdata);
33
34int check_attr(attr_kind* attr, int err, void* func_err_userdata) {
35        if (*attr == attr_kind_a) {
36                fprintf(stderr, "stack size cannot be honored, "
37                        "but we will continue anyways: %zu",
38                        ((attr_a*)attr)->stack_size);
39                return err;
40
41        }
42        if (*attr == attr_kind_b) {
43                fprintf(stderr, "guard page size cannot be honored, "
44                        "but we will continue anyways: %zu",
45                        ((attr_b*)attr)->guard_pages);
46                return do_some_success;
47        }
48        fprintf(stderr, "unknown attribute: %d "
49                "(we will continue anyways)", (unsigned int)*attr);
50        return do_some_success;
51}
52
53int main () {
54        attr_a a = { attr_kind_a, 0 };
55        attr_b b = { attr_kind_b, 0 };
56        typedef struct attr_c = {
57                attr_kind kind;
58                const char* name;
59        } attr_c;
60        attr_c c = { 359503, "teehee" };
61        attr_kind* attrs[] = {
62                { &a.kind },
63                { &b.kind },
64                { &c.kind },
65        };
66
67        int err = do_something_err(2, attrs, check_attr, NULL);
68        assert(err == do_some_success);
69
70        // ...
71
72        return 0;
73}

Here, we have a check_attr function that gives us the appropriate ability to inspect and check the values passed to the function. We can report errors as we please, without needing to inspect the internals of the do_something implementation. The implementation ultimately controls what it does and does not know about, however:

 1#include <stddef.h>
 2
 3typedef struct secret_attr {
 4        attr_kind kind;
 5        const char* name;
 6        size_t size;
 7} secret_attr;
 8
 9int do_something_err(size_t n, attr_kind* attrs[],
10        do_some_err_func_t* func_err, void* func_err_userdata)
11{
12        for (size_t i = 0; i < n; ++i) {
13                attr_kind* attr_tag = attrs[i];
14                if (!attr_tag) {
15                        continue;
16                }
17                switch (*attr_tag) {
18                        case attr_kind_a: {
19                                attr_a* attr = (attr_a*)(void*)attr_tag;
20                                // we can handle this one, no need to call the error!
21                                // ...
22                        } break;
23                        case attr_kind_b: {
24                                attr_b* attr = (attr_b*)(void*)attr_tag;
25                                // we don't know what to do for this one, etc. etc.
26                                // ...
27                                int attr_err = func_err(attr_kind,
28                                                        do_some_fail,
29                                                        func_err_userdata);
30                                if (attr_err != do_some_success) {
31                                        // do not proceed: leave
32                                        return attr_err;
33                                }
34                        } break;
35                        case 0x10000: {
36                                secret_attr* attr = (secret_attr*)(void*)attr_tag;
37                                // secret implementation-defined attribute this
38                                // specific implementation knows about
39                                // ...
40                        } break;
41                        default: {
42                                // we do not recognize the attribute at ALL
43                                int attr_err = func_err(attr_kind,
44                                                        do_some_fail,
45                                                        func_err_userdata);
46                                if (attr_err != do_some_success) {
47                                        // do not proceed: leave
48                                        return attr_err;
49                                }
50                        } break;
51                }
52        }
53
54        // ...
55        return do_some_success;
56}

If the func_err is called and it returns something other than do_some_success, then we know that the implementation could not process the attribute. This allows the implementation to control what it wants to handle, but also lets the user report / crash / etc. on any kind of failure to handle an attribute. This is the driving force behind ztdc_thrd_create_attrs_err(), and forms the core of the additions to this API.

Achieving the Goal

There are some additional constraints and workarounds for specific implementation shenanigans and the fact that we’re not just writing purely synchronous code here. But this is the core principles behind how ztdc_thrd_create_attrs_err and how it’s structured works. At any point, an implementation can add a new secret_attr that it wants to work with, and it won’t disturb the other structures or older implementations. And, older implementations that are called with newer attributes will simply just report them to the user.

By default, do_something (and its analogous ztdc_thrd_create_attrs()) take the approach of using a function that simply returns do_some_success for an error as by-default one wants to ignore any weird implementation quirks:

 1int default_do_something_err(attr_kind* attr, int err, void* userdata) {
 2        return do_some_success;
 3}
 4
 5int do_something(size_t n, attr_kind* attrs[],
 6        do_some_err_func_t* func_err, void* func_err_userdata)
 7{
 8        return do_something_err(n, attrs, default_do_something_err, NULL);
 9}
10
11int do_something_err(size_t n, attr_kind* attrs[],
12        do_some_err_func_t* func_err, void* func_err_userdata)
13{
14        // implementation from above...
15        // ...
16}

This is the full core of the design of ztd.thread.