Core Design
The design of this library is centered around 4 things:
the
ztdc_thrd_attr_kindenumeration having appropriate values that map to a specific structure;the
ztdc_thrd_attr_*structures (and theztdc_thrd_attr__* implementation-defined structures) for each enumerator in theztdc_thrd_attr_kindenumeration;the
ztdc_thrd_create_attrs()andztdc_thrd_create_attrs_err()functions allowing an implementation to set data before or immediately after thread creation;and, the
ztdc_thrd_attr_err_funcerror function type used inztdc_thrd_create_attrs_err()to allow for checking and either passing over or denying an attribute.
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?
“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.