Skip to content

Emit string and byte-string constants (#927, #546)#1171

Open
cf-rhett wants to merge 1 commit into
mozilla:mainfrom
cf-rhett:feature/const-str-cstr
Open

Emit string and byte-string constants (#927, #546)#1171
cf-rhett wants to merge 1 commit into
mozilla:mainfrom
cf-rhett:feature/const-str-cstr

Conversation

@cf-rhett

@cf-rhett cf-rhett commented Jul 10, 2026

Copy link
Copy Markdown

Closes #927 and closes #546.

&str, &CStr, and byte-string (b"...") constants were previously dropped silently. This emits them:

  • &str and &CStr constants become C string literals — #define, constexpr, or static const char * depending on config. Arrays of them ([&CStr; N]) work too.
  • Byte-string constants (b"...") become uint8_t[] array initializers rather than string literals, so non-printable and non-NUL-terminated data survives (per the guidance in Support for constant byte literal #546). The declared type may be the unsized &[u8], which cbindgen can't load, so the array length is taken from the literal. An empty byte string yields the same zero-length array as any other empty array constant — it is not special-cased.

Design notes (to save reviewer time)

  • Why const char * for &str/&CStr. These are fat/thin pointers, so const char * is not ABI-compatible with the Rust type. But a Rust const has no symbol or address — it is inlined at every use site — so there is nothing to link against and no ABI to preserve. What a C consumer of the constant wants is the string value, which is what we emit.
  • Where the type rewrite lives. The &str/&CStrconst char * rewrite is confined to Constant::load, not the global type-simplification pass. Collapsing these types is only sound for a constant definition, where the runtime representation is discarded in favor of the literal; it must never apply to struct fields, function arguments, or returns.
  • Byte strings are handled one level up. Literal::load sees only the expression, so it can produce a self-describing string literal but cannot synthesize the uint8_t[N] type a byte string needs — that requires the length, which only Constant::load reasons about. Nested byte strings remain unsupported (same silent-skip as before).
  • Escaping. The C string escaper renders non-ASCII UTF-8 bytes as \xNN (not Rust's \u{...}, which is invalid C), escapes ? to prevent trigraph formation, and splits a literal when a \xNN escape is followed by a hex-digit character ("...\xNN" "f...") so the escape stays a single byte.

Tests

New fixtures cover string edge cases (empty, interior NUL, non-UTF-8 &CStr, hex-digit adjacency in both cases, control/DEL bytes, trigraphs, arrays), byte strings (sized, unsized slice, binary, empty), and the allow_static_const/allow_constexpr off fallback path. Several existing constant/assoc_constant/namespace* expectations gained the &str constants they previously dropped.

@cf-rhett

Copy link
Copy Markdown
Author

Gentle ping on this @emilio — I think it's waiting on a maintainer to approve the CI run (no checks have kicked off yet). This implements the uint8_t[] approach you outlined in #546 and also closes #927. Happy to rebase or adjust anything. Thanks!

@emilio emilio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sensible. One question about the const char* version of this, and thanks for the ping!

Comment thread src/bindgen/ir/constant.rs Outdated
let ty = Type::load(ty)?;
let mut ty = match ty {
Some(ty) => ty,
let (mut ty, mut lit) = match byte_string_bytes(expr) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit hacky, but fair enough I guess?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was. Pulled it into a single load_byte_string(expr) -> Option<(Type, Literal)> so it reads as "byte string, else the usual typed load." The type and value have to come out together: &[u8; N] loads as a pointer-to-array rather than a bare array, and &[u8] doesn't load at all, so I synthesize the uint8_t[N] from the literal length right next to the initializer. Nice side effect is the sized and unsized forms end up identical.

Comment thread tests/expectations/assoc_constant.cpp Outdated

};
constexpr static const int32_t Foo_GA = 10;
constexpr static const char *Foo_BU = "hello world";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional choice of char pointer intentional out of curiosity? const char Foo_BU[] = might be more useful as it allows you to get the length at compile time more easily, and it's what the actual literal would produce.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed — done. Scalar &str/&CStr now emit const char NAME[]. Compile-time sizeof is the obvious win, and it also stops a &str with an interior NUL from getting silently truncated the way the pointer form did. Arrays of them stay const char *[N] though — ragged lengths can't be char[N][]. C is unaffected, still a bare #define.

`&str` and `&CStr` constants were silently dropped; they are now emitted
as C string literals. A scalar `&str`/`&CStr` becomes `const char NAME[]`
(an unsized array) in the C++/Cython typed forms, so `sizeof` gives the
length at compile time and the declaration mirrors its initializer; in C
it stays a bare `#define`. An array of them (`[&CStr; N]`) instead maps to
`const char *[N]`, since ragged element lengths can't form a `char[N][]`.

Byte-string constants (`b"..."`) are emitted as `uint8_t[]` array
initializers rather than string literals, so non-printable and
non-NUL-terminated data survives. The declared type (`&[u8; N]` or the
unsized `&[u8]`) doesn't load into a bare array, so `load_byte_string`
derives both the type and the initializer from the literal bytes, which
also makes the sized and unsized forms behave identically. An empty byte
string yields the same zero-length array as any other empty array
constant; it is not special-cased.

`&str`/`&CStr` are fat pointers, so their C form is not ABI-compatible
with the Rust type; what is emitted is the string *value*, which is what a
C consumer of these constants wants. The rewrite is confined to constant
definitions and never applied to fields or arguments.

The C string escaper renders non-ASCII UTF-8 bytes as `\xNN` (not Rust's
`\u{...}`, which is invalid C), escapes `?` to prevent trigraph
formation, and splits a literal when a `\xNN` escape is followed by a
hex-digit character so the escape stays one byte.

The three const-dedup checks that suppress a doubled `const` for pointer
constants are consolidated into `write_field_prepends_const`, which also
recurses through arrays so arrays of const pointers dedup correctly.
@cf-rhett
cf-rhett force-pushed the feature/const-str-cstr branch from c1c8c8b to d0d6d28 Compare July 16, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support 'pub const BAR: &CStr = c"hello world";' Support for constant byte literal

2 participants