use super::super::FfiBackend;
use super::super::functions::{
gen_free_function, gen_free_function_result_presence_wrapper, gen_method_result_presence_wrapper,
gen_method_wrapper,
};
use super::common::*;
use crate::core::backend::Backend;
use crate::core::ir::*;
fn optional_u64(name: &str, error_type: Option<&str>) -> FunctionDef {
FunctionDef {
name: name.to_string(),
rust_path: format!("sample_core::{name}"),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
error_type: error_type.map(str::to_string),
..FunctionDef::default()
}
}
#[test]
fn generated_bindings_emit_result_presence_companion_for_every_ambiguous_optional_return_only() {
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
functions: vec![
optional_u64("get_port", None),
optional_u64("checked_port", Some("String")),
],
types: vec![TypeDef {
name: "SampleConfig".to_string(),
rust_path: "my_lib::SampleConfig".to_string(),
is_clone: true,
methods: vec![
MethodDef {
name: "margin_fraction".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::F64))),
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
},
MethodDef {
name: "consume".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
receiver: Some(ReceiverKind::Owned),
..MethodDef::default()
},
MethodDef {
name: "label".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::String)),
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
},
],
..TypeDef::default()
}],
..ApiSurface::default()
};
let config = resolved_one(
r#"
[workspace]
languages = ["ffi"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "smp"
"#,
);
let backend = FfiBackend;
let lib = backend
.generate_bindings(&api, &config)
.unwrap()
.into_iter()
.find(|f| f.path.ends_with("lib.rs"))
.unwrap()
.content;
assert!(
lib.contains("fn smp_get_port_has_result(\n) -> i32"),
"infallible free function returning Option<u64> must get a presence companion, got:\n{lib}"
);
assert!(
lib.contains("i32::from(result.is_some())"),
"infallible presence companion must check result.is_some(), got:\n{lib}"
);
assert!(
lib.contains("fn smp_checked_port_has_result(\n) -> i32"),
"fallible free function returning Option<u64> must get a presence companion, got:\n{lib}"
);
assert!(
lib.contains("i32::from(val.is_some())"),
"fallible presence companion must check val.is_some() inside the Ok arm, got:\n{lib}"
);
assert!(
lib.contains("fn smp_sample_config_margin_fraction_has_result(\n this: AlefHandle) -> i32"),
"&self method returning Option<f64> must get a presence companion, got:\n{lib}"
);
assert!(
!lib.contains("smp_sample_config_consume_has_result"),
"owned-receiver method must NOT get a presence companion (second call would consume an \
already-removed handle), got:\n{lib}"
);
assert!(
!lib.contains("smp_sample_config_label_has_result"),
"String-returning method already has a real null on None and must NOT get a presence \
companion, got:\n{lib}"
);
}
#[test]
fn result_presence_companion_distinguishes_none_from_zero_valued_some_at_runtime() {
let port_present = FunctionDef {
name: "port_present".to_string(),
rust_path: "sample_core::port_present".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
..FunctionDef::default()
};
let port_absent = FunctionDef {
name: "port_absent".to_string(),
rust_path: "sample_core::port_absent".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
..FunctionDef::default()
};
let checked_present = FunctionDef {
name: "checked_present".to_string(),
rust_path: "sample_core::checked_present".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
error_type: Some("String".to_string()),
..FunctionDef::default()
};
let checked_absent = FunctionDef {
name: "checked_absent".to_string(),
rust_path: "sample_core::checked_absent".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
error_type: Some("String".to_string()),
..FunctionDef::default()
};
let checked_err = FunctionDef {
name: "checked_err".to_string(),
rust_path: "sample_core::checked_err".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
error_type: Some("String".to_string()),
..FunctionDef::default()
};
let functions = [
&port_present,
&port_absent,
&checked_present,
&checked_absent,
&checked_err,
];
let typ = TypeDef {
name: "SampleConfig".to_string(),
rust_path: "sample_core::SampleConfig".to_string(),
is_clone: true,
..TypeDef::default()
};
let margin_method = MethodDef {
name: "margin_fraction".to_string(),
return_type: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::F64))),
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
};
let empty_map = ahash::AHashMap::new();
let empty_set = ahash::AHashSet::new();
let mut generated = String::new();
for func in functions {
generated.push_str(&gen_free_function(
func,
"smp",
"sample_core",
&empty_map,
&empty_set,
&empty_set,
None,
false,
));
generated.push('\n');
if let Some(presence) =
gen_free_function_result_presence_wrapper(func, "smp", "sample_core", &empty_map, &empty_set)
{
generated.push_str(&presence);
generated.push('\n');
}
}
generated.push_str(&gen_method_wrapper(
&typ,
&margin_method,
"smp",
"sample_core",
&empty_map,
&empty_set,
&empty_set,
));
generated.push('\n');
let method_presence =
gen_method_result_presence_wrapper(&typ, &margin_method, "smp", "sample_core", &empty_map, &empty_set)
.expect("Option<f64> &self method must get a presence companion");
generated.push_str(&method_presence);
generated.push('\n');
let last_error = crate::backends::ffi::template_env::render(
"last_error.jinja",
minijinja::context! {
prefix => "smp",
builtin_prefix => "",
error_code_impls => Vec::<String>::new(),
has_error_code_impls => false,
taxonomy => Vec::<String>::new(),
no_error_code => 0,
conversion_error_code => 1,
unknown_error_code => 2,
panic_error_code => 3,
invalid_handle_error_code => 4,
},
);
let mut handle_registry =
crate::backends::ffi::template_env::render("handle_registry.rs.jinja", minijinja::context! {});
let serialized_start = handle_registry
.find("struct SerializedHandle")
.expect("serialized helper start");
let core_registry_resume = handle_registry[serialized_start..]
.find("fn with_handle")
.map(|offset| serialized_start + offset)
.expect("core registry helpers resume");
handle_registry.replace_range(serialized_start..core_registry_resume, "");
let source = format!(
r#"
use std::cell::RefCell;
use std::ffi::{{c_char, CString}};
mod sample_core {{
#[derive(Clone, Default)]
pub struct SampleConfig {{
pub margin: Option<f64>,
}}
impl SampleConfig {{
pub fn margin_fraction(&self) -> Option<f64> {{
self.margin
}}
}}
pub fn port_present() -> Option<u64> {{ Some(0) }}
pub fn port_absent() -> Option<u64> {{ None }}
pub fn checked_present() -> Result<Option<u64>, String> {{ Ok(Some(0)) }}
pub fn checked_absent() -> Result<Option<u64>, String> {{ Ok(None) }}
pub fn checked_err() -> Result<Option<u64>, String> {{ Err("boom".to_string()) }}
}}
{last_error}
{handle_registry}
{generated}
fn main() {{
unsafe {{
// Direction 1: the getter alone cannot tell `None` from `Some(zero)` for either codegen
// path -- both collapse to the same sentinel. Asserted first so a future regression that
// removes the presence companion's reason to exist is still caught.
assert_eq!(smp_port_present(), 0);
assert_eq!(smp_port_absent(), 0);
assert_eq!(smp_checked_present(), 0);
assert_eq!(smp_checked_absent(), 0);
let absent = insert_handle(sample_core::SampleConfig::default()).expect("insert absent");
let present_zero = insert_handle(sample_core::SampleConfig {{ margin: Some(0.0) }})
.expect("insert present-zero");
assert_eq!(smp_sample_config_margin_fraction(absent), 0.0);
assert_eq!(smp_sample_config_margin_fraction(present_zero), 0.0);
// Direction 2: the presence companion distinguishes them, for both the free-function and
// the method codegen path, and for both the infallible and fallible shape.
assert_eq!(smp_port_present_has_result(), 1);
assert_eq!(smp_port_absent_has_result(), 0);
assert_eq!(smp_checked_present_has_result(), 1);
assert_eq!(smp_checked_absent_has_result(), 0);
assert_eq!(smp_checked_err_has_result(), -1);
assert_eq!(smp_sample_config_margin_fraction_has_result(absent), 0);
assert_eq!(smp_sample_config_margin_fraction_has_result(present_zero), 1);
// An invalid handle reports -1 on the presence channel too, distinct from both 0 and 1.
assert_eq!(smp_sample_config_margin_fraction_has_result(0), -1);
}}
}}
"#
);
let directory = tempfile::tempdir().expect("temporary directory");
let source_path = directory.path().join("result_presence.rs");
let binary_path = directory.path().join("result-presence-test");
std::fs::write(&source_path, &source).expect("write compile harness");
let compile = std::process::Command::new("rustc")
.current_dir(directory.path())
.args(["--edition=2024", "-o"])
.arg(&binary_path)
.arg(&source_path)
.output()
.expect("run rustc");
assert!(
compile.status.success(),
"{}\n---source---\n{source}",
String::from_utf8_lossy(&compile.stderr)
);
let run = std::process::Command::new(&binary_path)
.current_dir(directory.path())
.output()
.expect("run compiled harness");
assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr));
}