From d0d6d2823ed027219b5d663869eea76165caceea Mon Sep 17 00:00:00 2001 From: Rhett Griggs Date: Fri, 10 Jul 2026 16:41:57 -0400 Subject: [PATCH] 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. --- CHANGES | 3 + src/bindgen/ir/constant.rs | 171 ++++++++++++++++-- .../const_byte_string.c.sym | 3 + tests/expectations-symbols/const_str.c.sym | 3 + .../const_str_no_static.c.sym | 3 + tests/expectations/assoc_constant.c | 1 + tests/expectations/assoc_constant.compat.c | 1 + tests/expectations/assoc_constant.cpp | 1 + tests/expectations/assoc_constant.pyx | 1 + tests/expectations/assoc_constant_both.c | 1 + .../expectations/assoc_constant_both.compat.c | 1 + tests/expectations/assoc_constant_tag.c | 1 + .../expectations/assoc_constant_tag.compat.c | 1 + tests/expectations/assoc_constant_tag.pyx | 1 + tests/expectations/const_byte_string.c | 19 ++ tests/expectations/const_byte_string.compat.c | 27 +++ tests/expectations/const_byte_string.cpp | 22 +++ tests/expectations/const_byte_string.pyx | 20 ++ tests/expectations/const_str.c | 41 +++++ tests/expectations/const_str.compat.c | 49 +++++ tests/expectations/const_str.cpp | 44 +++++ tests/expectations/const_str.pyx | 42 +++++ tests/expectations/const_str_no_static.c | 12 ++ .../expectations/const_str_no_static.compat.c | 20 ++ tests/expectations/const_str_no_static.cpp | 17 ++ tests/expectations/const_str_no_static.pyx | 15 ++ tests/expectations/constant.c | 2 + tests/expectations/constant.compat.c | 2 + tests/expectations/constant.cpp | 2 + tests/expectations/constant.pyx | 2 + tests/expectations/constant_both.c | 2 + tests/expectations/constant_both.compat.c | 2 + tests/expectations/constant_tag.c | 2 + tests/expectations/constant_tag.compat.c | 2 + tests/expectations/constant_tag.pyx | 2 + tests/expectations/namespace_constant.c | 2 + .../expectations/namespace_constant.compat.c | 2 + tests/expectations/namespace_constant.cpp | 2 + tests/expectations/namespace_constant.pyx | 2 + tests/expectations/namespace_constant_both.c | 2 + .../namespace_constant_both.compat.c | 2 + tests/expectations/namespace_constant_tag.c | 2 + .../namespace_constant_tag.compat.c | 2 + tests/expectations/namespace_constant_tag.pyx | 2 + tests/expectations/namespaces_constant.c | 2 + .../expectations/namespaces_constant.compat.c | 2 + tests/expectations/namespaces_constant.cpp | 2 + tests/expectations/namespaces_constant.pyx | 2 + tests/expectations/namespaces_constant_both.c | 2 + .../namespaces_constant_both.compat.c | 2 + tests/expectations/namespaces_constant_tag.c | 2 + .../namespaces_constant_tag.compat.c | 2 + .../expectations/namespaces_constant_tag.pyx | 2 + tests/rust/const_byte_string.rs | 18 ++ tests/rust/const_str.rs | 40 ++++ tests/rust/const_str_no_static.rs | 10 + tests/rust/const_str_no_static.toml | 3 + 57 files changed, 629 insertions(+), 16 deletions(-) create mode 100644 tests/expectations-symbols/const_byte_string.c.sym create mode 100644 tests/expectations-symbols/const_str.c.sym create mode 100644 tests/expectations-symbols/const_str_no_static.c.sym create mode 100644 tests/expectations/const_byte_string.c create mode 100644 tests/expectations/const_byte_string.compat.c create mode 100644 tests/expectations/const_byte_string.cpp create mode 100644 tests/expectations/const_byte_string.pyx create mode 100644 tests/expectations/const_str.c create mode 100644 tests/expectations/const_str.compat.c create mode 100644 tests/expectations/const_str.cpp create mode 100644 tests/expectations/const_str.pyx create mode 100644 tests/expectations/const_str_no_static.c create mode 100644 tests/expectations/const_str_no_static.compat.c create mode 100644 tests/expectations/const_str_no_static.cpp create mode 100644 tests/expectations/const_str_no_static.pyx create mode 100644 tests/rust/const_byte_string.rs create mode 100644 tests/rust/const_str.rs create mode 100644 tests/rust/const_str_no_static.rs create mode 100644 tests/rust/const_str_no_static.toml diff --git a/CHANGES b/CHANGES index a8064a8ca..6fd28678c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,8 @@ # unreleased + * Emit `&str`, `&CStr`, and arrays thereof as C string literals, and byte-string + (`b"..."`) constants as `uint8_t[]` arrays, instead of dropping them. + # 0.29.4 * Support constant enums and arrays. diff --git a/src/bindgen/ir/constant.rs b/src/bindgen/ir/constant.rs index f96b2da13..3e0416618 100644 --- a/src/bindgen/ir/constant.rs +++ b/src/bindgen/ir/constant.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use std::collections::HashMap; +use std::fmt::Write as _; use std::io::Write; use syn::ext::IdentExt; @@ -13,8 +14,8 @@ use crate::bindgen::config::{Config, Language}; use crate::bindgen::declarationtyperesolver::DeclarationTypeResolver; use crate::bindgen::dependencies::Dependencies; use crate::bindgen::ir::{ - AnnotationSet, Cfg, ConditionWrite, Documentation, GenericParams, Item, ItemContainer, Path, - Struct, ToCondition, Type, + AnnotationSet, Cfg, ConditionWrite, ConstExpr, Documentation, GenericParams, IntKind, Item, + ItemContainer, Path, PrimitiveType, Struct, ToCondition, Type, }; use crate::bindgen::language_backend::LanguageBackend; use crate::bindgen::library::Library; @@ -29,10 +30,140 @@ fn member_to_ident(member: &syn::Member) -> String { } } +/// Format a byte slice as a double-quoted string literal valid in both C and C++. +/// +/// Printable ASCII passes through; `"`, `\`, `?`, and the common whitespace +/// controls use their named escapes; every other byte (other control characters +/// and the raw bytes of non-ASCII UTF-8) becomes `\xNN`. Rust's own `{:?}` / +/// `escape_default` is unsuitable here because it renders non-ASCII as `\u{...}`, +/// which is not valid C. +/// +/// A `\xNN` escape in C greedily consumes every following hex digit, so when such an +/// escape is followed by a literal hex-digit character the literal is split +/// (`"...\xNN" "f..."`); adjacent string literals concatenate, keeping the escape a +/// single byte. +fn to_c_string_literal(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 4 + 2); + out.push('"'); + let mut prev_was_hex_escape = false; + for &byte in bytes { + match byte { + b'"' => out.push_str("\\\""), + b'\\' => out.push_str("\\\\"), + b'\n' => out.push_str("\\n"), + b'\r' => out.push_str("\\r"), + b'\t' => out.push_str("\\t"), + // Escape `?` so no `??x` trigraph can form (trigraphs are translated + // even inside string literals in C before C23 / C++ before C++17). + b'?' => out.push_str("\\?"), + 0x20..=0x7e => { + if prev_was_hex_escape && byte.is_ascii_hexdigit() { + out.push_str("\" \""); + } + out.push(byte as char); + } + _ => { + let _ = write!(out, "\\x{byte:02x}"); + prev_was_hex_escape = true; + continue; + } + } + prev_was_hex_escape = false; + } + out.push('"'); + out +} + +/// Rewrite the Rust standard string reference types (`&str`, `&CStr`) a string +/// constant carries into the C `char` pointer they are exposed as. +/// +/// Whether `ty` is the `str` / `CStr` path a `&str` / `&CStr` points to. Neither +/// is a real C type, so a constant of that pointee is lowered to `char`. +fn is_string_path(ty: &Type) -> bool { + matches!(ty, Type::Path(path) if path.generics().is_empty() && matches!(path.name(), "str" | "CStr")) +} + +/// Rewrites a `&str` / `&CStr` constant's type into its C form. +/// +/// A scalar `&str` / `&CStr` becomes `char[]` -- an array, not a pointer -- so +/// `sizeof` yields the length at compile time and the declaration mirrors its +/// string-literal initializer. Inside an array each element instead stays a +/// `char *`: a `[&CStr; N]` maps to `const char *[N]`, since ragged element +/// lengths can't form a `char[N][]`. +/// +/// In C the constant emits as a bare `#define`, so the type is unused; this only +/// shapes the C++ typed-constant and Cython declarations. +fn rewrite_string_reference_type(ty: &mut Type) { + if matches!(ty, Type::Ptr { ty, .. } if is_string_path(ty)) { + *ty = Type::Array( + Box::new(Type::Primitive(PrimitiveType::Char)), + ConstExpr::Value(String::new()), + ); + } else if let Type::Array(inner, _) = ty { + reduce_string_reference_pointee(inner); + } +} + +/// Reduces the `str` / `CStr` pointee of a `&str` / `&CStr` array element to +/// `char`, keeping it a pointer, and recurses through nested arrays. +fn reduce_string_reference_pointee(ty: &mut Type) { + match ty { + Type::Ptr { ty, .. } if is_string_path(ty) => { + **ty = Type::Primitive(PrimitiveType::Char); + } + Type::Ptr { ty, .. } | Type::Array(ty, _) => reduce_string_reference_pointee(ty), + _ => {} + } +} + +/// Whether `write_field` will itself render a leading `const` for `ty`, so the +/// constant writers must not prepend a second one. True for a const pointer and +/// for an array whose element is (recursively) a const pointer. +fn write_field_prepends_const(ty: &Type) -> bool { + match ty { + Type::Ptr { is_const, .. } => *is_const, + Type::Array(inner, _) => write_field_prepends_const(inner), + _ => false, + } +} + +/// Loads a byte-string literal (`b"..."`) as a `uint8_t[N]` constant. +/// +/// Returns the constant's type and initializer, or `None` if `expr` is not a +/// byte string. A byte string is exposed as a `uint8_t` array rather than a C +/// string literal so that non-printable and non-NUL-terminated data survives (a +/// `b"..."` is not NUL-terminated and may contain interior NULs). Its declared +/// type (`&[u8; N]` or the unsized `&[u8]`) does not load into a bare array, so +/// both the type and the initializer are derived together from the literal +/// bytes -- which also makes the sized and unsized forms behave identically. +fn load_byte_string(expr: &syn::Expr) -> Option<(Type, Literal)> { + let bytes = match expr { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::ByteStr(value), + .. + }) => value.value(), + _ => return None, + }; + + let ty = Type::Array( + Box::new(Type::Primitive(PrimitiveType::Integer { + zeroable: true, + signed: false, + kind: IntKind::B8, + })), + ConstExpr::Value(bytes.len().to_string()), + ); + let lit = Literal::Array { + items: bytes + .iter() + .map(|byte| Literal::Expr(byte.to_string())) + .collect(), + }; + Some((ty, lit)) +} + // TODO: Maybe add support to more std associated constants. pub(crate) fn to_known_assoc_constant(associated_to: &Path, name: &str) -> Option { - use crate::bindgen::ir::{IntKind, PrimitiveType}; - if name != "MAX" && name != "MIN" { return None; } @@ -413,7 +544,15 @@ impl Literal { Ok(Literal::Expr(value.base10_digits().to_string())) } syn::Lit::Bool(ref value) => Ok(Literal::Expr(format!("{}", value.value))), - // TODO: Add support for byte string and Verbatim + syn::Lit::Str(ref value) => { + Ok(Literal::Expr(to_c_string_literal(value.value().as_bytes()))) + } + syn::Lit::CStr(ref value) => { + Ok(Literal::Expr(to_c_string_literal(value.value().to_bytes()))) + } + // Byte-string literals are handled by `load_byte_string`, which + // synthesizes the array type; nested byte strings stay unsupported. + // TODO: Add support for Verbatim _ => Err(format!("Unsupported literal expression. {:?}", *lit)), } } @@ -562,16 +701,16 @@ impl Constant { attrs: &[syn::Attribute], associated_to: Option, ) -> Result { - let ty = Type::load(ty)?; - let mut ty = match ty { - Some(ty) => ty, + let (mut ty, mut lit) = match load_byte_string(expr) { + Some(byte_string) => byte_string, None => { - return Err("Cannot have a zero sized const definition.".to_owned()); + let mut ty = Type::load(ty)? + .ok_or_else(|| "Cannot have a zero sized const definition.".to_owned())?; + rewrite_string_reference_type(&mut ty); + (ty, Literal::load(expr)?) } }; - let mut lit = Literal::load(expr)?; - if let Some(ref associated_to) = associated_to { ty.replace_self_with(associated_to); lit.replace_self_with(associated_to); @@ -690,7 +829,7 @@ impl Constant { let condition = self.cfg.to_condition(config); condition.write_before(config, out); - if let Type::Ptr { is_const: true, .. } = self.ty { + if write_field_prepends_const(&self.ty) { out.write("static "); } else { out.write("static const "); @@ -790,9 +929,7 @@ impl Constant { out.write(if in_body { "inline " } else { "static " }); } - if let Type::Ptr { is_const: true, .. } = self.ty { - // Nothing. - } else { + if !write_field_prepends_const(&self.ty) { out.write("const "); } crate::bindgen::cdecl::write_field(language_backend, out, &self.ty, &name, config); @@ -805,7 +942,9 @@ impl Constant { language_backend.write_literal(out, value); } Language::Cython => { - out.write("const "); + if !write_field_prepends_const(&self.ty) { + out.write("const "); + } // For extern Cython declarations the initializer is ignored, // but still useful as documentation, so we write it as a comment. crate::bindgen::cdecl::write_field(language_backend, out, &self.ty, &name, config); diff --git a/tests/expectations-symbols/const_byte_string.c.sym b/tests/expectations-symbols/const_byte_string.c.sym new file mode 100644 index 000000000..f018156fc --- /dev/null +++ b/tests/expectations-symbols/const_byte_string.c.sym @@ -0,0 +1,3 @@ +{ +root; +}; \ No newline at end of file diff --git a/tests/expectations-symbols/const_str.c.sym b/tests/expectations-symbols/const_str.c.sym new file mode 100644 index 000000000..f018156fc --- /dev/null +++ b/tests/expectations-symbols/const_str.c.sym @@ -0,0 +1,3 @@ +{ +root; +}; \ No newline at end of file diff --git a/tests/expectations-symbols/const_str_no_static.c.sym b/tests/expectations-symbols/const_str_no_static.c.sym new file mode 100644 index 000000000..f018156fc --- /dev/null +++ b/tests/expectations-symbols/const_str_no_static.c.sym @@ -0,0 +1,3 @@ +{ +root; +}; \ No newline at end of file diff --git a/tests/expectations/assoc_constant.c b/tests/expectations/assoc_constant.c index 7f319d789..8aeb1f3bd 100644 --- a/tests/expectations/assoc_constant.c +++ b/tests/expectations/assoc_constant.c @@ -7,6 +7,7 @@ typedef struct { } Foo; #define Foo_GA 10 +#define Foo_BU "hello world" #define Foo_ZO 3.14 void root(Foo x); diff --git a/tests/expectations/assoc_constant.compat.c b/tests/expectations/assoc_constant.compat.c index 711ed8d6c..aaa2eabd7 100644 --- a/tests/expectations/assoc_constant.compat.c +++ b/tests/expectations/assoc_constant.compat.c @@ -7,6 +7,7 @@ typedef struct { } Foo; #define Foo_GA 10 +#define Foo_BU "hello world" #define Foo_ZO 3.14 #ifdef __cplusplus diff --git a/tests/expectations/assoc_constant.cpp b/tests/expectations/assoc_constant.cpp index 599322923..0dcfd49e0 100644 --- a/tests/expectations/assoc_constant.cpp +++ b/tests/expectations/assoc_constant.cpp @@ -8,6 +8,7 @@ struct Foo { }; constexpr static const int32_t Foo_GA = 10; +constexpr static const char Foo_BU[] = "hello world"; constexpr static const float Foo_ZO = 3.14; extern "C" { diff --git a/tests/expectations/assoc_constant.pyx b/tests/expectations/assoc_constant.pyx index 51da63ec4..7b053bebe 100644 --- a/tests/expectations/assoc_constant.pyx +++ b/tests/expectations/assoc_constant.pyx @@ -9,6 +9,7 @@ cdef extern from *: ctypedef struct Foo: pass const int32_t Foo_GA # = 10 + const char Foo_BU[] # = "hello world" const float Foo_ZO # = 3.14 void root(Foo x); diff --git a/tests/expectations/assoc_constant_both.c b/tests/expectations/assoc_constant_both.c index 1aebafff4..0e4eae866 100644 --- a/tests/expectations/assoc_constant_both.c +++ b/tests/expectations/assoc_constant_both.c @@ -7,6 +7,7 @@ typedef struct Foo { } Foo; #define Foo_GA 10 +#define Foo_BU "hello world" #define Foo_ZO 3.14 void root(struct Foo x); diff --git a/tests/expectations/assoc_constant_both.compat.c b/tests/expectations/assoc_constant_both.compat.c index c137cce40..36ec70b6d 100644 --- a/tests/expectations/assoc_constant_both.compat.c +++ b/tests/expectations/assoc_constant_both.compat.c @@ -7,6 +7,7 @@ typedef struct Foo { } Foo; #define Foo_GA 10 +#define Foo_BU "hello world" #define Foo_ZO 3.14 #ifdef __cplusplus diff --git a/tests/expectations/assoc_constant_tag.c b/tests/expectations/assoc_constant_tag.c index 600cb48f9..86797eddc 100644 --- a/tests/expectations/assoc_constant_tag.c +++ b/tests/expectations/assoc_constant_tag.c @@ -7,6 +7,7 @@ struct Foo { }; #define Foo_GA 10 +#define Foo_BU "hello world" #define Foo_ZO 3.14 void root(struct Foo x); diff --git a/tests/expectations/assoc_constant_tag.compat.c b/tests/expectations/assoc_constant_tag.compat.c index 3ce582db7..29eb0533a 100644 --- a/tests/expectations/assoc_constant_tag.compat.c +++ b/tests/expectations/assoc_constant_tag.compat.c @@ -7,6 +7,7 @@ struct Foo { }; #define Foo_GA 10 +#define Foo_BU "hello world" #define Foo_ZO 3.14 #ifdef __cplusplus diff --git a/tests/expectations/assoc_constant_tag.pyx b/tests/expectations/assoc_constant_tag.pyx index a0aed8afe..0b11d8c27 100644 --- a/tests/expectations/assoc_constant_tag.pyx +++ b/tests/expectations/assoc_constant_tag.pyx @@ -9,6 +9,7 @@ cdef extern from *: cdef struct Foo: pass const int32_t Foo_GA # = 10 + const char Foo_BU[] # = "hello world" const float Foo_ZO # = 3.14 void root(Foo x); diff --git a/tests/expectations/const_byte_string.c b/tests/expectations/const_byte_string.c new file mode 100644 index 000000000..3de8342c6 --- /dev/null +++ b/tests/expectations/const_byte_string.c @@ -0,0 +1,19 @@ +#include +#include +#include +#include + +#define BYTES { 97, 98, 99, } + +#define SLICE { 110, 111, 116, 32, 110, 117, 108, 108, 0, 116, 101, 114, 109, 105, 110, 97, 116, 101, 100, } + +#define BINARY { 0, 255, 16, 127, } + +#define EMPTY { } + +/** + * A documented byte-string constant. + */ +#define DOCUMENTED { 100, 111, 99, } + +void root(void); diff --git a/tests/expectations/const_byte_string.compat.c b/tests/expectations/const_byte_string.compat.c new file mode 100644 index 000000000..7bcd26d34 --- /dev/null +++ b/tests/expectations/const_byte_string.compat.c @@ -0,0 +1,27 @@ +#include +#include +#include +#include + +#define BYTES { 97, 98, 99, } + +#define SLICE { 110, 111, 116, 32, 110, 117, 108, 108, 0, 116, 101, 114, 109, 105, 110, 97, 116, 101, 100, } + +#define BINARY { 0, 255, 16, 127, } + +#define EMPTY { } + +/** + * A documented byte-string constant. + */ +#define DOCUMENTED { 100, 111, 99, } + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +void root(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/tests/expectations/const_byte_string.cpp b/tests/expectations/const_byte_string.cpp new file mode 100644 index 000000000..7cc5641cd --- /dev/null +++ b/tests/expectations/const_byte_string.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include + +constexpr static const uint8_t BYTES[3] = { 97, 98, 99, }; + +constexpr static const uint8_t SLICE[19] = { 110, 111, 116, 32, 110, 117, 108, 108, 0, 116, 101, 114, 109, 105, 110, 97, 116, 101, 100, }; + +constexpr static const uint8_t BINARY[4] = { 0, 255, 16, 127, }; + +constexpr static const uint8_t EMPTY[0] = { }; + +/// A documented byte-string constant. +constexpr static const uint8_t DOCUMENTED[3] = { 100, 111, 99, }; + +extern "C" { + +void root(); + +} // extern "C" diff --git a/tests/expectations/const_byte_string.pyx b/tests/expectations/const_byte_string.pyx new file mode 100644 index 000000000..28f40414a --- /dev/null +++ b/tests/expectations/const_byte_string.pyx @@ -0,0 +1,20 @@ +from libc.stdint cimport int8_t, int16_t, int32_t, int64_t, intptr_t +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, uintptr_t +cdef extern from *: + ctypedef bint bool + ctypedef struct va_list + +cdef extern from *: + + const uint8_t BYTES[3] # = [ 97, 98, 99, ] + + const uint8_t SLICE[19] # = [ 110, 111, 116, 32, 110, 117, 108, 108, 0, 116, 101, 114, 109, 105, 110, 97, 116, 101, 100, ] + + const uint8_t BINARY[4] # = [ 0, 255, 16, 127, ] + + const uint8_t EMPTY[0] # = [ ] + + # A documented byte-string constant. + const uint8_t DOCUMENTED[3] # = [ 100, 111, 99, ] + + void root(); diff --git a/tests/expectations/const_str.c b/tests/expectations/const_str.c new file mode 100644 index 000000000..3dfd7d23f --- /dev/null +++ b/tests/expectations/const_str.c @@ -0,0 +1,41 @@ +#include +#include +#include +#include + +#define STRING "string" + +#define C_STRING "c string" + +#define EMPTY "" + +#define WITH_QUOTES "a \"quoted\" word" + +#define WITH_BACKSLASH "back\\slash" + +#define WITH_CONTROLS "tab\tnewline\nreturn\r" + +#define OTHER_CONTROLS "\x01\x0b\x0c\x7f" + +#define TRIGRAPHS "what\?\?!" + +#define INTERIOR_NUL "a\x00" "b" + +#define NON_UTF8 "\xff\xfe" + +#define NON_ASCII "caf\xc3\xa9" + +#define HEX_ADJACENT "\xc3" "a" + +#define HEX_ADJACENT_UPPER "\xc3" "F" + +#define HEX_NON_ADJACENT "\xc3z" + +#define ARRAY { "first", "second", } + +/** + * A documented string constant. + */ +#define DOCUMENTED "documented" + +void root(void); diff --git a/tests/expectations/const_str.compat.c b/tests/expectations/const_str.compat.c new file mode 100644 index 000000000..049bdc81e --- /dev/null +++ b/tests/expectations/const_str.compat.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +#define STRING "string" + +#define C_STRING "c string" + +#define EMPTY "" + +#define WITH_QUOTES "a \"quoted\" word" + +#define WITH_BACKSLASH "back\\slash" + +#define WITH_CONTROLS "tab\tnewline\nreturn\r" + +#define OTHER_CONTROLS "\x01\x0b\x0c\x7f" + +#define TRIGRAPHS "what\?\?!" + +#define INTERIOR_NUL "a\x00" "b" + +#define NON_UTF8 "\xff\xfe" + +#define NON_ASCII "caf\xc3\xa9" + +#define HEX_ADJACENT "\xc3" "a" + +#define HEX_ADJACENT_UPPER "\xc3" "F" + +#define HEX_NON_ADJACENT "\xc3z" + +#define ARRAY { "first", "second", } + +/** + * A documented string constant. + */ +#define DOCUMENTED "documented" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +void root(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/tests/expectations/const_str.cpp b/tests/expectations/const_str.cpp new file mode 100644 index 000000000..caf1e1f3f --- /dev/null +++ b/tests/expectations/const_str.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include + +constexpr static const char STRING[] = "string"; + +constexpr static const char C_STRING[] = "c string"; + +constexpr static const char EMPTY[] = ""; + +constexpr static const char WITH_QUOTES[] = "a \"quoted\" word"; + +constexpr static const char WITH_BACKSLASH[] = "back\\slash"; + +constexpr static const char WITH_CONTROLS[] = "tab\tnewline\nreturn\r"; + +constexpr static const char OTHER_CONTROLS[] = "\x01\x0b\x0c\x7f"; + +constexpr static const char TRIGRAPHS[] = "what\?\?!"; + +constexpr static const char INTERIOR_NUL[] = "a\x00" "b"; + +constexpr static const char NON_UTF8[] = "\xff\xfe"; + +constexpr static const char NON_ASCII[] = "caf\xc3\xa9"; + +constexpr static const char HEX_ADJACENT[] = "\xc3" "a"; + +constexpr static const char HEX_ADJACENT_UPPER[] = "\xc3" "F"; + +constexpr static const char HEX_NON_ADJACENT[] = "\xc3z"; + +constexpr static const char *ARRAY[2] = { "first", "second", }; + +/// A documented string constant. +constexpr static const char DOCUMENTED[] = "documented"; + +extern "C" { + +void root(); + +} // extern "C" diff --git a/tests/expectations/const_str.pyx b/tests/expectations/const_str.pyx new file mode 100644 index 000000000..d77ed33b4 --- /dev/null +++ b/tests/expectations/const_str.pyx @@ -0,0 +1,42 @@ +from libc.stdint cimport int8_t, int16_t, int32_t, int64_t, intptr_t +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, uintptr_t +cdef extern from *: + ctypedef bint bool + ctypedef struct va_list + +cdef extern from *: + + const char STRING[] # = "string" + + const char C_STRING[] # = "c string" + + const char EMPTY[] # = "" + + const char WITH_QUOTES[] # = "a \"quoted\" word" + + const char WITH_BACKSLASH[] # = "back\\slash" + + const char WITH_CONTROLS[] # = "tab\tnewline\nreturn\r" + + const char OTHER_CONTROLS[] # = "\x01\x0b\x0c\x7f" + + const char TRIGRAPHS[] # = "what\?\?!" + + const char INTERIOR_NUL[] # = "a\x00" "b" + + const char NON_UTF8[] # = "\xff\xfe" + + const char NON_ASCII[] # = "caf\xc3\xa9" + + const char HEX_ADJACENT[] # = "\xc3" "a" + + const char HEX_ADJACENT_UPPER[] # = "\xc3" "F" + + const char HEX_NON_ADJACENT[] # = "\xc3z" + + const char *ARRAY[2] # = [ "first", "second", ] + + # A documented string constant. + const char DOCUMENTED[] # = "documented" + + void root(); diff --git a/tests/expectations/const_str_no_static.c b/tests/expectations/const_str_no_static.c new file mode 100644 index 000000000..448ac15b9 --- /dev/null +++ b/tests/expectations/const_str_no_static.c @@ -0,0 +1,12 @@ +#include +#include +#include +#include + +#define C_STRING "winner" + +#define BYTES { 97, 98, 99, } + +#define ARRAY { "first", "second", } + +void root(void); diff --git a/tests/expectations/const_str_no_static.compat.c b/tests/expectations/const_str_no_static.compat.c new file mode 100644 index 000000000..2328c454b --- /dev/null +++ b/tests/expectations/const_str_no_static.compat.c @@ -0,0 +1,20 @@ +#include +#include +#include +#include + +#define C_STRING "winner" + +#define BYTES { 97, 98, 99, } + +#define ARRAY { "first", "second", } + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +void root(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/tests/expectations/const_str_no_static.cpp b/tests/expectations/const_str_no_static.cpp new file mode 100644 index 000000000..b456103cc --- /dev/null +++ b/tests/expectations/const_str_no_static.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include +#include + +#define C_STRING "winner" + +#define BYTES { 97, 98, 99, } + +#define ARRAY { "first", "second", } + +extern "C" { + +void root(); + +} // extern "C" diff --git a/tests/expectations/const_str_no_static.pyx b/tests/expectations/const_str_no_static.pyx new file mode 100644 index 000000000..eb992cde4 --- /dev/null +++ b/tests/expectations/const_str_no_static.pyx @@ -0,0 +1,15 @@ +from libc.stdint cimport int8_t, int16_t, int32_t, int64_t, intptr_t +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, uintptr_t +cdef extern from *: + ctypedef bint bool + ctypedef struct va_list + +cdef extern from *: + + const char C_STRING[] # = "winner" + + const uint8_t BYTES[3] # = [ 97, 98, 99, ] + + const char *ARRAY[2] # = [ "first", "second", ] + + void root(); diff --git a/tests/expectations/constant.c b/tests/expectations/constant.c index 3ae46806d..82969d0a8 100644 --- a/tests/expectations/constant.c +++ b/tests/expectations/constant.c @@ -50,4 +50,6 @@ typedef struct { int32_t x[FOO]; } Foo; +#define BAR "hello world" + void root(Foo x); diff --git a/tests/expectations/constant.compat.c b/tests/expectations/constant.compat.c index 2906a062d..664d2a158 100644 --- a/tests/expectations/constant.compat.c +++ b/tests/expectations/constant.compat.c @@ -50,6 +50,8 @@ typedef struct { int32_t x[FOO]; } Foo; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/constant.cpp b/tests/expectations/constant.cpp index b72206b62..b226efa54 100644 --- a/tests/expectations/constant.cpp +++ b/tests/expectations/constant.cpp @@ -47,6 +47,8 @@ struct Foo { int32_t x[FOO]; }; +constexpr static const char BAR[] = "hello world"; + extern "C" { void root(Foo x); diff --git a/tests/expectations/constant.pyx b/tests/expectations/constant.pyx index 8ec614ec1..3a2a74753 100644 --- a/tests/expectations/constant.pyx +++ b/tests/expectations/constant.pyx @@ -48,4 +48,6 @@ cdef extern from *: ctypedef struct Foo: int32_t x[FOO]; + const char BAR[] # = "hello world" + void root(Foo x); diff --git a/tests/expectations/constant_both.c b/tests/expectations/constant_both.c index 73ae53298..d48e6fe36 100644 --- a/tests/expectations/constant_both.c +++ b/tests/expectations/constant_both.c @@ -50,4 +50,6 @@ typedef struct Foo { int32_t x[FOO]; } Foo; +#define BAR "hello world" + void root(struct Foo x); diff --git a/tests/expectations/constant_both.compat.c b/tests/expectations/constant_both.compat.c index 467fd70e9..8e8bfd07f 100644 --- a/tests/expectations/constant_both.compat.c +++ b/tests/expectations/constant_both.compat.c @@ -50,6 +50,8 @@ typedef struct Foo { int32_t x[FOO]; } Foo; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/constant_tag.c b/tests/expectations/constant_tag.c index b5aa35526..308e0db54 100644 --- a/tests/expectations/constant_tag.c +++ b/tests/expectations/constant_tag.c @@ -50,4 +50,6 @@ struct Foo { int32_t x[FOO]; }; +#define BAR "hello world" + void root(struct Foo x); diff --git a/tests/expectations/constant_tag.compat.c b/tests/expectations/constant_tag.compat.c index d406f5e70..13019ae67 100644 --- a/tests/expectations/constant_tag.compat.c +++ b/tests/expectations/constant_tag.compat.c @@ -50,6 +50,8 @@ struct Foo { int32_t x[FOO]; }; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/constant_tag.pyx b/tests/expectations/constant_tag.pyx index 5d60f6ad0..7e4c8013d 100644 --- a/tests/expectations/constant_tag.pyx +++ b/tests/expectations/constant_tag.pyx @@ -48,4 +48,6 @@ cdef extern from *: cdef struct Foo: int32_t x[FOO]; + const char BAR[] # = "hello world" + void root(Foo x); diff --git a/tests/expectations/namespace_constant.c b/tests/expectations/namespace_constant.c index cb6f6cf25..c50f724f2 100644 --- a/tests/expectations/namespace_constant.c +++ b/tests/expectations/namespace_constant.c @@ -11,4 +11,6 @@ typedef struct { int32_t x[FOO]; } Foo; +#define BAR "hello world" + void root(Foo x); diff --git a/tests/expectations/namespace_constant.compat.c b/tests/expectations/namespace_constant.compat.c index cd0df69b1..2b6c0a988 100644 --- a/tests/expectations/namespace_constant.compat.c +++ b/tests/expectations/namespace_constant.compat.c @@ -15,6 +15,8 @@ typedef struct { int32_t x[FOO]; } Foo; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/namespace_constant.cpp b/tests/expectations/namespace_constant.cpp index f62b20d7d..c6cc60e15 100644 --- a/tests/expectations/namespace_constant.cpp +++ b/tests/expectations/namespace_constant.cpp @@ -14,6 +14,8 @@ struct Foo { int32_t x[FOO]; }; +constexpr static const char BAR[] = "hello world"; + extern "C" { void root(Foo x); diff --git a/tests/expectations/namespace_constant.pyx b/tests/expectations/namespace_constant.pyx index 8bc3b8b78..a6760fda3 100644 --- a/tests/expectations/namespace_constant.pyx +++ b/tests/expectations/namespace_constant.pyx @@ -13,4 +13,6 @@ cdef extern from *: ctypedef struct Foo: int32_t x[FOO]; + const char BAR[] # = "hello world" + void root(Foo x); diff --git a/tests/expectations/namespace_constant_both.c b/tests/expectations/namespace_constant_both.c index 853713a50..5e0c5705f 100644 --- a/tests/expectations/namespace_constant_both.c +++ b/tests/expectations/namespace_constant_both.c @@ -11,4 +11,6 @@ typedef struct Foo { int32_t x[FOO]; } Foo; +#define BAR "hello world" + void root(struct Foo x); diff --git a/tests/expectations/namespace_constant_both.compat.c b/tests/expectations/namespace_constant_both.compat.c index 4d7cd8c4b..78e800bad 100644 --- a/tests/expectations/namespace_constant_both.compat.c +++ b/tests/expectations/namespace_constant_both.compat.c @@ -15,6 +15,8 @@ typedef struct Foo { int32_t x[FOO]; } Foo; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/namespace_constant_tag.c b/tests/expectations/namespace_constant_tag.c index 80a8ea03f..a845bfd20 100644 --- a/tests/expectations/namespace_constant_tag.c +++ b/tests/expectations/namespace_constant_tag.c @@ -11,4 +11,6 @@ struct Foo { int32_t x[FOO]; }; +#define BAR "hello world" + void root(struct Foo x); diff --git a/tests/expectations/namespace_constant_tag.compat.c b/tests/expectations/namespace_constant_tag.compat.c index ee17dc667..df9c26350 100644 --- a/tests/expectations/namespace_constant_tag.compat.c +++ b/tests/expectations/namespace_constant_tag.compat.c @@ -15,6 +15,8 @@ struct Foo { int32_t x[FOO]; }; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/namespace_constant_tag.pyx b/tests/expectations/namespace_constant_tag.pyx index 8c1d8aabe..1b39c7806 100644 --- a/tests/expectations/namespace_constant_tag.pyx +++ b/tests/expectations/namespace_constant_tag.pyx @@ -13,4 +13,6 @@ cdef extern from *: cdef struct Foo: int32_t x[FOO]; + const char BAR[] # = "hello world" + void root(Foo x); diff --git a/tests/expectations/namespaces_constant.c b/tests/expectations/namespaces_constant.c index cb6f6cf25..c50f724f2 100644 --- a/tests/expectations/namespaces_constant.c +++ b/tests/expectations/namespaces_constant.c @@ -11,4 +11,6 @@ typedef struct { int32_t x[FOO]; } Foo; +#define BAR "hello world" + void root(Foo x); diff --git a/tests/expectations/namespaces_constant.compat.c b/tests/expectations/namespaces_constant.compat.c index 58efa97aa..8a771051b 100644 --- a/tests/expectations/namespaces_constant.compat.c +++ b/tests/expectations/namespaces_constant.compat.c @@ -16,6 +16,8 @@ typedef struct { int32_t x[FOO]; } Foo; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/namespaces_constant.cpp b/tests/expectations/namespaces_constant.cpp index 6140c2abe..7511b014b 100644 --- a/tests/expectations/namespaces_constant.cpp +++ b/tests/expectations/namespaces_constant.cpp @@ -15,6 +15,8 @@ struct Foo { int32_t x[FOO]; }; +constexpr static const char BAR[] = "hello world"; + extern "C" { void root(Foo x); diff --git a/tests/expectations/namespaces_constant.pyx b/tests/expectations/namespaces_constant.pyx index 8bc3b8b78..a6760fda3 100644 --- a/tests/expectations/namespaces_constant.pyx +++ b/tests/expectations/namespaces_constant.pyx @@ -13,4 +13,6 @@ cdef extern from *: ctypedef struct Foo: int32_t x[FOO]; + const char BAR[] # = "hello world" + void root(Foo x); diff --git a/tests/expectations/namespaces_constant_both.c b/tests/expectations/namespaces_constant_both.c index 853713a50..5e0c5705f 100644 --- a/tests/expectations/namespaces_constant_both.c +++ b/tests/expectations/namespaces_constant_both.c @@ -11,4 +11,6 @@ typedef struct Foo { int32_t x[FOO]; } Foo; +#define BAR "hello world" + void root(struct Foo x); diff --git a/tests/expectations/namespaces_constant_both.compat.c b/tests/expectations/namespaces_constant_both.compat.c index 9e49dbdf5..359a16f10 100644 --- a/tests/expectations/namespaces_constant_both.compat.c +++ b/tests/expectations/namespaces_constant_both.compat.c @@ -16,6 +16,8 @@ typedef struct Foo { int32_t x[FOO]; } Foo; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/namespaces_constant_tag.c b/tests/expectations/namespaces_constant_tag.c index 80a8ea03f..a845bfd20 100644 --- a/tests/expectations/namespaces_constant_tag.c +++ b/tests/expectations/namespaces_constant_tag.c @@ -11,4 +11,6 @@ struct Foo { int32_t x[FOO]; }; +#define BAR "hello world" + void root(struct Foo x); diff --git a/tests/expectations/namespaces_constant_tag.compat.c b/tests/expectations/namespaces_constant_tag.compat.c index 9a93f9679..77ee9cca2 100644 --- a/tests/expectations/namespaces_constant_tag.compat.c +++ b/tests/expectations/namespaces_constant_tag.compat.c @@ -16,6 +16,8 @@ struct Foo { int32_t x[FOO]; }; +#define BAR "hello world" + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/tests/expectations/namespaces_constant_tag.pyx b/tests/expectations/namespaces_constant_tag.pyx index 8c1d8aabe..1b39c7806 100644 --- a/tests/expectations/namespaces_constant_tag.pyx +++ b/tests/expectations/namespaces_constant_tag.pyx @@ -13,4 +13,6 @@ cdef extern from *: cdef struct Foo: int32_t x[FOO]; + const char BAR[] # = "hello world" + void root(Foo x); diff --git a/tests/rust/const_byte_string.rs b/tests/rust/const_byte_string.rs new file mode 100644 index 000000000..a0ef58f3a --- /dev/null +++ b/tests/rust/const_byte_string.rs @@ -0,0 +1,18 @@ +pub const BYTES: &[u8; 3] = b"abc"; + +// The unsized `&[u8]` slice form: the array length comes from the literal. +pub const SLICE: &[u8] = b"not null\0terminated"; + +// Non-printable and interior-NUL bytes survive because the value is a `uint8_t` +// array, not a C string literal. +pub const BINARY: &[u8; 4] = b"\x00\xff\x10\x7f"; + +// An empty byte string produces the same zero-length array as any other empty +// array constant (e.g. `[u8; 0]`); it is not special-cased here. +pub const EMPTY: &[u8] = b""; + +/// A documented byte-string constant. +pub const DOCUMENTED: &[u8; 3] = b"doc"; + +#[no_mangle] +pub extern "C" fn root() {} diff --git a/tests/rust/const_str.rs b/tests/rust/const_str.rs new file mode 100644 index 000000000..737533032 --- /dev/null +++ b/tests/rust/const_str.rs @@ -0,0 +1,40 @@ +use std::ffi::CStr; + +pub const STRING: &str = "string"; +pub const C_STRING: &CStr = c"c string"; +pub const EMPTY: &str = ""; + +pub const WITH_QUOTES: &str = "a \"quoted\" word"; +pub const WITH_BACKSLASH: &str = "back\\slash"; +pub const WITH_CONTROLS: &str = "tab\tnewline\nreturn\r"; +// Control bytes without a named escape, plus DEL (0x7f), become `\xNN`. +pub const OTHER_CONTROLS: &str = "\x01\x0b\x0c\x7f"; + +// `?` is escaped so no `??x` trigraph can form. +pub const TRIGRAPHS: &str = "what??!"; + +// A `&str` may contain an interior NUL; it survives as `\x00` (and forces a split +// before the following hex-digit character). +pub const INTERIOR_NUL: &str = "a\0b"; + +// A `&CStr` may hold non-UTF-8 bytes; they are escaped byte-for-byte. +pub const NON_UTF8: &CStr = c"\xff\xfe"; + +// Non-ASCII is emitted as the raw UTF-8 bytes escaped as `\xNN`, never Rust's +// `\u{...}`, which is not valid C. +pub const NON_ASCII: &str = "caf\u{e9}"; + +// A `\xNN` escape followed by a literal hex digit must not be absorbed into the +// escape: these stay two bytes, not one. Covers lower- and upper-case digits. +pub const HEX_ADJACENT: &CStr = c"\xc3a"; +pub const HEX_ADJACENT_UPPER: &CStr = c"\xc3F"; +// A `\xNN` escape followed by a non-hex character needs no split. +pub const HEX_NON_ADJACENT: &CStr = c"\xc3z"; + +pub const ARRAY: [&CStr; 2] = [c"first", c"second"]; + +/// A documented string constant. +pub const DOCUMENTED: &str = "documented"; + +#[no_mangle] +pub extern "C" fn root() {} diff --git a/tests/rust/const_str_no_static.rs b/tests/rust/const_str_no_static.rs new file mode 100644 index 000000000..13e4400dc --- /dev/null +++ b/tests/rust/const_str_no_static.rs @@ -0,0 +1,10 @@ +use std::ffi::CStr; + +// With `allow_static_const` and `allow_constexpr` both off, string, byte-string, +// and array constants fall back to the `#define` form in C++ too. +pub const C_STRING: &CStr = c"winner"; +pub const BYTES: &[u8; 3] = b"abc"; +pub const ARRAY: [&CStr; 2] = [c"first", c"second"]; + +#[no_mangle] +pub extern "C" fn root() {} diff --git a/tests/rust/const_str_no_static.toml b/tests/rust/const_str_no_static.toml new file mode 100644 index 000000000..c0bd8a55e --- /dev/null +++ b/tests/rust/const_str_no_static.toml @@ -0,0 +1,3 @@ +[const] +allow_static_const = false +allow_constexpr = false