mod call_body;
mod helpers;
mod registration;
mod vtable;
use crate::codegen::generators::trait_bridge::{TraitBridgeSpec, gen_bridge_plugin_impl};
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{ApiSurface, TypeDef, TypeRef};
use heck::ToPascalCase;
use std::collections::HashMap;
use helpers::prim_to_c;
pub struct FfiBridgeGenerator {
pub prefix: String,
pub core_import: String,
pub type_paths: HashMap<String, String>,
pub error_type: String,
pub plugin_error_constructor: Option<String>,
}
impl FfiBridgeGenerator {
pub(super) fn vtable_name(&self, spec: &TraitBridgeSpec) -> String {
let pascal = self.prefix.to_pascal_case();
format!("{}{}VTable", pascal, spec.trait_def.name)
}
pub(super) fn bridge_name(&self, spec: &TraitBridgeSpec) -> String {
let pascal = self.prefix.to_pascal_case();
format!("{}{}Bridge", pascal, spec.trait_def.name)
}
pub(super) fn c_param_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "*const std::ffi::c_char".to_string(),
TypeRef::Bytes => "*const u8".to_string(),
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
"*const std::ffi::c_char".to_string()
}
TypeRef::Optional(inner) => {
match inner.as_ref() {
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
_ => "*const std::ffi::c_char".to_string(),
}
}
TypeRef::Unit => "()".to_string(),
TypeRef::Duration => "u64".to_string(),
}
}
pub(super) fn c_return_convention(ty: &TypeRef, has_error: bool) -> (Vec<String>, String) {
let needs_out_error = matches!(
ty,
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::String | TypeRef::Json
) || has_error;
let out_params = match ty {
TypeRef::Unit => {
if has_error {
vec!["out_error: *mut *mut std::ffi::c_char".to_string()]
} else {
vec![]
}
}
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
let mut v = vec!["out_result: *mut *mut std::ffi::c_char".to_string()];
if needs_out_error {
v.push("out_error: *mut *mut std::ffi::c_char".to_string());
}
v
}
TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
let mut v = vec!["out_result: *mut *mut std::ffi::c_char".to_string()];
if needs_out_error {
v.push("out_error: *mut *mut std::ffi::c_char".to_string());
}
v
}
_ => {
if has_error {
vec!["out_error: *mut *mut std::ffi::c_char".to_string()]
} else {
vec![]
}
}
};
let ret = if has_error || needs_out_error {
"i32".to_string()
} else {
match ty {
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
TypeRef::Unit => "()".to_string(),
TypeRef::Duration => "u64".to_string(),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Primitive(p) => prim_to_c(p).to_string(),
_ => "i32".to_string(), },
_ => "i32".to_string(),
}
};
(out_params, ret)
}
}
pub fn gen_ffi_set_out_error_helper() -> String {
crate::backends::ffi::template_env::render("ffi_set_out_error_helper.jinja", minijinja::context! {})
}
#[allow(clippy::too_many_arguments)]
pub fn gen_trait_bridge(
trait_type: &TypeDef,
bridge_cfg: &TraitBridgeConfig,
prefix: &str,
core_import: &str,
error_type: &str,
error_constructor: &str,
plugin_error_constructor: Option<&str>,
api: &ApiSurface,
) -> String {
let type_paths: HashMap<String, String> = api
.types
.iter()
.map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
.chain(
api.enums
.iter()
.map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
)
.chain(
api.excluded_type_paths
.iter()
.map(|(name, path)| (name.clone(), path.replace('-', "_"))),
)
.collect();
let generator = FfiBridgeGenerator {
prefix: prefix.to_string(),
core_import: core_import.to_string(),
type_paths: type_paths.clone(),
error_type: error_type.to_string(),
plugin_error_constructor: plugin_error_constructor.map(str::to_string),
};
let spec = TraitBridgeSpec {
trait_def: trait_type,
bridge_config: bridge_cfg,
core_import,
wrapper_prefix: &prefix.to_pascal_case(),
type_paths,
error_type: error_type.to_string(),
error_constructor: error_constructor.to_string(),
};
let mut out = String::with_capacity(4096);
out.push_str(&generator.gen_vtable_struct(&spec));
out.push('\n');
out.push_str(&generator.gen_bridge_struct(&spec));
out.push('\n');
out.push_str(&generator.gen_bridge_drop(&spec));
out.push('\n');
out.push_str(&generator.gen_constructor_impl(&spec));
out.push('\n');
if let Some(plugin_impl) = generator.gen_ffi_plugin_impl(&spec) {
out.push_str(&plugin_impl);
out.push('\n');
} else {
if let Some(plugin_impl) = gen_bridge_plugin_impl(&spec, &generator) {
out.push_str(&plugin_impl);
out.push('\n');
}
}
out.push_str(&generator.gen_ffi_trait_impl(&spec));
out.push('\n');
if spec.bridge_config.register_fn.is_some() {
out.push('\n');
out.push_str(&generator.gen_registration_fn_impl(&spec));
}
out
}
pub fn gen_bridge_new_free(prefix: &str, pascal_prefix: &str, trait_name: &str) -> String {
let bridge_name = format!("{pascal_prefix}{trait_name}Bridge");
let vtable_name = format!("{pascal_prefix}{trait_name}VTable");
let bridge_snake = to_snake_case(&bridge_name);
let fn_new = format!("{prefix}_{bridge_snake}_new");
let fn_free = format!("{prefix}_{bridge_snake}_free");
format!(
r#"/// Create a new `{bridge_name}` from a vtable and opaque user_data pointer.
///
/// Returns a heap-allocated `{bridge_name}` on success, or null if `vtable` is null.
/// The caller is responsible for calling `{fn_free}` exactly once when the bridge is
/// no longer needed.
///
/// # Safety
///
/// `vtable` must be a non-null pointer to a fully initialised `{vtable_name}` that
/// remains valid for the lifetime of the returned bridge. `user_data` must be valid
/// for any thread that calls methods on this bridge.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_new}(
vtable: *const {vtable_name},
user_data: *const std::ffi::c_void,
) -> *mut {bridge_name} {{
if vtable.is_null() {{
return std::ptr::null_mut();
}}
// SAFETY: vtable is non-null (checked above); caller guarantees it is valid for this call.
let bridge = unsafe {{ {bridge_name}::new(String::new(), *vtable, user_data) }};
Box::into_raw(Box::new(bridge))
}}
/// Free a `{bridge_name}` created by `{fn_new}`.
///
/// After this call `ptr` is invalid. Passing null is a no-op.
///
/// # Safety
///
/// `ptr` must be either null or a non-null pointer returned by `{fn_new}` that has
/// not yet been freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_free}(ptr: *mut {bridge_name}) {{
if !ptr.is_null() {{
// SAFETY: ptr is non-null and was created via Box::into_raw in {fn_new}.
drop(unsafe {{ Box::from_raw(ptr) }});
}}
}}"#,
)
}
fn to_snake_case(s: &str) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_ascii_uppercase() && i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::*;
fn make_trait_def(name: &str, methods: Vec<MethodDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("my_lib::{name}"),
original_rust_path: String::new(),
fields: vec![],
methods,
is_opaque: false,
is_clone: false,
is_copy: false,
is_trait: true,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
}
}
fn make_method(name: &str, return_type: TypeRef, has_error: bool, has_default: bool) -> MethodDef {
MethodDef {
name: name.to_string(),
params: vec![],
return_type,
is_async: false,
is_static: false,
error_type: if has_error {
Some("Box<dyn std::error::Error + Send + Sync>".to_string())
} else {
None
},
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: has_default,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
fn sample_api() -> ApiSurface {
ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
}
}
fn sample_bridge_cfg(trait_name: &str) -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: trait_name.to_string(),
super_trait: None,
registry_getter: None,
register_fn: None,
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: Vec::new(),
bind_via: crate::core::config::BridgeBinding::FunctionParam,
options_type: None,
options_field: None,
context_type: None,
result_type: None,
ffi_skip_methods: Vec::new(),
}
}
#[test]
fn test_vtable_struct_is_repr_c() {
let trait_def = make_trait_def("OcrBackend", vec![make_method("process", TypeRef::String, true, false)]);
let bridge_cfg = sample_bridge_cfg("OcrBackend");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(code.contains("#[repr(C)]"), "vtable must be #[repr(C)]");
assert!(
code.contains("MlOcrBackendVTable"),
"vtable name must include prefix + trait name"
);
}
#[test]
fn test_vtable_has_method_fn_ptrs() {
let trait_def = make_trait_def(
"OcrBackend",
vec![
make_method("process", TypeRef::String, true, false),
make_method("status", TypeRef::Primitive(PrimitiveType::I32), false, true),
],
);
let bridge_cfg = sample_bridge_cfg("OcrBackend");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(code.contains("pub process:"), "vtable must have fn ptr for 'process'");
assert!(code.contains("pub status:"), "vtable must have fn ptr for 'status'");
assert!(
code.contains("pub free_user_data:"),
"vtable must have free_user_data destructor"
);
}
#[test]
fn test_vtable_fn_ptrs_take_user_data() {
let trait_def = make_trait_def(
"Checker",
vec![make_method(
"ping",
TypeRef::Primitive(PrimitiveType::Bool),
false,
false,
)],
);
let bridge_cfg = sample_bridge_cfg("Checker");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"lib",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("user_data: *const std::ffi::c_void"),
"every vtable fn pointer must accept user_data as first param"
);
}
#[test]
fn test_bridge_struct_fields() {
let trait_def = make_trait_def("Runner", vec![make_method("run", TypeRef::Unit, true, false)]);
let bridge_cfg = sample_bridge_cfg("Runner");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"my_lib",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(code.contains("vtable: MyLibRunnerVTable"), "bridge must hold vtable");
assert!(
code.contains("user_data: *const std::ffi::c_void"),
"bridge must hold user_data"
);
assert!(code.contains("cached_name: String"), "bridge must hold cached_name");
}
#[test]
fn test_bridge_is_send_sync() {
let trait_def = make_trait_def("Worker", vec![make_method("work", TypeRef::Unit, false, false)]);
let bridge_cfg = sample_bridge_cfg("Worker");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"w",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("unsafe impl Send for WWorkerBridge"),
"bridge must be Send"
);
assert!(
code.contains("unsafe impl Sync for WWorkerBridge"),
"bridge must be Sync"
);
}
#[test]
fn test_bridge_has_drop_impl_for_free_user_data() {
let trait_def = make_trait_def("Plugin", vec![make_method("tick", TypeRef::Unit, false, false)]);
let bridge_cfg = sample_bridge_cfg("Plugin");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"p",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("impl Drop for PPluginBridge"),
"bridge must implement Drop"
);
assert!(code.contains("free_user_data"), "Drop impl must call free_user_data");
}
#[test]
fn test_super_trait_generates_plugin_impl() {
let trait_def = make_trait_def("OcrBackend", vec![make_method("process", TypeRef::String, true, false)]);
let bridge_cfg = TraitBridgeConfig {
trait_name: "OcrBackend".to_string(),
super_trait: Some("Plugin".to_string()),
registry_getter: None,
register_fn: None,
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: Vec::new(),
bind_via: crate::core::config::BridgeBinding::FunctionParam,
options_type: None,
options_field: None,
context_type: None,
result_type: None,
ffi_skip_methods: Vec::new(),
};
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"kr",
"sample_crate",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("impl sample_crate::Plugin for KrOcrBackendBridge"),
"must generate Plugin impl"
);
assert!(code.contains("fn name(&self)"), "Plugin impl must have name()");
assert!(code.contains("fn version(&self)"), "Plugin impl must have version()");
assert!(
code.contains("fn initialize(&self)"),
"Plugin impl must have initialize()"
);
assert!(code.contains("fn shutdown(&self)"), "Plugin impl must have shutdown()");
}
#[test]
fn test_register_fn_generates_extern_c() {
let trait_def = make_trait_def("OcrBackend", vec![make_method("process", TypeRef::String, true, false)]);
let bridge_cfg = TraitBridgeConfig {
trait_name: "OcrBackend".to_string(),
super_trait: None,
registry_getter: Some("sample_crate::registry::get_ocr".to_string()),
register_fn: Some("register_ocr_backend".to_string()),
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: Vec::new(),
bind_via: crate::core::config::BridgeBinding::FunctionParam,
options_type: None,
options_field: None,
context_type: None,
result_type: None,
ffi_skip_methods: Vec::new(),
};
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"kr",
"sample_crate",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("extern \"C\" fn kr_register_ocr_backend"),
"register fn must be extern C with correct name"
);
assert!(
code.contains("extern \"C\" fn kr_unregister_ocr_backend"),
"unregister fn must be extern C with correct name"
);
assert!(code.contains("#[unsafe(no_mangle)]"), "register fn must be no_mangle");
}
#[test]
fn test_register_fn_validates_name_null() {
let trait_def = make_trait_def("MyTrait", vec![make_method("do_thing", TypeRef::Unit, true, false)]);
let bridge_cfg = TraitBridgeConfig {
trait_name: "MyTrait".to_string(),
super_trait: None,
registry_getter: Some("my_lib::get_registry".to_string()),
register_fn: Some("register_my_trait".to_string()),
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: Vec::new(),
bind_via: crate::core::config::BridgeBinding::FunctionParam,
options_type: None,
options_field: None,
context_type: None,
result_type: None,
ffi_skip_methods: Vec::new(),
};
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("if name.is_null()"),
"register fn must check for null name"
);
}
#[test]
fn test_register_fn_validates_required_fn_ptrs() {
let trait_def = make_trait_def(
"Transform",
vec![
make_method("transform", TypeRef::String, true, false), make_method("describe", TypeRef::String, false, true), ],
);
let bridge_cfg = TraitBridgeConfig {
trait_name: "Transform".to_string(),
super_trait: None,
registry_getter: Some("my_lib::get_registry".to_string()),
register_fn: Some("register_transform".to_string()),
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: Vec::new(),
bind_via: crate::core::config::BridgeBinding::FunctionParam,
options_type: None,
options_field: None,
context_type: None,
result_type: None,
ffi_skip_methods: Vec::new(),
};
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("vtable.transform.is_none()"),
"required fn ptr must be validated non-null"
);
}
#[test]
fn test_safety_comments_present() {
let trait_def = make_trait_def("Processor", vec![make_method("run", TypeRef::String, true, false)]);
let bridge_cfg = TraitBridgeConfig {
trait_name: "Processor".to_string(),
super_trait: None,
registry_getter: Some("my_lib::get_registry".to_string()),
register_fn: Some("register_processor".to_string()),
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: Vec::new(),
bind_via: crate::core::config::BridgeBinding::FunctionParam,
options_type: None,
options_field: None,
context_type: None,
result_type: None,
ffi_skip_methods: Vec::new(),
};
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("// SAFETY:"),
"generated code must contain SAFETY comments"
);
assert!(
code.contains("unsafe"),
"generated code must use unsafe for raw pointer ops"
);
}
#[test]
fn test_trait_impl_generated() {
let trait_def = make_trait_def("Scanner", vec![make_method("scan", TypeRef::String, true, false)]);
let bridge_cfg = sample_bridge_cfg("Scanner");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"sc",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("impl my_lib::Scanner for ScScannerBridge"),
"must generate trait impl"
);
assert!(code.contains("fn scan("), "trait impl must contain the method");
}
#[test]
fn test_string_param_marshalled_to_c_char() {
let trait_def = make_trait_def(
"Greeter",
vec![MethodDef {
name: "greet".to_string(),
params: vec![ParamDef {
name: "message".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
}],
);
let bridge_cfg = sample_bridge_cfg("Greeter");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"g",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("*const std::ffi::c_char"),
"string param must map to *const c_char in vtable"
);
}
#[test]
fn test_c_param_type_mappings() {
assert_eq!(
FfiBridgeGenerator::c_param_type(&TypeRef::String),
"*const std::ffi::c_char"
);
assert_eq!(FfiBridgeGenerator::c_param_type(&TypeRef::Bytes), "*const u8");
assert_eq!(
FfiBridgeGenerator::c_param_type(&TypeRef::Primitive(PrimitiveType::Bool)),
"i32"
);
assert_eq!(FfiBridgeGenerator::c_param_type(&TypeRef::Duration), "u64");
}
#[test]
fn test_c_return_convention_unit_fallible() {
let (out_params, ret) = FfiBridgeGenerator::c_return_convention(&TypeRef::Unit, true);
assert_eq!(ret, "i32");
assert_eq!(out_params.len(), 1);
assert!(out_params[0].contains("out_error"));
}
#[test]
fn test_c_return_convention_string_infallible() {
let (out_params, ret) = FfiBridgeGenerator::c_return_convention(&TypeRef::String, false);
assert_eq!(out_params.len(), 2);
assert!(out_params[0].contains("out_result"));
assert!(out_params.iter().any(|p| p.contains("out_error")));
assert_eq!(ret, "i32");
}
#[test]
fn bug1_excluded_type_is_fully_qualified_in_trait_impl() {
let internal_doc_method = MethodDef {
name: "render".to_string(),
params: vec![crate::core::ir::ParamDef {
name: "doc".to_string(),
ty: TypeRef::Named("InternalDocument".to_string()),
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let trait_def = make_trait_def("Renderer", vec![internal_doc_method]);
let bridge_cfg = sample_bridge_cfg("Renderer");
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: {
let mut m = ::std::collections::HashMap::new();
m.insert(
"InternalDocument".to_string(),
"my_lib::internal::InternalDocument".to_string(),
);
m
},
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
};
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("&my_lib::internal::InternalDocument"),
"excluded type must be fully-qualified, not bare;\n\
actual code:\n{code}"
);
assert!(
!code.contains("&InternalDocument"),
"bare type reference must not appear in generated trait impl;\n\
actual code:\n{code}"
);
}
#[test]
fn bug2_sync_method_body_uses_trait_error_type_not_box_from() {
use crate::codegen::generators::trait_bridge::TraitBridgeSpec;
let method = MethodDef {
name: "run".to_string(),
params: vec![],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: Some("MyError".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let trait_def = make_trait_def("Backend", vec![method.clone()]);
let bridge_cfg = sample_bridge_cfg("Backend");
let spec = TraitBridgeSpec {
trait_def: &trait_def,
bridge_config: &bridge_cfg,
core_import: "my_lib",
wrapper_prefix: "Ml",
type_paths: ::std::collections::HashMap::new(),
error_type: "MyError".to_string(),
error_constructor: "MyError::from({msg})".to_string(),
};
let generator = FfiBridgeGenerator {
prefix: "ml".to_string(),
core_import: "my_lib".to_string(),
type_paths: ::std::collections::HashMap::new(),
error_type: "MyError".to_string(),
plugin_error_constructor: None,
};
let sync_body = generator.gen_vtable_call_body(&method, &spec, false);
assert!(
sync_body.contains("MyError::from("),
"sync method body must use the trait's error constructor;\n\
actual body:\n{sync_body}"
);
assert!(
!sync_body.contains("Err(Box::from("),
"sync method body must NOT use Box::from (that's for the async closure);\n\
actual body:\n{sync_body}"
);
let closure_body = generator.gen_vtable_call_body(&method, &spec, true);
assert!(
closure_body.contains("Err(Box::from("),
"async closure body must use Box::from;\n\
actual body:\n{closure_body}"
);
}
#[test]
fn bug3_returns_ref_vec_string_emits_slice_ref_and_cache_field() {
let method = MethodDef {
name: "supported_mime_types".to_string(),
params: vec![],
return_type: TypeRef::Vec(Box::new(TypeRef::String)),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: true, returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let trait_def = make_trait_def("DocumentExtractor", vec![method]);
let bridge_cfg = sample_bridge_cfg("DocumentExtractor");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"kr",
"sample_crate",
"SampleCrateError",
"SampleCrateError::from({msg})",
None,
&api,
);
assert!(
code.contains("fn supported_mime_types(&self) -> &[&str]"),
"returns_ref Vec<String> must produce &[&str] in trait impl;\n\
actual code:\n{code}"
);
assert!(
code.contains("supported_mime_types_strs: &'static [&'static str]"),
"bridge struct must have supported_mime_types_strs cache field;\n\
actual code:\n{code}"
);
assert!(
code.contains("self.supported_mime_types_strs"),
"trait impl body must return from the cached field;\n\
actual code:\n{code}"
);
assert!(
code.contains("Box::leak"),
"constructor must use Box::leak to build &'static [&'static str];\n\
actual code:\n{code}"
);
}
#[test]
fn bug4_ffi_skip_methods_opts_out_of_trait_impl() {
let required = make_method("run", TypeRef::String, true, false);
let optional = make_method("shutdown", TypeRef::Unit, false, true);
let trait_def = make_trait_def("Backend", vec![required, optional]);
let mut bridge_cfg = sample_bridge_cfg("Backend");
bridge_cfg.ffi_skip_methods = vec!["shutdown".to_string()];
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("fn run("),
"required method must appear in trait impl;\n\
actual code:\n{code}"
);
assert!(
!code.contains("fn shutdown("),
"method listed in ffi_skip_methods must NOT get a generated body;\n\
actual code:\n{code}"
);
}
#[test]
fn bug5_async_str_param_uses_to_string_not_clone() {
let method = MethodDef {
name: "process".to_string(),
params: vec![ParamDef {
name: "mime_type".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}],
return_type: TypeRef::Unit,
is_async: true, is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let trait_def = make_trait_def("Backend", vec![method]);
let bridge_cfg = sample_bridge_cfg("Backend");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("let mime_type = mime_type.to_string()"),
"async &str param must be captured via .to_string() to avoid E0521;\n\
actual code:\n{code}"
);
assert!(
!code.contains("let mime_type = mime_type.clone()"),
"async &str param must NOT use .clone() (returns &str, still borrows);\n\
actual code:\n{code}"
);
}
#[test]
fn bug6_async_excluded_type_return_signature_and_deserialization() {
let method = MethodDef {
name: "extract_bytes".to_string(),
params: vec![ParamDef {
name: "content".to_string(),
ty: TypeRef::Bytes,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}],
return_type: TypeRef::Named("InternalDocument".to_string()),
is_async: true,
is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let trait_def = make_trait_def("Extractor", vec![method]);
let bridge_cfg = sample_bridge_cfg("Extractor");
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: {
let mut m = ::std::collections::HashMap::new();
m.insert(
"InternalDocument".to_string(),
"my_lib::internal::InternalDocument".to_string(),
);
m
},
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
};
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("-> std::result::Result<my_lib::internal::InternalDocument,"),
"async method return type must be qualified excluded type in signature;\n\
actual code:\n{code}"
);
assert!(
!code.contains("-> std::result::Result<String,"),
"async method return type must NOT be String for Named return types;\n\
actual code:\n{code}"
);
assert!(
code.contains("serde_json::from_str::<my_lib::internal::InternalDocument>"),
"async closure body must deserialize JSON to InternalDocument;\n\
actual code:\n{code}"
);
assert!(
!code.contains("Ok(cs.to_string_lossy().into_owned())"),
"async closure body must NOT return raw String for Named return types;\n\
actual code:\n{code}"
);
}
#[test]
fn bug_ffi1_trait_impl_param_types_respect_is_ref() {
let method = MethodDef {
name: "process".to_string(),
params: vec![
ParamDef {
name: "content".to_string(),
ty: TypeRef::Bytes,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
},
ParamDef {
name: "mime_type".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
},
ParamDef {
name: "path".to_string(),
ty: TypeRef::Path,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true, is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
},
],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: Some("Box<dyn std::error::Error + Send + Sync>".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let trait_def = make_trait_def("Extractor", vec![method]);
let bridge_cfg = sample_bridge_cfg("Extractor");
let api = sample_api();
let code = gen_trait_bridge(
&trait_def,
&bridge_cfg,
"ml",
"my_lib",
"MyError",
"MyError::from({msg})",
None,
&api,
);
assert!(
code.contains("content: &[u8]"),
"is_ref Bytes param must be &[u8] in trait impl, not Vec<u8>;\n\
actual code:\n{code}"
);
assert!(
code.contains("mime_type: &str"),
"is_ref String param must be &str in trait impl, not String;\n\
actual code:\n{code}"
);
assert!(
code.contains("path: &std::path::Path"),
"is_ref Path param must be &std::path::Path in trait impl, not PathBuf;\n\
actual code:\n{code}"
);
}
}