March 8, 2018·4 min read·engineeringcclangdiagnosticsabi

Clang 6 diagnoses packing state that escapes a header

Clang 6 adds warnings for unbalanced packing directives and includes affected by earlier packing state. The release makes hidden compilation context easier to identify in C interfaces.

A header can affect declarations that appear after it has finished. Packing directives are one way that happens, because their state can survive the return to the including file. The Clang 6.0.0 release notes describe new diagnostics for this problem. The release, dated March 8 in the LLVM release index, makes the relationship between header boundaries and packing state more visible.

The new -Wpragma-pack warning covers missing terminating #pragma pack(pop) directives. It also warns when an included file leaves the current alignment different from the value in effect before inclusion. A second warning, -Wpragma-pack-suspicious-include, addresses the reverse direction. It can identify structures or unions in a header that an earlier packing directive affects. That warning is disabled by default.

C's role in calling conventions and structure layout is a large part of why it matters to me. A declaration used at a language boundary needs a stable interpretation. Packing state is relevant because a reader can see the declaration without seeing the directive that affects it. These diagnostics provide information about that otherwise distant dependency.

The include boundary does not restore packing state

Consider a header that deliberately changes packing while declaring a record. A paired push and pop expresses the intended extent of that change:

/* record.h */
#pragma pack(push, 1)
struct record {
    char kind;
    int value;
};
#pragma pack(pop)

This example uses the stack operations exercised in Clang's release-branch packing tests. The push saves the preceding state and the pop restores it. The example deliberately makes no claim about the record's total size, since that would also depend on the target's type representation. Its purpose is to show where the packing change begins and ends.

Removing the final pop changes the relationship between the header and its caller. The header leaves a changed packing state behind, so subsequent declarations can depend on whether it has already been included. Clang 6 can diagnose the changed alignment at the include boundary. It can also detect an unterminated push when the translation unit ends.

Those checks identify related but different mistakes. An include-boundary diagnostic identifies a file whose processing changed the active alignment. An end-of-file diagnostic identifies saved packing state that the translation unit never restored. The first points at the boundary across which the change escaped. The second catches an imbalance even where no particular included file explains it.

The packing test contains a useful detail about restoration. It expects a diagnostic note suggesting #pragma pack(pop) when an unmatched push is followed by #pragma pack(). Resetting to the target's default state does not express the same operation as restoring the saved state. An outer caller may itself have selected a non-default setting before entering the header.

The opposite direction needs its own warning

A header can balance its own directives and still be affected by the state inherited from its caller. That is the case covered by -Wpragma-pack-suspicious-include. The release notes describe a non-default packing alignment established before an include, with structures or unions in the included file affected by that alignment.

The distinction matters when locating responsibility. A header that changes the active alignment has an observable effect on its caller. A header processed under an inherited alignment may leave that state unchanged. Checking only the value on entry and exit cannot identify the latter case, because the two values may agree throughout.

The optional warning therefore asks a different question about the same compilation process. It looks for declarations whose interpretation depends on an earlier directive outside the file. Enabling it can make an intended use of inherited packing visible as well as an accidental one. Its disabled-by-default status should remain part of any description of the release, because upgrading Clang alone does not imply that this additional diagnostic is active.

The practical value is in locating the hidden input to a declaration. A record definition and its surrounding packing context jointly determine what the compiler sees. A warning at the include helps connect those pieces without requiring the programmer to reconstruct every earlier directive manually.

Other diagnostics tighten integer and pointer reasoning

The release also adjusts C diagnostics to use the underlying type of an enumeration. The affected warnings include -Wsign-compare and -Wsign-conversion, together with the tautological constant comparison warnings. This makes the chosen integer representation relevant to the diagnostic decision instead of treating the enumeration's spelling as sufficient information.

Clang 6 expands -Wtautological-compare for unsigned comparisons with zero, including cases where a signed integer converts to unsigned for the comparison. The release notes distinguish that warning from -Wsign-compare, which is adjusted to avoid reporting that same case. The division matters because the resulting comparison can be constant even though the source expression contains a signed variable.

Pointer arithmetic receives a related diagnostic improvement. -Wnull-pointer-arithmetic now warns about arithmetic on a null pointer and about the GNU-extension case where the pointer is treated as a cast from an integer. The notes specifically identify nonzero offsets on a null pointer as undefined behavior. The warning concerns the operation's validity, independently of whether the resulting address is later dereferenced.

I find these changes useful because they expose the assumptions behind compact expressions. In C, a short spelling can conceal an integer conversion or inherited compilation state. A diagnostic that names that mechanism gives the reader a more specific starting point than a general warning about portability.

C17 mode is a separate compatibility change

Clang 6 also adds -std=c17, -std=gnu17 and -std=iso9899:2017. The release notes describe the next C revision as a defect-correction release expected to be published in 2018. They state that the only difference between Clang's C17 and C11 modes is the value of __STDC_VERSION__.

That mode change should not be confused with the diagnostic work. The packing warnings and conversion diagnostics improve what the compiler can report about existing source. Selecting C17 identifies the requested language revision. For this release, the useful operational distinction is between choosing that revision and enabling the particular warning that inspects packing inherited across an include.