use crate::backends::ffi::type_map::{c_param_type_with_paths_and_enums, scalar_c_abi_named_types};
use crate::core::ir::{ApiSurface, EnumDef, EnumVariant, FunctionDef, ParamDef, PrimitiveType, TypeDef, TypeRef};
use ahash::{AHashMap, AHashSet};
use std::collections::{HashMap, HashSet};
#[derive(Debug, PartialEq, Eq)]
enum Width {
Bits(u32),
Pointer,
}
type ScalarShape = Option<(Width, bool)>;
const HANDLE_BITS: u32 = 64;
const DISCRIMINANT_BITS: u32 = 32;
fn c_scalar_shape(c_type: &str) -> ScalarShape {
match c_type {
"AlefHandle" | "u64" => Some((Width::Bits(HANDLE_BITS), false)),
"i64" => Some((Width::Bits(HANDLE_BITS), true)),
"u32" => Some((Width::Bits(32), false)),
"i32" => Some((Width::Bits(DISCRIMINANT_BITS), true)),
"u16" => Some((Width::Bits(16), false)),
"i16" => Some((Width::Bits(16), true)),
"u8" => Some((Width::Bits(8), false)),
"i8" => Some((Width::Bits(8), true)),
"usize" => Some((Width::Pointer, false)),
"isize" => Some((Width::Pointer, true)),
_ => None,
}
}
fn csharp_scalar_shape(csharp_type: &str) -> ScalarShape {
match csharp_type {
"ulong" => Some((Width::Bits(HANDLE_BITS), false)),
"long" => Some((Width::Bits(HANDLE_BITS), true)),
"uint" => Some((Width::Bits(32), false)),
"int" => Some((Width::Bits(DISCRIMINANT_BITS), true)),
"ushort" => Some((Width::Bits(16), false)),
"short" => Some((Width::Bits(16), true)),
"byte" => Some((Width::Bits(8), false)),
"sbyte" => Some((Width::Bits(8), true)),
"nuint" => Some((Width::Pointer, false)),
"nint" => Some((Width::Pointer, true)),
"bool" => Some((Width::Bits(8), false)),
_ => None,
}
}
fn scalar_enum(name: &str) -> EnumDef {
EnumDef {
name: name.to_string(),
rust_path: format!("sample_core::{name}"),
is_copy: true,
variants: vec![
EnumVariant {
name: "First".to_string(),
..Default::default()
},
EnumVariant {
name: "Second".to_string(),
..Default::default()
},
],
..Default::default()
}
}
fn record_type(name: &str) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("sample_core::{name}"),
..Default::default()
}
}
fn param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
..Default::default()
}
}
fn optional_param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
optional: true,
..param(name, ty)
}
}
fn fixture() -> ApiSurface {
ApiSurface {
crate_name: "sample_core".to_string(),
enums: vec![scalar_enum("Mode")],
types: vec![record_type("Settings")],
functions: vec![FunctionDef {
name: "build_report".to_string(),
rust_path: "sample_core::build_report".to_string(),
params: vec![
param("mode", TypeRef::Named("Mode".to_string())),
optional_param("fallback_mode", TypeRef::Named("Mode".to_string())),
param("settings", TypeRef::Named("Settings".to_string())),
param("depth", TypeRef::Primitive(PrimitiveType::U64)),
param("verbose", TypeRef::Primitive(PrimitiveType::Bool)),
],
return_type: TypeRef::Named("Settings".to_string()),
..Default::default()
}],
..Default::default()
}
}
fn declared_param_types(declaration: &str) -> Vec<String> {
const SIGNATURE_MARKER: &str = "internal static extern ";
let Some((_, signature)) = declaration.split_once(SIGNATURE_MARKER) else {
return Vec::new();
};
let Some((_, after_open)) = signature.split_once('(') else {
return Vec::new();
};
let Some((inside, _)) = after_open.rsplit_once(')') else {
return Vec::new();
};
inside
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(|entry| {
let without_attribute = entry.rsplit_once(']').map_or(entry, |(_, rest)| rest).trim();
without_attribute
.rsplit_once(char::is_whitespace)
.map_or(without_attribute, |(ty, _)| ty)
.trim()
.to_string()
})
.collect()
}
fn csharp_declaration(api: &ApiSurface) -> String {
super::functions::gen_pinvoke_for_func(
"sample_ffi_build_report",
&api.functions[0],
&HashSet::new(),
&HashSet::new(),
&HashMap::new(),
&scalar_c_abi_named_types(api),
)
}
fn c_declaration_types(api: &ApiSurface) -> Vec<String> {
let scalar_named_types = scalar_c_abi_named_types(api);
let path_map = AHashMap::new();
api.functions[0]
.params
.iter()
.map(|parameter| {
c_param_type_with_paths_and_enums(
¶meter.ty,
&api.crate_name,
&path_map,
&scalar_named_types,
parameter.is_mut,
)
.into_owned()
})
.collect()
}
#[test]
fn should_declare_every_scalar_pinvoke_param_at_the_width_the_c_signature_uses() {
let api = fixture();
let csharp_types = declared_param_types(&csharp_declaration(&api));
let c_types = c_declaration_types(&api);
assert_eq!(
csharp_types.len(),
5,
"expected five declared C# params, got {csharp_types:?}"
);
assert_eq!(c_types.len(), 5, "expected five C param types, got {c_types:?}");
for (index, (csharp_type, c_type)) in csharp_types.iter().zip(c_types.iter()).enumerate() {
let c_shape = c_scalar_shape(c_type);
if c_shape.is_none() {
continue;
}
assert_eq!(
csharp_scalar_shape(csharp_type),
c_shape,
"param {index}: C# declares `{csharp_type}` but the C signature declares `{c_type}`",
);
}
}
#[test]
fn should_declare_a_copy_enum_param_as_the_c_discriminant_not_an_alef_handle() {
let api = fixture();
let csharp_types = declared_param_types(&csharp_declaration(&api));
let c_types = c_declaration_types(&api);
assert_eq!(
c_types[0], "i32",
"fixture precondition: the C signature must use a discriminant"
);
assert_eq!(
csharp_scalar_shape(&csharp_types[0]),
Some((Width::Bits(DISCRIMINANT_BITS), true))
);
assert_eq!(
csharp_scalar_shape(&csharp_types[1]),
Some((Width::Bits(DISCRIMINANT_BITS), true))
);
}
#[test]
fn should_still_declare_a_json_backed_record_param_as_an_alef_handle() {
let api = fixture();
let csharp_types = declared_param_types(&csharp_declaration(&api));
let c_types = c_declaration_types(&api);
assert_eq!(c_types[2], "AlefHandle");
assert_eq!(
csharp_scalar_shape(&csharp_types[2]),
Some((Width::Bits(HANDLE_BITS), false))
);
}
#[test]
fn should_not_treat_a_non_copy_enum_as_a_c_abi_discriminant() {
let mut api = fixture();
api.enums[0].is_copy = false;
let scalar_named_types = scalar_c_abi_named_types(&api);
assert!(
!scalar_named_types.contains("Mode"),
"a non-Copy enum is boxed as a handle by the FFI backend, so it must not be a discriminant",
);
assert_eq!(c_declaration_types(&api)[0], "AlefHandle");
assert_eq!(
csharp_scalar_shape(&declared_param_types(&csharp_declaration(&api))[0]),
Some((Width::Bits(HANDLE_BITS), false)),
);
}
#[test]
fn should_build_the_scalar_named_type_set_from_copy_ness_not_enum_ness() {
let mut api = fixture();
api.types[0].is_copy = true;
let scalar_named_types: AHashSet<String> = scalar_c_abi_named_types(&api);
assert!(scalar_named_types.contains("Mode"));
assert!(scalar_named_types.contains("Settings"));
}
#[test]
fn should_declare_a_bool_param_at_the_c_int_width_not_a_one_byte_managed_bool() {
let api = fixture();
let csharp_types = declared_param_types(&csharp_declaration(&api));
let c_types = c_declaration_types(&api);
assert_eq!(
c_types[4], "i32",
"fixture precondition: a Rust bool crosses the C ABI as i32"
);
assert_eq!(csharp_scalar_shape(&csharp_types[4]), c_scalar_shape(&c_types[4]));
}
#[test]
fn should_pass_a_bool_argument_as_the_c_int_the_declaration_expects() {
let optional_bool = optional_param("verbose", TypeRef::Primitive(PrimitiveType::Bool));
let required = super::marshalling::native_call_arg(
&TypeRef::Primitive(PrimitiveType::Bool),
"verbose",
false,
&HashSet::new(),
);
let optional =
super::marshalling::native_call_arg(&optional_bool.ty, "verbose", optional_bool.optional, &HashSet::new());
assert_eq!(required, "(verbose ? 1 : 0)");
assert_eq!(optional, "((verbose ?? false) ? 1 : 0)");
}
#[test]
fn optional_return_pointerness_matches_the_ffi_crate() {
use crate::backends::csharp::gen_bindings::marshalling::pinvoke_return_type;
use crate::backends::ffi::type_map::c_return_type;
let shapes = vec![
TypeRef::Primitive(PrimitiveType::U64),
TypeRef::Primitive(PrimitiveType::I32),
TypeRef::Primitive(PrimitiveType::F64),
TypeRef::Primitive(PrimitiveType::Bool),
TypeRef::Duration,
TypeRef::String,
TypeRef::Path,
TypeRef::Json,
TypeRef::Bytes,
TypeRef::Named("Config".to_string()),
TypeRef::Vec(Box::new(TypeRef::String)),
TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::U64))),
TypeRef::Optional(Box::new(TypeRef::Duration)),
TypeRef::Optional(Box::new(TypeRef::String)),
];
for inner in shapes {
let optional = TypeRef::Optional(Box::new(inner.clone()));
let c_type = c_return_type(&optional, "core_crate");
let ffi_returns_pointer = c_type.contains('*');
let csharp_declares_pointer = pinvoke_return_type(&optional) == "IntPtr";
assert_eq!(
csharp_declares_pointer,
ffi_returns_pointer,
"Option<{inner:?}>: the FFI crate returns `{c_type}` but C# declares \
`{}`. A scalar declared as IntPtr is read as a pointer and freed.",
pinvoke_return_type(&optional)
);
}
}