Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cxx-qt-gen: add support for passing in CfgEvaluator #1144

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .github/workflows/github-cxx-qt-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,22 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Note we need to match the LLVM and Rust versions
#
# See versions from the table in this link
# https://github.com/taiki-e/cargo-llvm-cov?tab=readme-ov-file#get-coverage-of-cc-code-linked-to-rust-librarybinary
- name: Install llvm 17
run: |
sudo apt-get update && sudo apt-get install -y llvm-17
test -d /usr/lib/llvm-17/bin/
- name: Setup toolchain
run: |
# Note that the llvm version needs to match, see the link above
rustup default 1.77.2
cargo install grcov
rustup component add llvm-tools rustfmt
rustup component add rustfmt
# Ensure we do not have any existing coverage files
- run: rm -f coverage/*.profraw
- name: build
env:
RUSTFLAGS: -Cinstrument-coverage
Expand All @@ -87,7 +98,7 @@ jobs:
LLVM_PROFILE_FILE: coverage/coverage_data-%p-%m.profraw
run: cargo test --lib --package cxx-qt-gen
- name: generate-report
run: grcov . -s . --binary-path ./target/debug/ -t lcov --branch --ignore-not-existing -o ./target/debug/lcov.info --excl-start CODECOV_EXCLUDE_START --excl-stop CODECOV_EXCLUDE_STOP
run: grcov . -s . --binary-path ./target/debug/ -t lcov --branch --ignore-not-existing --llvm --llvm-path /usr/lib/llvm-17/bin/ -o ./target/debug/lcov.info --excl-start CODECOV_EXCLUDE_START --excl-stop CODECOV_EXCLUDE_STOP
- name: upload-report
uses: codecov/codecov-action@v5
with:
Expand All @@ -96,6 +107,13 @@ jobs:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
- name: Upload GitHub Actions artifacts of lcov
if: always()
uses: actions/upload-artifact@v4
with:
name: lcov
path: ./target/debug/lcov.info
if-no-files-found: ignore

build-wasm:
name: Ubuntu 24.04 (wasm_32) Qt 6
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `QDateTime::from_string` to parse `QDateTime` from a `QString`.
- Support for further types: `QUuid`
- New example: Basic greeter app
- Support for `cfg` attributes through to C++ generation

### Fixed

Expand Down
7 changes: 5 additions & 2 deletions crates/cxx-qt-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use std::{
};

use cxx_qt_gen::{
parse_qt_file, write_cpp, write_rust, CppFragment, CxxQtItem, GeneratedCppBlocks,
parse_qt_file, write_cpp, write_rust, CppFragment, CxxQtItem, GeneratedCppBlocks, GeneratedOpt,
GeneratedRustBlocks, Parser,
};

Expand Down Expand Up @@ -103,6 +103,9 @@ impl GeneratedCpp {
// The include path we inject needs any prefix (eg the crate name) too
let include_ident = format!("{include_prefix}/{file_ident}");

let mut cxx_qt_opt = GeneratedOpt::default();
cxx_qt_opt.cfg_evaluator = Box::new(cfg_evaluator::CargoEnvCfgEvaluator);

// Loop through the items looking for any CXX or CXX-Qt blocks
let mut found_bridge = false;
for item in &file.items {
Expand Down Expand Up @@ -132,7 +135,7 @@ impl GeneratedCpp {
let parser = Parser::from(m.clone())
.map_err(GeneratedError::from)
.map_err(to_diagnostic)?;
let generated_cpp = GeneratedCppBlocks::from(&parser)
let generated_cpp = GeneratedCppBlocks::from(&parser, &cxx_qt_opt)
.map_err(GeneratedError::from)
.map_err(to_diagnostic)?;
let generated_rust = GeneratedRustBlocks::from(&parser)
Expand Down
1 change: 1 addition & 0 deletions crates/cxx-qt-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ exclude = ["update_expected.sh"]
rust-version = "1.64.0"

[dependencies]
cxx-gen.workspace = true
proc-macro2.workspace = true
syn.workspace = true
quote.workspace = true
Expand Down
222 changes: 222 additions & 0 deletions crates/cxx-qt-gen/src/generator/cfg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: CXX Authors
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
// SPDX-FileContributor: David Tolnay <[email protected]>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::syntax::cfg::{parse_attribute, CfgExpr};
use cxx_gen::{CfgEvaluator, CfgResult};
use quote::quote;
use syn::{Attribute, Error, LitStr};

pub(crate) fn try_eval_attributes(
cfg_evaluator: &dyn CfgEvaluator,
attrs: &[Attribute],
) -> Result<bool, Error> {
// Build a single CfgExpr from the Attributes
let cfg_expr = attrs
.iter()
.map(parse_attribute)
.collect::<Result<Vec<CfgExpr>, Error>>()?
.into_iter()
.reduce(|mut acc, e| {
acc.merge(e);
acc
});

// Evaluate the CfgExpr against the CfgEvaluator
if let Some(cfg_expr) = cfg_expr {
try_eval(cfg_evaluator, &cfg_expr).map_err(|errs| {
errs.into_iter()
.reduce(|mut acc, e| {
acc.combine(e);
acc
})
.expect("There should be at least one error")
})
} else {
Ok(true)
}
}

fn try_eval(cfg_evaluator: &dyn CfgEvaluator, expr: &CfgExpr) -> Result<bool, Vec<Error>> {
match expr {
CfgExpr::Unconditional => Ok(true),
CfgExpr::Eq(ident, string) => {
let key = ident.to_string();
let value = string.as_ref().map(LitStr::value);
match cfg_evaluator.eval(&key, value.as_deref()) {
CfgResult::True => Ok(true),
CfgResult::False => Ok(false),
CfgResult::Undetermined { msg } => {
let span = quote!(#ident #string);
Err(vec![Error::new_spanned(span, msg)])
}
}
}
CfgExpr::All(list) => {
let mut all_errors = Vec::new();
for subexpr in list {
match try_eval(cfg_evaluator, subexpr) {
Ok(true) => {}
Ok(false) => return Ok(false),
Err(errors) => all_errors.extend(errors),
}
}
if all_errors.is_empty() {
Ok(true)
} else {
Err(all_errors)
}
}
CfgExpr::Any(list) => {
let mut all_errors = Vec::new();
for subexpr in list {
match try_eval(cfg_evaluator, subexpr) {
Ok(true) => return Ok(true),
Ok(false) => {}
Err(errors) => all_errors.extend(errors),
}
}
if all_errors.is_empty() {
Ok(false)
} else {
Err(all_errors)
}
}
CfgExpr::Not(subexpr) => match try_eval(cfg_evaluator, subexpr) {
Ok(value) => Ok(!value),
Err(errors) => Err(errors),
},
}
}

#[cfg(test)]
mod tests {
use super::*;

use crate::generator::UnsupportedCfgEvaluator;
use std::collections::HashMap;
use syn::{parse_quote, ItemMod};

#[derive(Default)]
struct CfgEvaluatorTest<'a> {
cfgs: HashMap<&'a str, Option<&'a str>>,
}

impl<'a> CfgEvaluator for CfgEvaluatorTest<'a> {
fn eval(&self, name: &str, query_value: Option<&str>) -> CfgResult {
if self.cfgs.get(name) == Some(&query_value) {
CfgResult::True
} else {
CfgResult::False
}
}
}

#[test]
fn test_try_eval_attributes_eq() {
let module: ItemMod = parse_quote! {
#[cfg(a = "1")]
#[cfg(b = "2")]
mod module;
};
let mut cfg_evaluator = Box::new(CfgEvaluatorTest::default());
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
false
);

// Insert cfg into map
cfg_evaluator.cfgs.insert("a", Some("1"));
cfg_evaluator.cfgs.insert("b", Some("2"));
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
true
);
}

#[test]
fn test_try_eval_attributes_any() {
let module: ItemMod = parse_quote! {
#[cfg(any(a = "1", b = "2"))]
mod module;
};
let mut cfg_evaluator = Box::new(CfgEvaluatorTest::default());
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
false
);

// Insert cfg into map
cfg_evaluator.cfgs.insert("a", Some("1"));
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
true
);
}

#[test]
fn test_try_eval_attributes_all() {
let module: ItemMod = parse_quote! {
#[cfg(all(a = "1", b = "2"))]
mod module;
};
let mut cfg_evaluator = Box::new(CfgEvaluatorTest::default());
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
false
);

// Insert cfg into map
cfg_evaluator.cfgs.insert("a", Some("1"));
cfg_evaluator.cfgs.insert("b", Some("2"));
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
true
);
}

#[test]
fn test_try_eval_attributes_not() {
let module: ItemMod = parse_quote! {
#[cfg(not(a = "1"))]
mod module;
};
let mut cfg_evaluator = Box::new(CfgEvaluatorTest::default());
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
true
);

// Insert cfg into map
cfg_evaluator.cfgs.insert("a", Some("1"));
assert_eq!(
try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs).unwrap(),
false
);
}

#[test]
fn test_try_eval_unconditional() {
let cfg_expr = CfgExpr::Unconditional;
let cfg_evaluator = Box::new(UnsupportedCfgEvaluator);
assert_eq!(try_eval(cfg_evaluator.as_ref(), &cfg_expr).unwrap(), true);
}

#[test]
fn test_try_eval_attributes_undetermined_err() {
let module: ItemMod = parse_quote! {
#[cfg(a = "1")]
#[cfg(all(a = "1", b = "2"))]
#[cfg(any(a = "1", b = "2"))]
#[cfg(not(a = "1"))]
mod module;
};
let cfg_evaluator = Box::new(UnsupportedCfgEvaluator);
assert!(try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs[0..1]).is_err());
assert!(try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs[1..2]).is_err());
assert!(try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs[2..3]).is_err());
assert!(try_eval_attributes(cfg_evaluator.as_ref(), &module.attrs[3..4]).is_err());
}
}
25 changes: 15 additions & 10 deletions crates/cxx-qt-gen/src/generator/cpp/externcxxqt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::{
generator::cpp::signal::generate_cpp_signal, naming::TypeNames,
parser::externcxxqt::ParsedExternCxxQt, CppFragment,
generator::{cpp::signal::generate_cpp_signal, GeneratedOpt},
naming::TypeNames,
parser::externcxxqt::ParsedExternCxxQt,
CppFragment,
};
use std::collections::BTreeSet;
use syn::Result;
Expand All @@ -23,18 +25,20 @@ pub struct GeneratedCppExternCxxQtBlocks {
pub fn generate(
blocks: &[ParsedExternCxxQt],
type_names: &TypeNames,
opt: &GeneratedOpt,
) -> Result<Vec<GeneratedCppExternCxxQtBlocks>> {
let mut out = vec![];

for block in blocks {
for signal in &block.signals {
let mut block = GeneratedCppExternCxxQtBlocks::default();
let qobject_name = type_names.lookup(&signal.qobject_ident)?;
let data = generate_cpp_signal(signal, qobject_name, type_names)?;
block.includes = data.includes;
block.forward_declares = data.forward_declares;
block.fragments = data.fragments;
let data = generate_cpp_signal(signal, qobject_name, type_names, opt)?;
debug_assert!(data.methods.is_empty());
let block = GeneratedCppExternCxxQtBlocks {
includes: data.includes,
forward_declares: data.forward_declares,
fragments: data.fragments,
};
out.push(block);
}
}
Expand Down Expand Up @@ -70,9 +74,10 @@ mod tests {
.unwrap()];

// Unknown types
assert!(generate(&blocks, &TypeNames::default()).is_err());
let opt = GeneratedOpt::default();
assert!(generate(&blocks, &TypeNames::default(), &opt).is_err());

let generated = generate(&blocks, &TypeNames::mock()).unwrap();
let generated = generate(&blocks, &TypeNames::mock(), &opt).unwrap();
assert_eq!(generated.len(), 2);
}

Expand All @@ -97,7 +102,7 @@ mod tests {
let mut type_names = TypeNames::default();
type_names.mock_insert("ObjRust", None, Some("ObjCpp"), Some("mynamespace"));

let generated = generate(&blocks, &type_names).unwrap();
let generated = generate(&blocks, &type_names, &GeneratedOpt::default()).unwrap();
assert_eq!(generated.len(), 1);
}
}
Loading
Loading