From 77c3ad5a9ab69190ee361986caf579afa2eae570 Mon Sep 17 00:00:00 2001 From: Shuai Mu Date: Sun, 23 Aug 2026 20:38:42 -0400 Subject: [PATCH] transpiler: support scalar value-init marker --- transpiler/src/codegen/emit_items.rs | 11 +- transpiler/src/cpp_name.rs | 1 + transpiler/src/cpp_value_init.rs | 327 +++++++++++++++++++++ transpiler/src/main.rs | 1 + transpiler/src/transpile.rs | 25 ++ transpiler/tests/cpp_value_init_codegen.rs | 176 +++++++++++ 6 files changed, 539 insertions(+), 2 deletions(-) create mode 100644 transpiler/src/cpp_value_init.rs create mode 100644 transpiler/tests/cpp_value_init_codegen.rs diff --git a/transpiler/src/codegen/emit_items.rs b/transpiler/src/codegen/emit_items.rs index 72410851..f97a33d9 100644 --- a/transpiler/src/codegen/emit_items.rs +++ b/transpiler/src/codegen/emit_items.rs @@ -2371,7 +2371,15 @@ impl CodeGen { let field_type = self .zero_len_array_field_type_override(&field.ty) .unwrap_or(field_type); - self.writeln(&format!("{} {};", field_type, emitted_field_name)); + let initializer = if crate::cpp_value_init::field_has_marker(field) { + "{}" + } else { + "" + }; + self.writeln(&format!( + "{} {}{};", + field_type, emitted_field_name, initializer + )); used_member_names.insert(emitted_field_name.clone()); named_field_types.insert(field_name.clone(), field.ty.clone()); if matches!(field.ty, syn::Type::Reference(_)) { @@ -11399,4 +11407,3 @@ pub(super) fn contains_whole_word(haystack: &str, needle: &str) -> bool { } false } - diff --git a/transpiler/src/cpp_name.rs b/transpiler/src/cpp_name.rs index 309c868d..54adf01e 100644 --- a/transpiler/src/cpp_name.rs +++ b/transpiler/src/cpp_name.rs @@ -267,6 +267,7 @@ fn audited_transpiler_marker_meta(meta: &Meta) -> bool { | "cpp_no_auto_traits" | "cpp_no_fieldwise_ctor" | "cpp_noexcept" + | "cpp_value_init" | "thread_local" ) }) diff --git a/transpiler/src/cpp_value_init.rs b/transpiler/src/cpp_value_init.rs new file mode 100644 index 00000000..3414e11c --- /dev/null +++ b/transpiler/src/cpp_value_init.rs @@ -0,0 +1,327 @@ +//! Validation for the narrow C++ default-member-initializer marker. +//! +//! Rust has no field-default syntax equivalent to C++'s `member{}`. A +//! downstream ABI may nevertheless require that exact C++ spelling: it keeps +//! an ordinary aggregate while making plain default construction initialize a +//! scalar field. The inert source spelling +//! +//! ```ignore +//! #[cfg_attr(any(), cpp_value_init)] +//! field: u64, +//! ``` +//! +//! is deliberately compiler-owned and fail-closed. Only named fields of +//! ordinary structs, and only built-in bool/integer scalar types, may carry it. + +use quote::ToTokens; +use syn::visit::Visit; + +const MARKER: &str = "cpp_value_init"; + +fn ident_is_exact(ident: &proc_macro2::Ident, expected: &str) -> bool { + // Raw identifiers are intentionally not an alternate spelling of a + // compiler-owned marker or its `any` predicate. + let spelling = ident.to_string(); + !spelling.starts_with("r#") && ident == expected +} + +fn ident_mentions(ident: &proc_macro2::Ident, expected: &str) -> bool { + let text = ident.to_string(); + text.strip_prefix("r#").unwrap_or(&text) == expected +} + +fn path_is_exact_ident(path: &syn::Path, expected: &str) -> bool { + path.leading_colon.is_none() + && path.segments.len() == 1 + && path.segments.first().is_some_and(|segment| { + ident_is_exact(&segment.ident, expected) + && matches!(segment.arguments, syn::PathArguments::None) + }) +} + +fn token_stream_marker_count(tokens: proc_macro2::TokenStream) -> usize { + tokens + .into_iter() + .map(|token| match token { + proc_macro2::TokenTree::Ident(ident) => usize::from(ident_mentions(&ident, MARKER)), + proc_macro2::TokenTree::Group(group) => token_stream_marker_count(group.stream()), + _ => 0, + }) + .sum() +} + +fn attribute_marker_count(attr: &syn::Attribute) -> usize { + token_stream_marker_count(attr.meta.to_token_stream()) +} + +fn attribute_is_exact_marker(attr: &syn::Attribute) -> bool { + if !path_is_exact_ident(attr.path(), "cfg_attr") { + return false; + } + let Ok(args) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) else { + return false; + }; + if args.len() != 2 || args.trailing_punct() { + return false; + } + let Some(syn::Meta::List(predicate)) = args.first() else { + return false; + }; + let Some(syn::Meta::Path(marker)) = args.iter().nth(1) else { + return false; + }; + path_is_exact_ident(&predicate.path, "any") + && predicate.tokens.is_empty() + && path_is_exact_ident(marker, MARKER) +} + +fn type_is_supported_scalar(ty: &syn::Type) -> bool { + let syn::Type::Path(path) = ty else { + return false; + }; + if path.qself.is_some() || path.path.leading_colon.is_some() || path.path.segments.len() != 1 { + return false; + } + path.path.segments.first().is_some_and(|segment| { + matches!(segment.arguments, syn::PathArguments::None) + && matches!( + segment.ident.to_string().as_str(), + "bool" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "u8" + | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + ) + }) +} + +/// Whether a field carries the one accepted marker spelling. +/// +/// Codegen calls this only after [`validate_file`] has accepted the complete +/// syntax tree, so `any` is sufficient here: validation already proves there +/// is exactly one marker and that the field/type placement is supported. +pub(crate) fn field_has_marker(field: &syn::Field) -> bool { + field.attrs.iter().any(attribute_is_exact_marker) +} + +/// Validate every occurrence of the reserved marker before any C++ is emitted. +pub(crate) fn validate_source(source: &str, file: &syn::File) -> Result<(), String> { + // Keep marker-free transpilation on its old hot path. The substring gate + // cannot admit anything (the AST audit below remains authoritative); it + // only avoids re-tokenizing and walking large ordinary source files. + if !source.contains(MARKER) { + return Ok(()); + } + validate_file(file) +} + +/// Validate a pre-parsed source file when the original text is unavailable. +pub(crate) fn validate_file(file: &syn::File) -> Result<(), String> { + struct Validator { + marker_tokens_in_attributes: usize, + error: Option, + } + + impl Validator { + fn reject_attribute(&mut self, attr: &syn::Attribute, placement: &str) { + let count = attribute_marker_count(attr); + self.marker_tokens_in_attributes += count; + if count != 0 && self.error.is_none() { + self.error = Some(format!( + "{MARKER} is supported only as exactly one \ + #[cfg_attr(any(), {MARKER})] on a named field of an ordinary struct; \ + found it {placement}" + )); + } + } + + fn validate_named_struct_field(&mut self, field: &syn::Field) { + let marker_count: usize = field.attrs.iter().map(attribute_marker_count).sum(); + self.marker_tokens_in_attributes += marker_count; + if marker_count == 0 || self.error.is_some() { + syn::visit::visit_type(self, &field.ty); + return; + } + + let exact_count = field + .attrs + .iter() + .filter(|attr| attribute_is_exact_marker(attr)) + .count(); + if marker_count != 1 || exact_count != 1 { + self.error = Some(format!( + "{MARKER} must use exactly one exact inert attribute \ + #[cfg_attr(any(), {MARKER})] on a named struct field" + )); + return; + } + if !type_is_supported_scalar(&field.ty) { + self.error = Some(format!( + "{MARKER} supports only fields whose Rust type is exactly bool or a \ + built-in signed/unsigned integer primitive" + )); + return; + } + + // Attributes cannot contain nested Rust types, but the field type + // can contain attributes/macros of its own. Continue walking it + // so a second reserved marker cannot hide there. + syn::visit::visit_type(self, &field.ty); + } + } + + impl<'ast> Visit<'ast> for Validator { + fn visit_attribute(&mut self, attr: &'ast syn::Attribute) { + self.reject_attribute(attr, "outside an eligible named struct field"); + } + + fn visit_item_struct(&mut self, item: &'ast syn::ItemStruct) { + for attr in &item.attrs { + self.reject_attribute(attr, "on a struct rather than one of its named fields"); + } + syn::visit::visit_generics(self, &item.generics); + match &item.fields { + syn::Fields::Named(fields) => { + for field in &fields.named { + self.validate_named_struct_field(field); + } + } + syn::Fields::Unnamed(fields) => { + for field in &fields.unnamed { + syn::visit::visit_field(self, field); + } + } + syn::Fields::Unit => {} + } + } + } + + let marker_tokens = token_stream_marker_count(file.to_token_stream()); + if marker_tokens == 0 { + return Ok(()); + } + + let mut validator = Validator { + marker_tokens_in_attributes: 0, + error: None, + }; + validator.visit_file(file); + if let Some(error) = validator.error { + return Err(error); + } + if validator.marker_tokens_in_attributes != marker_tokens { + return Err(format!( + "reserved {MARKER} identifier is supported only in exactly one \ + #[cfg_attr(any(), {MARKER})] attribute on an eligible named struct field" + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn validate(source: &str) -> Result<(), String> { + validate_file(&syn::parse_file(source).expect("fixture should parse")) + } + + #[test] + fn accepts_bool_and_integer_primitive_named_fields() { + validate( + r#" + struct Scalars { + #[cfg_attr(any(), cpp_value_init)] a: bool, + #[cfg_attr(any(), cpp_value_init)] b: i8, + #[cfg_attr(any(), cpp_value_init)] c: i16, + #[cfg_attr(any(), cpp_value_init)] d: i32, + #[cfg_attr(any(), cpp_value_init)] e: i64, + #[cfg_attr(any(), cpp_value_init)] f: i128, + #[cfg_attr(any(), cpp_value_init)] g: isize, + #[cfg_attr(any(), cpp_value_init)] h: u8, + #[cfg_attr(any(), cpp_value_init)] i: u16, + #[cfg_attr(any(), cpp_value_init)] j: u32, + #[cfg_attr(any(), cpp_value_init)] k: u64, + #[cfg_attr(any(), cpp_value_init)] l: u128, + #[cfg_attr(any(), cpp_value_init)] m: usize, + } + "#, + ) + .expect("supported scalar fields should validate"); + } + + #[test] + fn rejects_non_exact_active_and_duplicate_spellings() { + for source in [ + "struct S { #[cpp_value_init] value: u64 }", + "struct S { #[cfg_attr(all(), cpp_value_init)] value: u64 }", + "struct S { #[cfg_attr(not(any()), cpp_value_init)] value: u64 }", + "struct S { #[cfg_attr(any(), cpp_value_init())] value: u64 }", + "struct S { #[cfg_attr(any(), crate::cpp_value_init)] value: u64 }", + "struct S { #[cfg_attr(any(), r#cpp_value_init)] value: u64 }", + "struct S { #[cfg_attr(any(), cpp_value_init, allow(dead_code))] value: u64 }", + "struct S { #[cfg_attr(any(), cfg_attr(any(), cpp_value_init))] value: u64 }", + "struct S { #[cfg_attr(any(), cpp_value_init,)] value: u64 }", + "struct S { #[cfg_attr(any(), cpp_value_init)] #[cfg_attr(any(), cpp_value_init)] value: u64 }", + ] { + let error = match validate(source) { + Ok(()) => panic!("accepted non-exact marker: {source}"), + Err(error) => error, + }; + assert!( + error.contains(MARKER), + "unexpected error for {source}: {error}" + ); + } + } + + #[test] + fn rejects_every_non_named_struct_field_placement() { + for source in [ + "#[cfg_attr(any(), cpp_value_init)] struct S { value: u64 }", + "struct S(#[cfg_attr(any(), cpp_value_init)] u64);", + "enum E { V { #[cfg_attr(any(), cpp_value_init)] value: u64 } }", + "enum E { V(#[cfg_attr(any(), cpp_value_init)] u64) }", + "union U { #[cfg_attr(any(), cpp_value_init)] value: u64 }", + "fn f(#[cfg_attr(any(), cpp_value_init)] value: u64) {}", + "const cpp_value_init: u64 = 0;", + "macro_rules! m { () => { #[cfg_attr(any(), cpp_value_init)] value: u64 } }", + ] { + let error = validate(source).expect_err("wrong marker placement must fail closed"); + assert!( + error.contains(MARKER), + "unexpected error for {source}: {error}" + ); + } + } + + #[test] + fn rejects_non_scalar_and_non_builtin_field_types() { + for ty in [ + "char", + "f32", + "f64", + "String", + "Alias", + "[u8; 4]", + "*const u8", + "&'static u64", + "Option", + "std::primitive::u64", + ] { + let source = format!("struct S {{ #[cfg_attr(any(), cpp_value_init)] value: {ty} }}"); + let error = validate(&source).expect_err("non-scalar marker must fail closed"); + assert!(error.contains("bool"), "unexpected error for {ty}: {error}"); + } + } +} diff --git a/transpiler/src/main.rs b/transpiler/src/main.rs index ff5f3f2b..f4999eb4 100644 --- a/transpiler/src/main.rs +++ b/transpiler/src/main.rs @@ -11,6 +11,7 @@ mod codegen; mod cpp_abi; mod cpp_default_args; mod cpp_name; +mod cpp_value_init; mod inline_rust; mod metadata; mod slots; diff --git a/transpiler/src/transpile.rs b/transpiler/src/transpile.rs index 6077223d..e2c79fe1 100644 --- a/transpiler/src/transpile.rs +++ b/transpiler/src/transpile.rs @@ -3107,12 +3107,14 @@ fn transpile_full_with_options_impl( } }; let (mut file, cpp_abi_plan, has_cpp_defaults) = if let Some((file, plan)) = prepared_cpp_abi { + crate::cpp_value_init::validate_file(&file)?; let has_cpp_defaults = validate_cpp_defaults(&file)?; (file, plan, has_cpp_defaults) } else { let file: syn::File = parse_with_expand_hygiene_fallback(rust_source) .map_err(|e| format!("Parse error: {}", e))?; log_profile("parse_with_expand_hygiene_fallback"); + crate::cpp_value_init::validate_source(rust_source, &file)?; let has_cpp_defaults = validate_cpp_defaults(&file)?; match crate::cpp_abi::lower(&file, options.flat_import_namespace.as_deref())? { Some((lowered, plan)) => (lowered, plan, has_cpp_defaults), @@ -3387,6 +3389,7 @@ const KNOWN_CPP_MARKER_NAMES: &[&str] = &[ "cpp_noexcept", "cpp_no_fieldwise_ctor", "cpp_trait_member_dispatch", + "cpp_value_init", ]; /// Reject `#[cfg_attr(any(), )]` carriers whose payload names an @@ -4736,6 +4739,28 @@ fn qualify_relative_path(raw: &str, module_path: &[String]) -> String { mod tests { use super::*; + #[test] + fn cpp_value_init_emits_braces_only_for_marked_scalar_fields() { + let output = transpile( + r#" + pub struct Message { + #[cfg_attr(any(), cpp_value_init)] + pub term: u64, + pub untouched: i32, + #[cfg_attr(any(), cpp_value_init)] + pub acknowledged: bool, + } + "#, + None, + ) + .expect("valid cpp_value_init fields should transpile"); + + assert!(output.contains("uint64_t term{};"), "{output}"); + assert!(output.contains("int32_t untouched;"), "{output}"); + assert!(output.contains("bool acknowledged{};"), "{output}"); + assert!(!output.contains("int32_t untouched{};"), "{output}"); + } + fn cpp_default_argument_type_map() -> UserTypeMap { let mut type_map = UserTypeMap::default(); type_map.mappings.insert( diff --git a/transpiler/tests/cpp_value_init_codegen.rs b/transpiler/tests/cpp_value_init_codegen.rs new file mode 100644 index 00000000..0911e02d --- /dev/null +++ b/transpiler/tests/cpp_value_init_codegen.rs @@ -0,0 +1,176 @@ +//! Compile-and-run proof for `#[cfg_attr(any(), cpp_value_init)]`. +//! +//! The string assertions pin the narrow generated spelling. The C++ checks +//! prove that this spelling preserves the downstream ABI traits it exists for: +//! aggregate and positional initialization, layout, trivial copying, and +//! scalar zeroing under plain default construction. + +use std::env; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +fn find_clang() -> Option { + if let Ok(cxx) = env::var("CXX") { + if !cxx.trim().is_empty() { + return Some(cxx); + } + } + for candidate in ["clang++", "clang++-22", "clang++-21"] { + if Command::new(candidate) + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok() + { + return Some(candidate.to_string()); + } + } + None +} + +fn project_include_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("include") +} + +#[test] +fn cpp_value_init_generated_scalars_preserve_aggregate_layout_and_defaults() { + let Some(compiler) = find_clang() else { + eprintln!("skipping cpp_value_init compile test: no clang++ in PATH or CXX"); + return; + }; + let temp = tempfile::tempdir().expect("create temp dir"); + let rust_path = temp.path().join("message.rs"); + let cpp_path = temp.path().join("message.cpp"); + let bin_path = temp.path().join("message.bin"); + std::fs::write( + &rust_path, + r#" +pub struct Message { + #[cfg_attr(any(), cpp_value_init)] + pub term: u64, + #[cfg_attr(any(), cpp_value_init)] + pub voter: u16, + #[cfg_attr(any(), cpp_value_init)] + pub acknowledged: bool, + pub untouched: i32, +} +"#, + ) + .expect("write Rust source"); + + let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let rust_metadata_path = temp.path().join("libmessage.rmeta"); + let rust_compile = Command::new(rustc) + .arg("--crate-type=lib") + .arg("--edition=2021") + .arg("-Dwarnings") + .arg("--emit=metadata") + .arg("-o") + .arg(&rust_metadata_path) + .arg(&rust_path) + .output() + .expect("invoke rustc"); + assert!( + rust_compile.status.success(), + "cpp_value_init fixture is not warning-clean Rust\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&rust_compile.stdout), + String::from_utf8_lossy(&rust_compile.stderr) + ); + + let transpile = Command::new(env!("CARGO_BIN_EXE_rusty-cpp-transpiler")) + .arg(&rust_path) + .arg("-o") + .arg(&cpp_path) + .output() + .expect("invoke transpiler"); + assert!( + transpile.status.success(), + "transpile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&transpile.stdout), + String::from_utf8_lossy(&transpile.stderr) + ); + + let mut cpp = std::fs::read_to_string(&cpp_path).expect("read generated C++"); + assert!(cpp.contains("uint64_t term{};"), "{cpp}"); + assert!(cpp.contains("uint16_t voter{};"), "{cpp}"); + assert!(cpp.contains("bool acknowledged{};"), "{cpp}"); + assert!(cpp.contains("int32_t untouched;"), "{cpp}"); + assert!(!cpp.contains("int32_t untouched{};"), "{cpp}"); + cpp.push_str( + r#" +#include +#include +#include +#include + +struct LegacyMessage { + uint64_t term{}; + uint16_t voter{}; + bool acknowledged{}; + int32_t untouched; +}; + +static_assert(std::is_aggregate_v); +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); +static_assert(!std::is_trivially_default_constructible_v); +static_assert(sizeof(Message) == sizeof(LegacyMessage)); +static_assert(alignof(Message) == alignof(LegacyMessage)); +static_assert(offsetof(Message, term) == offsetof(LegacyMessage, term)); +static_assert(offsetof(Message, voter) == offsetof(LegacyMessage, voter)); +static_assert(offsetof(Message, acknowledged) == offsetof(LegacyMessage, acknowledged)); +static_assert(offsetof(Message, untouched) == offsetof(LegacyMessage, untouched)); + +int main() { + Message plain; + assert(plain.term == 0); + assert(plain.voter == 0); + assert(!plain.acknowledged); + + Message value{}; + assert(value.term == 0); + assert(value.voter == 0); + assert(!value.acknowledged); + assert(value.untouched == 0); + + Message positional{11, 12, true, 13}; + assert(positional.term == 11); + assert(positional.voter == 12); + assert(positional.acknowledged); + assert(positional.untouched == 13); + return 0; +} +"#, + ); + std::fs::write(&cpp_path, cpp).expect("append C++ assertions"); + + let compile = Command::new(&compiler) + .arg("-std=c++23") + .arg("-DRUSTY_PORTABLE_INTRINSICS=1") + .arg("-I") + .arg(project_include_dir()) + .arg(&cpp_path) + .arg("-o") + .arg(&bin_path) + .output() + .expect("invoke clang++"); + assert!( + compile.status.success(), + "generated cpp_value_init fixture did not compile\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&bin_path) + .output() + .expect("run compiled fixture"); + assert!( + run.status.success(), + "generated cpp_value_init fixture failed at runtime\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); +}