use crate::codegen::c_consumer;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
pub(crate) fn cgo_feature_cflags(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
cgo_feature_macros(api, config)
.iter()
.map(|macro_name| format!("-D{macro_name}=1"))
.collect::<Vec<_>>()
.join(" ")
}
pub(crate) fn cgo_feature_macros(api: &ApiSurface, config: &ResolvedCrateConfig) -> Vec<String> {
let ffi_prefix = config.ffi_prefix();
let mut macros: Vec<String> = crate::codegen::cfg::effective_ffi_default_features(api, config)
.iter()
.map(|feature| guard_macro_name(&ffi_prefix, feature))
.collect();
macros.sort_unstable();
macros.dedup();
macros
}
pub(crate) fn guard_macro_name(ffi_prefix: &str, feature: &str) -> String {
feature_macro_name(&c_consumer::export_type_prefix(ffi_prefix), feature)
}
fn feature_macro_name(prefix_upper: &str, feature: &str) -> String {
let suffix: String = feature
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() {
character.to_ascii_uppercase()
} else {
'_'
}
})
.collect();
format!("{prefix_upper}_FEATURE_{suffix}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::backend::Backend as _;
use crate::core::config::{Language, NewAlefConfig};
use crate::core::ir::FunctionDef;
use std::collections::BTreeSet;
fn resolved(toml: &str) -> ResolvedCrateConfig {
let config: NewAlefConfig = toml::from_str(toml).unwrap();
config.resolve().unwrap().remove(0)
}
fn api_with_gated_functions(gates: &[(&str, Option<&str>)]) -> ApiSurface {
let mut api = ApiSurface {
crate_name: "ts_pack".to_string(),
version: "0.1.0".to_string(),
..ApiSurface::default()
};
api.functions = gates
.iter()
.map(|(name, cfg)| FunctionDef {
name: (*name).to_string(),
rust_path: format!("ts_pack::{name}"),
cfg: cfg.map(str::to_string),
..FunctionDef::default()
})
.collect();
api
}
fn config_toml(ffi_features: &str) -> String {
format!(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "ts-pack"
sources = ["src/lib.rs"]
features = ["full"]
[crates.ffi]
prefix = "ts_pack"
{ffi_features}
[crates.go]
module = "github.com/test/ts-pack"
"#
)
}
#[test]
fn cgo_feature_macros_are_spelled_the_way_cbindgen_defines_them() {
let config = resolved(&config_toml(""));
let api = api_with_gated_functions(&[
("ping", None),
("download", Some(r#"feature = "download""#)),
("render", Some(r#"feature = "document-render""#)),
]);
let files = crate::backends::ffi::FfiBackend
.generate_bindings(&api, &config)
.expect("generate the FFI backend files");
let cbindgen = files
.iter()
.find(|file| file.path.to_string_lossy().ends_with("cbindgen.toml"))
.expect("cbindgen.toml is generated");
let parsed: toml::Value = toml::from_str(&cbindgen.content).expect("cbindgen.toml is valid TOML");
let guard_macros: Vec<&str> = parsed["defines"]
.as_table()
.expect("[defines] is a table")
.values()
.filter_map(toml::Value::as_str)
.collect();
assert!(
guard_macros.contains(&"TS_PACK_FEATURE_DOWNLOAD"),
"control: cbindgen must be told to guard the gated export, got: {guard_macros:?}"
);
let emitted = cgo_feature_macros(&api, &config);
let cfg_referenced = crate::codegen::cfg::collect_cfg_features(&api);
assert!(
!cfg_referenced.is_empty(),
"control: the fixture must reference at least one feature from a cfg gate"
);
for feature in &cfg_referenced {
let macro_name = feature_macro_name("TS_PACK", feature);
assert!(
guard_macros.contains(¯o_name.as_str()),
"cbindgen guards `{feature}` with a macro other than {macro_name}; defines are {guard_macros:?}"
);
assert!(
emitted.contains(¯o_name),
"the cgo preamble never defines {macro_name}, so every declaration cbindgen guards \
with it is deleted before cgo sees it; emitted: {emitted:?}"
);
}
}
#[test]
fn cgo_feature_cflags_covers_features_discovered_only_from_cfg_gates() {
let config = resolved(&config_toml(""));
let api = api_with_gated_functions(&[
("ping", None),
("download", Some(r#"feature = "download""#)),
("render", Some(r#"feature = "document-render""#)),
]);
assert_eq!(
cgo_feature_cflags(&api, &config),
"-DTS_PACK_FEATURE_DOCUMENT_RENDER=1 -DTS_PACK_FEATURE_DOWNLOAD=1 -DTS_PACK_FEATURE_FULL=1"
);
}
#[test]
fn cgo_feature_macros_omit_declare_only_extra_features() {
let config = resolved(&config_toml(r#"extra_features = ["wasm-http"]"#));
let api = api_with_gated_functions(&[
("fetch_native", Some(r#"feature = "native-http""#)),
("fetch_wasm", Some(r#"feature = "wasm-http""#)),
]);
let macros = cgo_feature_macros(&api, &config);
assert!(
macros.contains(&"TS_PACK_FEATURE_NATIVE_HTTP".to_string()),
"auto-discovered cfg features default ON in the FFI crate, got: {macros:?}"
);
assert!(
!macros.contains(&"TS_PACK_FEATURE_WASM_HTTP".to_string()),
"`extra_features` are declare-only and stay OFF, so their guards must stay false: {macros:?}"
);
}
#[test]
fn the_emitted_cgo_preamble_defines_exactly_the_effective_ffi_default_features() {
let config = resolved(&config_toml(r#"extra_features = ["wasm-http"]"#));
let api = api_with_gated_functions(&[
("ping", None),
("download", Some(r#"feature = "download""#)),
("render", Some(r#"feature = "document-render""#)),
("fetch_wasm", Some(r#"feature = "wasm-http""#)),
]);
let effective = crate::codegen::cfg::effective_ffi_default_features(&api, &config);
assert!(
effective.contains(&"full".to_string()),
"control: the fixture must exercise a configured passthrough feature, got: {effective:?}"
);
assert!(
effective.contains(&"download".to_string()),
"control: the fixture must exercise a feature discovered only from a cfg gate, got: {effective:?}"
);
assert!(
!effective.contains(&"wasm-http".to_string()),
"control: the fixture must exercise a declare-only extra feature that stays OFF, got: {effective:?}"
);
let ffi_prefix = config.ffi_prefix();
let expected: BTreeSet<String> = effective
.iter()
.map(|feature| guard_macro_name(&ffi_prefix, feature))
.collect();
let files = crate::backends::go::GoBackend
.generate_bindings(&api, &config)
.expect("generate the Go backend files");
let defined: BTreeSet<String> = files
.iter()
.flat_map(|file| file.content.split_whitespace())
.filter_map(|token| token.strip_prefix("-D"))
.filter_map(|token| token.strip_suffix("=1"))
.map(str::to_string)
.collect();
assert_eq!(
defined, expected,
"the cgo preamble's -D set no longer matches codegen::cfg::effective_ffi_default_features; \
a macro missing here deletes the guarded declaration before cgo sees it, and an extra one \
resurrects a declaration the shipped cdylib never exported"
);
}
#[test]
fn cgo_feature_macros_match_the_generated_ffi_cargo_toml() {
let config = resolved(&config_toml(r#"extra_features = ["wasm-http"]"#));
let api = api_with_gated_functions(&[
("ping", None),
("download", Some(r#"feature = "download""#)),
("render", Some(r#"feature = "document-render""#)),
("fetch_wasm", Some(r#"feature = "wasm-http""#)),
]);
let files = crate::scaffold::scaffold(&api, &config, &[Language::Ffi]).expect("scaffold the FFI crate");
let manifest = files
.iter()
.find(|file| file.path.to_string_lossy().ends_with("-ffi/Cargo.toml"))
.expect("the FFI crate manifest is scaffolded");
let parsed: toml::Value = toml::from_str(&manifest.content).expect("scaffolded manifest is valid TOML");
let scaffolded: Vec<String> = parsed["features"]["default"]
.as_array()
.expect("[features] default is an array")
.iter()
.map(|value| value.as_str().expect("feature names are strings").to_string())
.collect();
assert!(
scaffolded.contains(&"download".to_string()),
"control: the scaffolder must default a cfg-discovered feature ON, got: {scaffolded:?}"
);
let ffi_prefix = config.ffi_prefix();
let expected: BTreeSet<String> = scaffolded
.iter()
.map(|feature| guard_macro_name(&ffi_prefix, feature))
.collect();
let derived: BTreeSet<String> = cgo_feature_macros(&api, &config).into_iter().collect();
assert_eq!(
derived, expected,
"the cgo -D set is derived from a different feature list than the one the FFI crate is built with"
);
}
}