Skip to content

Commit d3bf7d6

Browse files
cf-rhettemilio
authored andcommitted
Emit string and byte-string constants (#927, #546)
`&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.
1 parent 4acf942 commit d3bf7d6

57 files changed

Lines changed: 629 additions & 16 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# unreleased
22

3+
* Emit `&str`, `&CStr`, and arrays thereof as C string literals, and byte-string
4+
(`b"..."`) constants as `uint8_t[]` arrays, instead of dropping them.
5+
36
# 0.29.4
47

58
* Support constant enums and arrays.

src/bindgen/ir/constant.rs

Lines changed: 155 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use std::borrow::Cow;
66
use std::collections::HashMap;
7+
use std::fmt::Write as _;
78
use std::io::Write;
89

910
use syn::ext::IdentExt;
@@ -13,8 +14,8 @@ use crate::bindgen::config::{Config, Language};
1314
use crate::bindgen::declarationtyperesolver::DeclarationTypeResolver;
1415
use crate::bindgen::dependencies::Dependencies;
1516
use crate::bindgen::ir::{
16-
AnnotationSet, Cfg, ConditionWrite, Documentation, GenericParams, Item, ItemContainer, Path,
17-
Struct, ToCondition, Type,
17+
AnnotationSet, Cfg, ConditionWrite, ConstExpr, Documentation, GenericParams, IntKind, Item,
18+
ItemContainer, Path, PrimitiveType, Struct, ToCondition, Type,
1819
};
1920
use crate::bindgen::language_backend::LanguageBackend;
2021
use crate::bindgen::library::Library;
@@ -29,10 +30,140 @@ fn member_to_ident(member: &syn::Member) -> String {
2930
}
3031
}
3132

33+
/// Format a byte slice as a double-quoted string literal valid in both C and C++.
34+
///
35+
/// Printable ASCII passes through; `"`, `\`, `?`, and the common whitespace
36+
/// controls use their named escapes; every other byte (other control characters
37+
/// and the raw bytes of non-ASCII UTF-8) becomes `\xNN`. Rust's own `{:?}` /
38+
/// `escape_default` is unsuitable here because it renders non-ASCII as `\u{...}`,
39+
/// which is not valid C.
40+
///
41+
/// A `\xNN` escape in C greedily consumes every following hex digit, so when such an
42+
/// escape is followed by a literal hex-digit character the literal is split
43+
/// (`"...\xNN" "f..."`); adjacent string literals concatenate, keeping the escape a
44+
/// single byte.
45+
fn to_c_string_literal(bytes: &[u8]) -> String {
46+
let mut out = String::with_capacity(bytes.len() * 4 + 2);
47+
out.push('"');
48+
let mut prev_was_hex_escape = false;
49+
for &byte in bytes {
50+
match byte {
51+
b'"' => out.push_str("\\\""),
52+
b'\\' => out.push_str("\\\\"),
53+
b'\n' => out.push_str("\\n"),
54+
b'\r' => out.push_str("\\r"),
55+
b'\t' => out.push_str("\\t"),
56+
// Escape `?` so no `??x` trigraph can form (trigraphs are translated
57+
// even inside string literals in C before C23 / C++ before C++17).
58+
b'?' => out.push_str("\\?"),
59+
0x20..=0x7e => {
60+
if prev_was_hex_escape && byte.is_ascii_hexdigit() {
61+
out.push_str("\" \"");
62+
}
63+
out.push(byte as char);
64+
}
65+
_ => {
66+
let _ = write!(out, "\\x{byte:02x}");
67+
prev_was_hex_escape = true;
68+
continue;
69+
}
70+
}
71+
prev_was_hex_escape = false;
72+
}
73+
out.push('"');
74+
out
75+
}
76+
77+
/// Rewrite the Rust standard string reference types (`&str`, `&CStr`) a string
78+
/// constant carries into the C `char` pointer they are exposed as.
79+
///
80+
/// Whether `ty` is the `str` / `CStr` path a `&str` / `&CStr` points to. Neither
81+
/// is a real C type, so a constant of that pointee is lowered to `char`.
82+
fn is_string_path(ty: &Type) -> bool {
83+
matches!(ty, Type::Path(path) if path.generics().is_empty() && matches!(path.name(), "str" | "CStr"))
84+
}
85+
86+
/// Rewrites a `&str` / `&CStr` constant's type into its C form.
87+
///
88+
/// A scalar `&str` / `&CStr` becomes `char[]` -- an array, not a pointer -- so
89+
/// `sizeof` yields the length at compile time and the declaration mirrors its
90+
/// string-literal initializer. Inside an array each element instead stays a
91+
/// `char *`: a `[&CStr; N]` maps to `const char *[N]`, since ragged element
92+
/// lengths can't form a `char[N][]`.
93+
///
94+
/// In C the constant emits as a bare `#define`, so the type is unused; this only
95+
/// shapes the C++ typed-constant and Cython declarations.
96+
fn rewrite_string_reference_type(ty: &mut Type) {
97+
if matches!(ty, Type::Ptr { ty, .. } if is_string_path(ty)) {
98+
*ty = Type::Array(
99+
Box::new(Type::Primitive(PrimitiveType::Char)),
100+
ConstExpr::Value(String::new()),
101+
);
102+
} else if let Type::Array(inner, _) = ty {
103+
reduce_string_reference_pointee(inner);
104+
}
105+
}
106+
107+
/// Reduces the `str` / `CStr` pointee of a `&str` / `&CStr` array element to
108+
/// `char`, keeping it a pointer, and recurses through nested arrays.
109+
fn reduce_string_reference_pointee(ty: &mut Type) {
110+
match ty {
111+
Type::Ptr { ty, .. } if is_string_path(ty) => {
112+
**ty = Type::Primitive(PrimitiveType::Char);
113+
}
114+
Type::Ptr { ty, .. } | Type::Array(ty, _) => reduce_string_reference_pointee(ty),
115+
_ => {}
116+
}
117+
}
118+
119+
/// Whether `write_field` will itself render a leading `const` for `ty`, so the
120+
/// constant writers must not prepend a second one. True for a const pointer and
121+
/// for an array whose element is (recursively) a const pointer.
122+
fn write_field_prepends_const(ty: &Type) -> bool {
123+
match ty {
124+
Type::Ptr { is_const, .. } => *is_const,
125+
Type::Array(inner, _) => write_field_prepends_const(inner),
126+
_ => false,
127+
}
128+
}
129+
130+
/// Loads a byte-string literal (`b"..."`) as a `uint8_t[N]` constant.
131+
///
132+
/// Returns the constant's type and initializer, or `None` if `expr` is not a
133+
/// byte string. A byte string is exposed as a `uint8_t` array rather than a C
134+
/// string literal so that non-printable and non-NUL-terminated data survives (a
135+
/// `b"..."` is not NUL-terminated and may contain interior NULs). Its declared
136+
/// type (`&[u8; N]` or the unsized `&[u8]`) does not load into a bare array, so
137+
/// both the type and the initializer are derived together from the literal
138+
/// bytes -- which also makes the sized and unsized forms behave identically.
139+
fn load_byte_string(expr: &syn::Expr) -> Option<(Type, Literal)> {
140+
let bytes = match expr {
141+
syn::Expr::Lit(syn::ExprLit {
142+
lit: syn::Lit::ByteStr(value),
143+
..
144+
}) => value.value(),
145+
_ => return None,
146+
};
147+
148+
let ty = Type::Array(
149+
Box::new(Type::Primitive(PrimitiveType::Integer {
150+
zeroable: true,
151+
signed: false,
152+
kind: IntKind::B8,
153+
})),
154+
ConstExpr::Value(bytes.len().to_string()),
155+
);
156+
let lit = Literal::Array {
157+
items: bytes
158+
.iter()
159+
.map(|byte| Literal::Expr(byte.to_string()))
160+
.collect(),
161+
};
162+
Some((ty, lit))
163+
}
164+
32165
// TODO: Maybe add support to more std associated constants.
33166
pub(crate) fn to_known_assoc_constant(associated_to: &Path, name: &str) -> Option<String> {
34-
use crate::bindgen::ir::{IntKind, PrimitiveType};
35-
36167
if name != "MAX" && name != "MIN" {
37168
return None;
38169
}
@@ -413,7 +544,15 @@ impl Literal {
413544
Ok(Literal::Expr(value.base10_digits().to_string()))
414545
}
415546
syn::Lit::Bool(ref value) => Ok(Literal::Expr(format!("{}", value.value))),
416-
// TODO: Add support for byte string and Verbatim
547+
syn::Lit::Str(ref value) => {
548+
Ok(Literal::Expr(to_c_string_literal(value.value().as_bytes())))
549+
}
550+
syn::Lit::CStr(ref value) => {
551+
Ok(Literal::Expr(to_c_string_literal(value.value().to_bytes())))
552+
}
553+
// Byte-string literals are handled by `load_byte_string`, which
554+
// synthesizes the array type; nested byte strings stay unsupported.
555+
// TODO: Add support for Verbatim
417556
_ => Err(format!("Unsupported literal expression. {:?}", *lit)),
418557
}
419558
}
@@ -562,16 +701,16 @@ impl Constant {
562701
attrs: &[syn::Attribute],
563702
associated_to: Option<Path>,
564703
) -> Result<Constant, String> {
565-
let ty = Type::load(ty)?;
566-
let mut ty = match ty {
567-
Some(ty) => ty,
704+
let (mut ty, mut lit) = match load_byte_string(expr) {
705+
Some(byte_string) => byte_string,
568706
None => {
569-
return Err("Cannot have a zero sized const definition.".to_owned());
707+
let mut ty = Type::load(ty)?
708+
.ok_or_else(|| "Cannot have a zero sized const definition.".to_owned())?;
709+
rewrite_string_reference_type(&mut ty);
710+
(ty, Literal::load(expr)?)
570711
}
571712
};
572713

573-
let mut lit = Literal::load(expr)?;
574-
575714
if let Some(ref associated_to) = associated_to {
576715
ty.replace_self_with(associated_to);
577716
lit.replace_self_with(associated_to);
@@ -690,7 +829,7 @@ impl Constant {
690829

691830
let condition = self.cfg.to_condition(config);
692831
condition.write_before(config, out);
693-
if let Type::Ptr { is_const: true, .. } = self.ty {
832+
if write_field_prepends_const(&self.ty) {
694833
out.write("static ");
695834
} else {
696835
out.write("static const ");
@@ -790,9 +929,7 @@ impl Constant {
790929
out.write(if in_body { "inline " } else { "static " });
791930
}
792931

793-
if let Type::Ptr { is_const: true, .. } = self.ty {
794-
// Nothing.
795-
} else {
932+
if !write_field_prepends_const(&self.ty) {
796933
out.write("const ");
797934
}
798935
crate::bindgen::cdecl::write_field(language_backend, out, &self.ty, &name, config);
@@ -805,7 +942,9 @@ impl Constant {
805942
language_backend.write_literal(out, value);
806943
}
807944
Language::Cython => {
808-
out.write("const ");
945+
if !write_field_prepends_const(&self.ty) {
946+
out.write("const ");
947+
}
809948
// For extern Cython declarations the initializer is ignored,
810949
// but still useful as documentation, so we write it as a comment.
811950
crate::bindgen::cdecl::write_field(language_backend, out, &self.ty, &name, config);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
root;
3+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
root;
3+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
root;
3+
};

tests/expectations/assoc_constant.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ typedef struct {
77

88
} Foo;
99
#define Foo_GA 10
10+
#define Foo_BU "hello world"
1011
#define Foo_ZO 3.14
1112

1213
void root(Foo x);

tests/expectations/assoc_constant.compat.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ typedef struct {
77

88
} Foo;
99
#define Foo_GA 10
10+
#define Foo_BU "hello world"
1011
#define Foo_ZO 3.14
1112

1213
#ifdef __cplusplus

tests/expectations/assoc_constant.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ struct Foo {
88

99
};
1010
constexpr static const int32_t Foo_GA = 10;
11+
constexpr static const char Foo_BU[] = "hello world";
1112
constexpr static const float Foo_ZO = 3.14;
1213

1314
extern "C" {

tests/expectations/assoc_constant.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ cdef extern from *:
99
ctypedef struct Foo:
1010
pass
1111
const int32_t Foo_GA # = 10
12+
const char Foo_BU[] # = "hello world"
1213
const float Foo_ZO # = 3.14
1314

1415
void root(Foo x);

tests/expectations/assoc_constant_both.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ typedef struct Foo {
77

88
} Foo;
99
#define Foo_GA 10
10+
#define Foo_BU "hello world"
1011
#define Foo_ZO 3.14
1112

1213
void root(struct Foo x);

0 commit comments

Comments
 (0)