Skip to content

Commit a414c93

Browse files
[CDRIVER-5859] [CDRIVER-5893] Add ckdint arithmetic backports (#1866)
* Our very own ckdint backport * MSVC & C++ compat - Don't use designated-initializers in headers. - Don't use compound-init syntax (used brace init when in C++ mode). - Spelling error around typeof detection - Need parens for some MSVC preproc bugs - Spelling errors in macros * Only use -fuse-lld if we are compiling (linking) as C * Add basic test coverage of ckdint for all platforms The C++ test case only compiles on some platforms, and requires GCC builtins. This adds test cases with known values that check the basic API for all platforms.
1 parent 309fa62 commit a414c93

File tree

9 files changed

+1360
-15
lines changed

9 files changed

+1360
-15
lines changed

build/cmake/LLDLinker.cmake

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,7 @@ endif ()
5656

5757
if (MONGO_USE_LLD)
5858
message (STATUS "Linking using LLVM lld. Disable by setting MONGO_USE_LLD to OFF")
59-
add_link_options (-fuse-ld=lld)
59+
# We only tested with C, so only use LLD on C (some platforms don't support lld with g++)
60+
# XXX: This should use $<LINK_LANGUAGE:C> when we can require CMake 3.18+
61+
add_link_options ($<$<COMPILE_LANGUAGE:C>:-fuse-ld=lld>)
6062
endif ()

src/common/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,20 @@ configure_file (
99
"${CMAKE_CURRENT_SOURCE_DIR}/src/common-config.h.in"
1010
"${CMAKE_CURRENT_BINARY_DIR}/src/common-config.h"
1111
)
12+
13+
add_library(mongo-mlib INTERFACE)
14+
add_library(mongo::mlib ALIAS mongo-mlib)
15+
target_include_directories(mongo-mlib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/src>)
16+
set_property(TARGET mongo-mlib PROPERTY EXPORT_NAME mongo::mlib)
17+
18+
if(CMAKE_CXX_COMPILER)
19+
add_executable(mlib-ckdint-test src/mlib/ckdint.test.cpp)
20+
set_target_properties(mlib-ckdint-test PROPERTIES
21+
COMPILE_FEATURES cxx_std_11
22+
LINK_LIBRARIES mongo::mlib
23+
# Enable -fPIC, required for some build configurations
24+
POSITION_INDEPENDENT_CODE TRUE
25+
)
26+
add_test(NAME mongoc/mlib/ckdint COMMAND mlib-ckdint-test)
27+
set_property(TEST mongoc/mlib/ckdint PROPERTY SKIP_REGULAR_EXPRESSION "@@ctest-skipped@@")
28+
endif()

src/common/src/mlib/ckdint.h

Lines changed: 679 additions & 0 deletions
Large diffs are not rendered by default.

src/common/src/mlib/ckdint.md

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# `mlib/ckdint.h` C23 `stdckdint.h` for C99
2+
3+
The `mlib/ckdint.h` header implements the [C23 checked arithmetic][stdckdint]
4+
functionality in a C99-compatible manner. There are only three caveats to keep
5+
in mind:
6+
7+
[stdckdint]: https://en.cppreference.com/w/c/numeric#Checked_integer_arithmetic
8+
9+
- The backport relies assumes two's complement signed integer encoding.
10+
- The operand expressions of a call to the ckdint function-like macros may be
11+
evaluated more than once.
12+
- The output parameter is read-from before the operation begins, meaning that it
13+
must be initialized to some value to prevent an uninitialized-read from being
14+
seen by the compiler. (The value isn't used, and the compiler will easily
15+
elide the dead store/read.)
16+
17+
Implementing this correctly, especially without the aide of `_Generic`, requires
18+
quite a few tricks, but the results are correct (tested against GCC's
19+
`__builtin_<op>_overflow` intrinsic, which is how `glibc` implements C23
20+
[stdckdint][]).
21+
22+
23+
# How to Use
24+
25+
The following function-like macros are defined:
26+
27+
- Regular: `mlib_add`, `mlib_sub`, and `mlib_mul`, with an additional
28+
`mlib_narrow` macro.
29+
- Asserting: `mlib_assert_add`, `mlib_assert_sub`, `mlib_assert_mul`, and
30+
`mlib_assert_narrow`
31+
32+
33+
## Regular Macros
34+
35+
The "regular" macros have the same API as [stdckdint][], with an additional
36+
feature: If called with two arguments, the output parameter is used as the
37+
left-hand operand of the operation:
38+
39+
```c
40+
int foo = 42;
41+
mlib_add(&foo, n); // Equivalent to `foo += n`
42+
```
43+
44+
`mlib_narrow(Dst, I)` is not from [stdckdint][], but is useful to check that an
45+
integral cast operation does not modify the value:
46+
47+
```c
48+
void foo(size_t N) {
49+
int a = 0;
50+
if (mlib_narrow(&a, N)) {
51+
fprintf(stderr, "Invalid operand: N is too large\n");
52+
abort();
53+
}
54+
// …
55+
}
56+
```
57+
58+
All of the "regular" macros return a boolean. If they return `true`, then the
59+
result written to the destination DOES NOT represent the true arithmetic result
60+
(i.e. the operation overflowed or narrowed). If it returns `false`, then the
61+
operation succeeded without issue.
62+
63+
This allows one to chain arithmetic operations together with the logical-or
64+
operator, short-circuiting when the operation fails:
65+
66+
```c
67+
ssize_t grow_size(size_t sz, size_t elem_size, size_t count) {
68+
ssize_t ret = 0;
69+
// Compute: ret = sz + (elem_size × count)
70+
if (mlib_mul(&elem_size, count) || // elem_size *= count
71+
mlib_add(&ret, sz, elem_size)) { // ret = sz + elem_size
72+
// Overflow. Indicate an error.
73+
return SSIZE_MIN;
74+
}
75+
return ret;
76+
}
77+
```
78+
79+
80+
## Asserting Macros
81+
82+
The `mlib_assert_…` macros take a type as their first argument instead of a
83+
destination pointer. The macro yields the result of the operation as a value of
84+
the specified type, asserting at runtime that no overflow or narrowing occurs.
85+
If the operation results in information loss, the program terminates at the call
86+
site.
87+
88+
89+
# How it Works
90+
91+
This section details how it works, since it isn't straightforward from reading.
92+
93+
94+
## Max-Precision Arithmetic
95+
96+
The basis of the checked arithmetic is to do the math in the maximum width
97+
unsigned integer type, which is well-defined. We can then treat the unsigned bit
98+
pattern as a signed or unsigned integer as appropriate to perform the arithmetic
99+
correctly and check for overflow. This arithmetic is implemented in the
100+
`mlib_add`, `mlib_sub`, and `mlib_mul` *functions* (not the macros). The bit
101+
fiddling tricks are a combination of straightforward arithmetic checks and more
102+
esoteric algorithms. The checks for addition and subtraction are fairly
103+
straightforward, while the multiplication implementation is substantially more
104+
complicated since its overflow semantics are much more pernicious.
105+
106+
The bit hacks are described within each function. They are split between each
107+
combination of signed/unsigned treatment for the dest/left/right operands. The
108+
basis of the bit checks is in treating the high bit as a special boolean: For
109+
unsigned types, a set high bit represents a value outside the bounds of the
110+
signed equivalent. For signed types, a set high bit indicates a negative value
111+
that cannot be stored in an unsigned integer. Thus, logical-bit operations on
112+
integers and then comparing the result as less-than-zero effectively treats the
113+
high bit as a boolean, e.g.:
114+
115+
- For signed X and Y, `(X ^ Y) < 0` yield `true` iff `X` and `Y` have different
116+
sign.
117+
- `(X & Y) < 0` tests that both X and Y are negative.
118+
- `(X | Y) < 0` tests that either X or Y are negative.
119+
120+
The very terse bit-manipulation expressions are difficult to parse at first, and
121+
have been expanded below each occurrence to explain what they are actually
122+
testing. The terse bit-manip tests are left as the main condition for overflow
123+
checking, as they generate significantly better machine code, even with the
124+
optimizer enabled.
125+
126+
If the arithmetic overflows in the max precision integer, then we can assume
127+
that it overflows for any smaller integer types.
128+
129+
For this integer promotion at macro sites, we use `mlib_upscale_integer`,
130+
defined in `mlib/intutil.h`.
131+
132+
133+
## Final Narrowing
134+
135+
While it is simple enough to perform arithmetic in the max precision, we need
136+
to narrow the result to the target type, and that requires knowing the min/max
137+
bounds of that type. This was the most difficult challenge, because it requires
138+
the following:
139+
140+
1. Given a pointer to an integer type $T$, what is the minimum value of $T$?
141+
2. ... what is the maximum value of $T$?
142+
3. How do we cast from a `uintmax_t` to $T$ through a generic `void*`?
143+
144+
Point (3) is fairly simple: If we know the byte-size of $T$, we can bit-copy the
145+
integer representation from `uintmax_t` into the `void*`, preserving
146+
endian-encoding. For little-endian encoding, this is as simple as copying the first
147+
$N$ bytes from the `uintmax_t` into the target, truncating to the target size.
148+
For big-endian encoding, we just adjust a pointer into the object representation
149+
of the `uintmax_t` to drop the high bytes that we don't need.
150+
151+
Points (1) and (2) are more subtle. We need a way to obtain a bit pattern that
152+
respects the "min" and "max" two's complement values. While one can easily form
153+
an aribtrary bit pattern using bit-shifts and `sizeof(*ptr)`, the trouble is
154+
that the min/max values depend on whether the target is signed, and it is *not
155+
possible* in C99 to ask whether an arbitrary integer expression is
156+
signed/unsigned.
157+
158+
159+
### Things that Don't Work™
160+
161+
Given a type `T`, we can check whether it is signed with a simple macro:
162+
163+
```c
164+
#define IS_SIGNED(T) ((T)-1 < 0)
165+
```
166+
167+
Unfortunately, we don't have a type `T`. We have an expression `V`:
168+
169+
```c
170+
#define IS_SIGNED_TYPEOF(V) ???
171+
```
172+
173+
With C23 or GNU's `__typeof__`, we could do this easily (see below).
174+
175+
There is one close call, that allows us to grab a zero and subtract one:
176+
177+
```c
178+
#define IS_SIGNED_TYPEOF(V) ((0 & V) - 1 < 0)
179+
```
180+
181+
This seems promising, but this **doesn't work**, because of C's awful,
182+
horrible, no-good, very-bad integer promotion rules. The expression `0 & V`
183+
*will* yield zero, but if `V` is smaller than `int`, it will be immediately
184+
promoted to `signed int` beforehand, regardless of the sign of `V`. This macro
185+
gives the correct answer for `(unsigned) int` and larger, but `(unsigned) short`
186+
and `(unsigned) char` will always yield `true`.
187+
188+
189+
### How `mlib/ckdint.h` Does It
190+
191+
There is one set of C operators that *don't* perform integer promotion:
192+
assignment and in-place arithmetic. This macro *does* work:
193+
194+
```c
195+
#define IS_SIGNED_TYPEOF(V) (((V) = -1) < 0)
196+
```
197+
198+
But this obviously can't be used, because we're modifying the operand! Right...?
199+
200+
Except: We're only needing to do this check on the *destination* of the
201+
arithmetic function. We already know that it's modifiable and that we're going
202+
to reassign to it, so it doesn't matter that we temporarily write a garbage `-1`
203+
into it!
204+
205+
With this, we can write our needed support macros:
206+
207+
```c
208+
#define MINOF_TYPEOF(V) \
209+
IS_SIGNED_TYPEOF(V) \
210+
? MIN_TYPEOF_SIGNED(V) \
211+
: MIN_TYPEOF_UNSIGNED(V)
212+
```
213+
214+
With this, a call-site of our checked arithmetic macros can inject the
215+
appropriate min/max values of the destination operand, and the checked
216+
arithmetic functions can do the final bounds check.
217+
218+
Almost
219+
220+
221+
### Big Problem, Though
222+
223+
Suppose the following:
224+
225+
```c
226+
int a = 42;
227+
mlib_add(&a, a, 5); // 42 + 5 ?
228+
```
229+
230+
The correct result of `a` is `47`, but the value is unspecified: It is either
231+
`4` or `47`, depending on argument evaluation order, because we are silently
232+
overwriting the value in `a` to `-1` before doing the operation. We need to save
233+
the value of `a`, do the check, and then restore the value of `a`, all in a
234+
single go. Thus we have a much hairier macro:
235+
236+
```c
237+
static thread_local uintmax_t P;
238+
static thread_local bool S;
239+
#define IS_SIGNED_TYPEOF(V) \
240+
(( \
241+
P = 0ull | (uintmax_t) V, \
242+
V = 0, \
243+
S = (--V < 0), \
244+
V = 0, \
245+
V |= P, \
246+
S \
247+
))
248+
```
249+
250+
This uses the comma-operator the enforce evaluation of each sub-expression:
251+
252+
1. Save the bit pattern of `V` in a global static temporary $P$.
253+
2. Set `V` to zero.
254+
3. Decrement `V` and check if the result is negative. Save that value in a
255+
separate global $S$.
256+
4. Restore the value of `V` by writing the bit pattern stored in $P$ back into
257+
`V`. (The use of `= 0` + `|= P` prevents compilers from emitting any
258+
integer conversion warnings)
259+
5. Yield the bool we saved in $S$.
260+
261+
$P$ and $S$ are `thread_local` to allow multiple threads to evaluate the macro
262+
simultaneously without interfering. The `static` allows the optimizer to delete
263+
$P$ and $S$ from the translation unit when it can statically determine that the
264+
values written into these variables are never read from after constant folding
265+
(usually: MSVC is currently unable to elide the writes, but is still able to
266+
constant-fold across these assignments, which is the most important optimization
267+
we need to ensure works to eliminate redundant branches after inlining).
268+
269+
With this modified roundabout definition, we can perform in-place checked
270+
arithmetic where the output can also be used as an input of the operation.
271+
272+
273+
### Optimize: Use `__typeof__`
274+
275+
If we have `__typeof__` (available in GCC, Clang, and MSVC 19.39+) or C23
276+
`typeof` , we can simplify our macro to a trivial one:
277+
278+
```c
279+
#define IS_SIGNED_TYPEOF(V) IS_SIGNED(__typeof__(V))
280+
```
281+
282+
This will yield an equivalent result, but improves debug codegen and gives the
283+
optimizer an easier time doing constant folding across function calls.

0 commit comments

Comments
 (0)