1use alef_codegen::generators::trait_bridge::{TraitBridgeGenerator, TraitBridgeSpec};
23use alef_core::config::TraitBridgeConfig;
24use alef_core::ir::{MethodDef, TypeDef, TypeRef};
25use heck::ToSnakeCase;
26
27fn vtable_param_type(ty: &TypeRef) -> &'static str {
32 match ty {
33 TypeRef::Primitive(p) => {
34 use alef_core::ir::PrimitiveType::*;
35 match p {
36 Bool => "i32",
37 U8 => "u8",
38 U16 => "u16",
39 U32 => "u32",
40 U64 => "u64",
41 I8 => "i8",
42 I16 => "i16",
43 I32 => "i32",
44 I64 => "i64",
45 F32 => "f32",
46 F64 => "f64",
47 Usize => "usize",
48 Isize => "isize",
49 }
50 }
51 TypeRef::Unit => "void",
52 TypeRef::Duration => "i64",
53 _ => "[*c]const u8",
55 }
56}
57
58fn vtable_return_type(method: &MethodDef) -> String {
64 if method.error_type.is_some() {
65 "i32".to_string()
66 } else {
67 vtable_param_type(&method.return_type).to_string()
68 }
69}
70
71fn trait_snake(trait_name: &str) -> String {
75 trait_name.to_snake_case()
76}
77
78fn vtable_c_params(method: &MethodDef) -> Vec<(String, String)> {
82 let mut params = vec![("ud".to_string(), "?*anyopaque".to_string())];
83 for p in &method.params {
84 if matches!(p.ty, TypeRef::Bytes) {
85 params.push((format!("{}_ptr", p.name), "[*c]const u8".to_string()));
86 params.push((format!("{}_len", p.name), "usize".to_string()));
87 } else {
88 params.push((p.name.clone(), vtable_param_type(&p.ty).to_string()));
89 }
90 }
91 if method.error_type.is_some() {
92 if !matches!(method.return_type, TypeRef::Unit) {
93 params.push(("out_result".to_string(), "?*?[*c]u8".to_string()));
94 }
95 params.push(("out_error".to_string(), "?*?[*c]u8".to_string()));
96 } else if !matches!(method.return_type, TypeRef::Unit) {
97 params.push(("out_result".to_string(), "?*?[*c]u8".to_string()));
98 }
99 params
100}
101
102pub fn emit_make_vtable(trait_name: &str, has_super_trait: bool, trait_def: &TypeDef, out: &mut String) {
116 let snake = trait_snake(trait_name);
117
118 out.push_str(&crate::template_env::render(
119 "vtable_header_doc.jinja",
120 minijinja::context! {
121 trait_name => trait_name,
122 snake => &snake,
123 },
124 ));
125 out.push_str(&crate::template_env::render(
126 "vtable_impl_method.jinja",
127 minijinja::context! {
128 snake => &snake,
129 trait_name => trait_name,
130 },
131 ));
132 out.push_str(&crate::template_env::render(
133 "vtable_make_fn_header.jinja",
134 minijinja::context! {
135 trait_name => trait_name,
136 },
137 ));
138
139 if has_super_trait {
141 out.push_str(&crate::template_env::render(
142 "vtable_field_name_fn.jinja",
143 minijinja::context! {},
144 ));
145 out.push_str(&crate::template_env::render(
146 "vtable_field_version_fn.jinja",
147 minijinja::context! {},
148 ));
149 out.push_str(&crate::template_env::render(
150 "vtable_field_initialize_fn.jinja",
151 minijinja::context! {},
152 ));
153 out.push_str(&crate::template_env::render(
154 "vtable_field_shutdown_fn.jinja",
155 minijinja::context! {},
156 ));
157 }
158
159 for method in &trait_def.methods {
161 let method_snake = method.name.to_snake_case();
162 let c_params = vtable_c_params(method);
163 let ret = vtable_return_type(method);
164
165 let params_str = c_params
167 .iter()
168 .map(|(name, ty)| format!("{name}: {ty}"))
169 .collect::<Vec<_>>()
170 .join(", ");
171
172 out.push_str(&crate::template_env::render(
173 "vtable_instance_field.jinja",
174 minijinja::context! {
175 method_snake => &method_snake,
176 params_str => ¶ms_str,
177 ret => &ret,
178 },
179 ));
180
181 out.push_str(" const self: *T = @ptrCast(@alignCast(ud));\n");
183
184 let mut call_args: Vec<String> = Vec::new();
186 for p in &method.params {
187 if matches!(p.ty, TypeRef::Bytes) {
188 out.push_str(&crate::template_env::render(
189 "thunk_bytes_slice.jinja",
190 minijinja::context! {
191 slice_name => format!("{}_slice", p.name),
192 ptr_name => format!("{}_ptr", p.name),
193 len_name => format!("{}_len", p.name),
194 },
195 ));
196 call_args.push(format!("{}_slice", p.name));
197 } else {
198 call_args.push(p.name.clone());
199 }
200 }
201
202 let args_str = call_args.join(", ");
203
204 let ok_binding = if method.params.iter().any(|p| p.name == "value") {
208 "ok_value"
209 } else {
210 "value"
211 };
212
213 if method.error_type.is_some() {
214 let has_result_out = !matches!(method.return_type, TypeRef::Unit);
216 out.push_str(&crate::template_env::render(
217 "thunk_fn_signature.jinja",
218 minijinja::context! {
219 method_snake => &method_snake,
220 args_str => &args_str,
221 ok_binding => &ok_binding,
222 },
223 ));
224 let mut success_path_diverges = false;
229 if has_result_out {
230 match &method.return_type {
231 TypeRef::Primitive(_) | TypeRef::Unit => {
232 out.push_str(&crate::template_env::render(
233 "thunk_result_assign.jinja",
234 minijinja::context! {
235 ok_binding => &ok_binding,
236 },
237 ));
238 }
239 _ => {
240 out.push_str(&crate::template_env::render(
242 "thunk_if_fallible.jinja",
243 minijinja::context! {
244 ok_binding => &ok_binding,
245 },
246 ));
247 success_path_diverges = true;
248 }
249 }
250 } else {
251 out.push_str(&crate::template_env::render(
253 "thunk_if_ok_result.jinja",
254 minijinja::context! {
255 ok_binding => &ok_binding,
256 },
257 ));
258 }
259 if !success_path_diverges {
260 out.push_str(" return 0;\n");
261 }
262 out.push_str(" } else |err| {\n");
263 out.push_str(" _ = err;\n");
264 out.push_str(" if (out_error) |ptr| ptr.* = null; // caller checks error code\n");
265 out.push_str(" return 1;\n");
266 out.push_str(" }\n");
267 } else {
268 if !matches!(method.return_type, TypeRef::Unit) {
273 out.push_str(" _ = out_result;\n");
274 }
275 match &method.return_type {
276 TypeRef::Unit => {
277 out.push_str(&crate::template_env::render(
278 "thunk_if_error.jinja",
279 minijinja::context! {
280 method_snake => &method_snake,
281 args_str => &args_str,
282 },
283 ));
284 }
285 TypeRef::Primitive(_) => {
286 out.push_str(&crate::template_env::render(
287 "thunk_infallible_return.jinja",
288 minijinja::context! {
289 method_snake => &method_snake,
290 args_str => &args_str,
291 },
292 ));
293 }
294 _ => {
295 out.push_str(&crate::template_env::render(
297 "thunk_infallible_return.jinja",
298 minijinja::context! {
299 method_snake => &method_snake,
300 args_str => &args_str,
301 },
302 ));
303 }
304 }
305 }
306
307 out.push_str(" }\n");
308 out.push_str(" }.thunk,\n");
309 out.push('\n');
310 }
311
312 out.push_str(&crate::template_env::render(
314 "vtable_free_user_data.jinja",
315 minijinja::context! {},
316 ));
317
318 out.push_str(" };\n");
319 out.push_str("}\n");
320}
321
322pub fn emit_trait_bridge(prefix: &str, bridge_cfg: &TraitBridgeConfig, trait_def: &TypeDef, out: &mut String) {
329 let trait_name = &trait_def.name;
330 let snake = trait_snake(trait_name);
331 let has_super_trait = bridge_cfg.super_trait.is_some();
332
333 out.push_str(&crate::template_env::render(
337 "trait_vtable_header.jinja",
338 minijinja::context! {
339 trait_name => trait_name,
340 snake => &snake,
341 },
342 ));
343 out.push_str(&crate::template_env::render(
344 "trait_struct_header.jinja",
345 minijinja::context! {
346 trait_name => trait_name,
347 },
348 ));
349
350 if has_super_trait {
352 out.push_str(" /// Return the plugin name into `out_name` (heap-allocated, caller frees).\n");
353 out.push_str(
354 " name_fn: ?*const fn (user_data: ?*anyopaque, out_name: ?*?[*c]u8) callconv(.C) void = null,\n",
355 );
356 out.push('\n');
357
358 out.push_str(" /// Return the plugin version into `out_version` (heap-allocated, caller frees).\n");
359 out.push_str(
360 " version_fn: ?*const fn (user_data: ?*anyopaque, out_version: ?*?[*c]u8) callconv(.C) void = null,\n",
361 );
362 out.push('\n');
363
364 out.push_str(" /// Initialise the plugin; return 0 on success, non-zero on error.\n");
365 out.push_str(
366 " initialize_fn: ?*const fn (user_data: ?*anyopaque, out_error: ?*?[*c]u8) callconv(.C) i32 = null,\n",
367 );
368 out.push('\n');
369
370 out.push_str(" /// Shut down the plugin; return 0 on success, non-zero on error.\n");
371 out.push_str(
372 " shutdown_fn: ?*const fn (user_data: ?*anyopaque, out_error: ?*?[*c]u8) callconv(.C) i32 = null,\n",
373 );
374 out.push('\n');
375 }
376
377 for method in &trait_def.methods {
379 if !method.doc.is_empty() {
380 out.push_str(&crate::template_env::render(
381 "trait_method_doc_lines.jinja",
382 minijinja::context! {
383 method_doc_lines => method.doc.lines().collect::<Vec<_>>(),
384 },
385 ));
386 }
387
388 let ret = vtable_return_type(method);
389 let method_snake = method.name.to_snake_case();
390
391 let mut params = vec!["user_data: ?*anyopaque".to_string()];
393 for p in &method.params {
394 let ty = vtable_param_type(&p.ty);
395 if matches!(p.ty, TypeRef::Bytes) {
397 params.push(format!("{}_ptr: [*c]const u8", p.name));
398 params.push(format!("{}_len: usize", p.name));
399 } else {
400 params.push(format!("{}: {ty}", p.name));
401 }
402 }
403
404 if method.error_type.is_some() {
406 if !matches!(method.return_type, TypeRef::Unit) {
407 params.push("out_result: ?*?[*c]u8".to_string());
408 }
409 params.push("out_error: ?*?[*c]u8".to_string());
410 } else if !matches!(method.return_type, TypeRef::Unit) {
411 params.push("out_result: ?*?[*c]u8".to_string());
413 }
414
415 let params_str = params.join(", ");
416 out.push_str(&crate::template_env::render(
417 "trait_method_signature.jinja",
418 minijinja::context! {
419 method_snake => &method_snake,
420 params_str => ¶ms_str,
421 ret => &ret,
422 },
423 ));
424 }
425
426 out.push_str(" /// Called by the Rust runtime when the bridge is dropped.\n");
428 out.push_str(" /// Use this to release any Zig-side state held via `user_data`.\n");
429 out.push_str(" free_user_data: ?*const fn (user_data: ?*anyopaque) callconv(.C) void = null,\n");
430
431 out.push_str("};\n");
432 out.push('\n');
433
434 let c_register = format!("c.{prefix}_register_{snake}");
438 let c_unregister = format!("c.{prefix}_unregister_{snake}");
439
440 out.push_str(&crate::template_env::render(
441 "register_fn_doc1.jinja",
442 minijinja::context! {
443 trait_name => trait_name,
444 snake => &snake,
445 },
446 ));
447 out.push_str(&crate::template_env::render(
448 "register_fn_signature.jinja",
449 minijinja::context! {
450 snake => &snake,
451 trait_name => trait_name,
452 },
453 ));
454 out.push_str(&crate::template_env::render(
455 "register_fn_body.jinja",
456 minijinja::context! {
457 c_register => &c_register,
458 },
459 ));
460 out.push_str("}\n");
461 out.push('\n');
462
463 out.push_str(&crate::template_env::render(
467 "unregister_fn_doc.jinja",
468 minijinja::context! {
469 trait_name => trait_name,
470 },
471 ));
472 out.push_str(&crate::template_env::render(
473 "unregister_fn_signature.jinja",
474 minijinja::context! {
475 snake => &snake,
476 },
477 ));
478 out.push_str(&crate::template_env::render(
479 "unregister_fn_body.jinja",
480 minijinja::context! {
481 c_unregister => &c_unregister,
482 },
483 ));
484 out.push_str("}\n");
485 out.push('\n');
486
487 emit_make_vtable(trait_name, has_super_trait, trait_def, out);
491}
492
493pub struct ZigTraitBridgeGenerator {
507 pub prefix: String,
509}
510
511impl ZigTraitBridgeGenerator {
512 pub fn new(prefix: impl Into<String>) -> Self {
514 Self { prefix: prefix.into() }
515 }
516}
517
518impl TraitBridgeGenerator for ZigTraitBridgeGenerator {
519 fn foreign_object_type(&self) -> &str {
524 ""
525 }
526
527 fn bridge_imports(&self) -> Vec<String> {
528 Vec::new()
529 }
530
531 fn gen_sync_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
532 String::new()
533 }
534
535 fn gen_async_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
536 String::new()
537 }
538
539 fn gen_constructor(&self, _spec: &TraitBridgeSpec) -> String {
540 String::new()
541 }
542
543 fn gen_registration_fn(&self, _spec: &TraitBridgeSpec) -> String {
544 String::new()
545 }
546
547 fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
555 let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
556 return String::new();
557 };
558 let c_unregister = format!("c.{}_{}", self.prefix, unregister_fn);
559
560 let mut out = String::new();
561 out.push_str(&crate::template_env::render(
562 "unregister_fn_doc.jinja",
563 minijinja::context! {
564 trait_name => spec.trait_def.name.as_str(),
565 },
566 ));
567 out.push_str(&crate::template_env::render(
570 "unregister_fn_configured_signature.jinja",
571 minijinja::context! {
572 unregister_fn => unregister_fn,
573 },
574 ));
575 out.push_str(&crate::template_env::render(
576 "unregister_fn_body.jinja",
577 minijinja::context! {
578 c_unregister => &c_unregister,
579 },
580 ));
581 out.push_str("}\n");
582 out
583 }
584
585 fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
589 let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
590 return String::new();
591 };
592 let c_clear = format!("c.{}_{}", self.prefix, clear_fn);
593
594 let mut out = String::new();
595 out.push_str(&crate::template_env::render(
596 "clear_fn_doc.jinja",
597 minijinja::context! {
598 trait_name => spec.trait_def.name.as_str(),
599 },
600 ));
601 out.push_str(&crate::template_env::render(
602 "clear_fn_signature.jinja",
603 minijinja::context! {
604 clear_fn => clear_fn,
605 },
606 ));
607 out.push_str(&crate::template_env::render(
608 "clear_fn_body.jinja",
609 minijinja::context! {
610 c_clear => &c_clear,
611 },
612 ));
613 out.push_str("}\n");
614 out
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use alef_core::ir::{FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeRef};
622
623 fn make_trait_def(name: &str, methods: Vec<MethodDef>) -> TypeDef {
624 TypeDef {
625 name: name.to_string(),
626 rust_path: format!("demo::{name}"),
627 original_rust_path: String::new(),
628 fields: Vec::<FieldDef>::new(),
629 methods,
630 is_opaque: true,
631 is_clone: false,
632 is_copy: false,
633 is_trait: true,
634 has_default: false,
635 has_stripped_cfg_fields: false,
636 is_return_type: false,
637 serde_rename_all: None,
638 has_serde: false,
639 super_traits: vec![],
640 doc: String::new(),
641 cfg: None,
642 }
643 }
644
645 fn make_method(name: &str, params: Vec<ParamDef>, return_type: TypeRef, error_type: Option<&str>) -> MethodDef {
646 MethodDef {
647 name: name.to_string(),
648 params,
649 return_type,
650 is_async: false,
651 is_static: false,
652 error_type: error_type.map(|s| s.to_string()),
653 doc: String::new(),
654 receiver: Some(ReceiverKind::Ref),
655 sanitized: false,
656 trait_source: None,
657 returns_ref: false,
658 returns_cow: false,
659 return_newtype_wrapper: None,
660 has_default_impl: false,
661 }
662 }
663
664 fn make_param(name: &str, ty: TypeRef) -> ParamDef {
665 ParamDef {
666 name: name.to_string(),
667 ty,
668 optional: false,
669 default: None,
670 sanitized: false,
671 typed_default: None,
672 is_ref: false,
673 is_mut: false,
674 newtype_wrapper: None,
675 original_type: None,
676 }
677 }
678
679 fn make_bridge_cfg(trait_name: &str, super_trait: Option<&str>) -> TraitBridgeConfig {
680 TraitBridgeConfig {
681 trait_name: trait_name.to_string(),
682 super_trait: super_trait.map(|s| s.to_string()),
683 registry_getter: None,
684 register_fn: None,
685
686 unregister_fn: None,
687
688 clear_fn: None,
689 type_alias: None,
690 param_name: None,
691 register_extra_args: None,
692 exclude_languages: vec![],
693 bind_via: alef_core::config::BridgeBinding::FunctionParam,
694 options_type: None,
695 options_field: None,
696 context_type: None,
697 result_type: None,
698 ffi_skip_methods: Vec::new(),
699 }
700 }
701
702 #[test]
703 fn single_method_trait_emits_vtable_and_register() {
704 let trait_def = make_trait_def(
705 "Validator",
706 vec![make_method(
707 "validate",
708 vec![make_param("input", TypeRef::String)],
709 TypeRef::Primitive(PrimitiveType::Bool),
710 None,
711 )],
712 );
713 let bridge_cfg = make_bridge_cfg("Validator", None);
714
715 let mut out = String::new();
716 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
717
718 assert!(
720 out.contains("pub const IValidator = extern struct {"),
721 "missing vtable struct: {out}"
722 );
723 assert!(out.contains("validate:"), "missing validate slot: {out}");
725 assert!(out.contains("user_data: ?*anyopaque"), "missing user_data: {out}");
727 assert!(out.contains("callconv(.C)"), "missing callconv: {out}");
729 assert!(out.contains("free_user_data:"), "missing free_user_data: {out}");
731 assert!(out.contains("pub fn register_validator("), "missing register fn: {out}");
733 assert!(out.contains("c.demo_register_validator("), "wrong C symbol: {out}");
734 assert!(
736 out.contains("pub fn unregister_validator("),
737 "missing unregister fn: {out}"
738 );
739 assert!(
740 out.contains("c.demo_unregister_validator("),
741 "wrong unregister C symbol: {out}"
742 );
743 assert!(
745 !out.contains("name_fn:"),
746 "should not emit name_fn without super_trait: {out}"
747 );
748 }
749
750 #[test]
751 fn multi_method_trait_with_super_trait_emits_lifecycle_slots() {
752 let trait_def = make_trait_def(
753 "OcrBackend",
754 vec![
755 make_method(
756 "process_image",
757 vec![
758 make_param("image_bytes", TypeRef::Bytes),
759 make_param("config", TypeRef::String),
760 ],
761 TypeRef::String,
762 Some("OcrError"),
763 ),
764 make_method(
765 "supports_language",
766 vec![make_param("lang", TypeRef::String)],
767 TypeRef::Primitive(PrimitiveType::Bool),
768 None,
769 ),
770 ],
771 );
772 let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::plugins::Plugin"));
773
774 let mut out = String::new();
775 emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
776
777 assert!(
779 out.contains("pub const IOcrBackend = extern struct {"),
780 "missing vtable: {out}"
781 );
782 assert!(out.contains("name_fn:"), "missing name_fn: {out}");
784 assert!(out.contains("version_fn:"), "missing version_fn: {out}");
785 assert!(out.contains("initialize_fn:"), "missing initialize_fn: {out}");
786 assert!(out.contains("shutdown_fn:"), "missing shutdown_fn: {out}");
787 assert!(out.contains("process_image:"), "missing process_image slot: {out}");
789 assert!(
790 out.contains("supports_language:"),
791 "missing supports_language slot: {out}"
792 );
793 assert!(out.contains("image_bytes_ptr:"), "missing bytes ptr expansion: {out}");
795 assert!(out.contains("image_bytes_len:"), "missing bytes len expansion: {out}");
796 assert!(
798 out.contains("out_error:"),
799 "missing out_error for fallible method: {out}"
800 );
801 assert!(
803 out.contains("c.kreuzberg_register_ocr_backend("),
804 "wrong register symbol: {out}"
805 );
806 assert!(
807 out.contains("c.kreuzberg_unregister_ocr_backend("),
808 "wrong unregister symbol: {out}"
809 );
810 assert!(
812 out.contains("pub fn register_ocr_backend("),
813 "missing register_ocr_backend fn: {out}"
814 );
815 }
816
817 #[test]
822 fn make_vtable_emits_comptime_function_and_thunk() {
823 let trait_def = make_trait_def(
824 "Validator",
825 vec![make_method(
826 "validate",
827 vec![make_param("input", TypeRef::String)],
828 TypeRef::Primitive(PrimitiveType::Bool),
829 None,
830 )],
831 );
832 let bridge_cfg = make_bridge_cfg("Validator", None);
833
834 let mut out = String::new();
835 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
836
837 assert!(
839 out.contains("pub fn make_validator_vtable(comptime T: type, instance: *T)"),
840 "missing make_validator_vtable: {out}"
841 );
842 assert!(out.contains("IValidator{"), "missing vtable literal: {out}");
844 assert!(out.contains("@ptrCast(@alignCast(ud))"), "missing @ptrCast cast: {out}");
846 assert!(out.contains("callconv(.C)"), "missing callconv(.C) in thunk: {out}");
848 assert!(out.contains(".validate ="), "missing .validate thunk field: {out}");
850 assert!(
852 out.contains(".free_user_data ="),
853 "missing .free_user_data thunk: {out}"
854 );
855 assert!(
857 !out.contains(".name_fn ="),
858 "must not emit .name_fn without super_trait: {out}"
859 );
860 }
861
862 #[test]
863 fn make_vtable_with_super_trait_emits_lifecycle_stubs() {
864 let trait_def = make_trait_def("OcrBackend", vec![]);
865 let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::Plugin"));
866
867 let mut out = String::new();
868 emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
869
870 assert!(
871 out.contains("pub fn make_ocr_backend_vtable(comptime T: type, instance: *T)"),
872 "missing make_ocr_backend_vtable: {out}"
873 );
874 assert!(out.contains(".name_fn ="), "missing .name_fn stub: {out}");
875 assert!(out.contains(".version_fn ="), "missing .version_fn stub: {out}");
876 assert!(out.contains(".initialize_fn ="), "missing .initialize_fn stub: {out}");
877 assert!(out.contains(".shutdown_fn ="), "missing .shutdown_fn stub: {out}");
878 }
879
880 #[test]
881 fn make_vtable_bytes_param_reconstructs_slice_in_thunk() {
882 let trait_def = make_trait_def(
883 "Processor",
884 vec![make_method(
885 "process",
886 vec![make_param("data", TypeRef::Bytes)],
887 TypeRef::Unit,
888 None,
889 )],
890 );
891 let bridge_cfg = make_bridge_cfg("Processor", None);
892
893 let mut out = String::new();
894 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
895
896 assert!(out.contains("data_ptr: [*c]const u8"), "missing data_ptr param: {out}");
898 assert!(out.contains("data_len: usize"), "missing data_len param: {out}");
899 assert!(
901 out.contains("data_ptr[0..data_len]"),
902 "thunk must reconstruct slice from ptr+len: {out}"
903 );
904 assert!(
906 out.contains("self.process(data_slice)"),
907 "thunk must call self.process: {out}"
908 );
909 }
910
911 #[test]
912 fn make_vtable_fallible_method_returns_i32_error_code() {
913 let trait_def = make_trait_def(
914 "Parser",
915 vec![make_method("parse", vec![], TypeRef::Unit, Some("ParseError"))],
916 );
917 let bridge_cfg = make_bridge_cfg("Parser", None);
918
919 let mut out = String::new();
920 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
921
922 assert!(
924 out.contains("callconv(.C) i32"),
925 "fallible thunk must return i32: {out}"
926 );
927 assert!(out.contains("return 0;"), "must return 0 on success: {out}");
929 assert!(out.contains("return 1;"), "must return 1 on error: {out}");
931 assert!(out.contains("out_error"), "must write to out_error: {out}");
933 }
934
935 #[test]
936 fn make_vtable_primitive_return_passes_through() {
937 let trait_def = make_trait_def(
938 "Counter",
939 vec![make_method(
940 "count",
941 vec![],
942 TypeRef::Primitive(PrimitiveType::I32),
943 None,
944 )],
945 );
946 let bridge_cfg = make_bridge_cfg("demo", None);
947
948 let mut out = String::new();
949 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
950
951 assert!(
953 out.contains("return self.count()"),
954 "primitive return must be forwarded directly: {out}"
955 );
956 }
957
958 fn make_spec<'a>(trait_def: &'a TypeDef, bridge_cfg: &'a TraitBridgeConfig) -> TraitBridgeSpec<'a> {
963 use alef_codegen::generators::trait_bridge::TraitBridgeSpec;
964 use std::collections::HashMap;
965 TraitBridgeSpec {
966 trait_def,
967 bridge_config: bridge_cfg,
968 core_import: "kreuzberg",
969 wrapper_prefix: "Zig",
970 type_paths: HashMap::new(),
971 error_type: "KreuzbergError".to_string(),
972 error_constructor: "KreuzbergError::msg({msg})".to_string(),
973 }
974 }
975
976 #[test]
977 fn gen_unregistration_fn_emits_wrapper_when_configured() {
978 let trait_def = make_trait_def("OcrBackend", vec![]);
979 let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
980 bridge_cfg.unregister_fn = Some("unregister_ocr_backend".to_string());
981
982 let generator = ZigTraitBridgeGenerator::new("kreuzberg");
983 let spec = make_spec(&trait_def, &bridge_cfg);
984 let out = generator.gen_unregistration_fn(&spec);
985
986 assert!(!out.is_empty(), "expected non-empty output when unregister_fn is set");
987 assert!(
988 out.contains("pub fn unregister_ocr_backend("),
989 "wrong function name: {out}"
990 );
991 assert!(
992 out.contains("c.kreuzberg_unregister_ocr_backend("),
993 "wrong C symbol: {out}"
994 );
995 assert!(
996 out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
997 "missing out_error param: {out}"
998 );
999 assert!(out.contains("return "), "missing return statement: {out}");
1000 assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1001 }
1002
1003 #[test]
1004 fn gen_unregistration_fn_returns_empty_when_not_configured() {
1005 let trait_def = make_trait_def("OcrBackend", vec![]);
1006 let bridge_cfg = make_bridge_cfg("OcrBackend", None); let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1009 let spec = make_spec(&trait_def, &bridge_cfg);
1010 let out = generator.gen_unregistration_fn(&spec);
1011
1012 assert!(
1013 out.is_empty(),
1014 "expected empty output when unregister_fn is None, got: {out}"
1015 );
1016 }
1017
1018 #[test]
1019 fn gen_clear_fn_emits_wrapper_when_configured() {
1020 let trait_def = make_trait_def("OcrBackend", vec![]);
1021 let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
1022 bridge_cfg.clear_fn = Some("clear_ocr_backends".to_string());
1023
1024 let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1025 let spec = make_spec(&trait_def, &bridge_cfg);
1026 let out = generator.gen_clear_fn(&spec);
1027
1028 assert!(!out.is_empty(), "expected non-empty output when clear_fn is set");
1029 assert!(out.contains("pub fn clear_ocr_backends("), "wrong function name: {out}");
1030 assert!(out.contains("c.kreuzberg_clear_ocr_backends("), "wrong C symbol: {out}");
1031 assert!(
1032 out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
1033 "missing out_error param: {out}"
1034 );
1035 assert!(out.contains("return "), "missing return statement: {out}");
1036 assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1037 }
1038
1039 #[test]
1040 fn gen_clear_fn_returns_empty_when_not_configured() {
1041 let trait_def = make_trait_def("OcrBackend", vec![]);
1042 let bridge_cfg = make_bridge_cfg("OcrBackend", None); let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1045 let spec = make_spec(&trait_def, &bridge_cfg);
1046 let out = generator.gen_clear_fn(&spec);
1047
1048 assert!(
1049 out.is_empty(),
1050 "expected empty output when clear_fn is None, got: {out}"
1051 );
1052 }
1053
1054 #[test]
1055 fn gen_unregistration_fn_uses_snake_case_function_name_verbatim() {
1056 let trait_def = make_trait_def("DocumentExtractor", vec![]);
1058 let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1059 bridge_cfg.unregister_fn = Some("unregister_extractor".to_string());
1060
1061 let generator = ZigTraitBridgeGenerator::new("demo");
1062 let spec = make_spec(&trait_def, &bridge_cfg);
1063 let out = generator.gen_unregistration_fn(&spec);
1064
1065 assert!(
1066 out.contains("pub fn unregister_extractor("),
1067 "must use configured fn name verbatim: {out}"
1068 );
1069 assert!(
1070 out.contains("c.demo_unregister_extractor("),
1071 "must use configured fn name in C symbol: {out}"
1072 );
1073 }
1074
1075 #[test]
1076 fn gen_clear_fn_uses_configured_fn_name_verbatim() {
1077 let trait_def = make_trait_def("DocumentExtractor", vec![]);
1078 let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1079 bridge_cfg.clear_fn = Some("clear_all_extractors".to_string());
1080
1081 let generator = ZigTraitBridgeGenerator::new("demo");
1082 let spec = make_spec(&trait_def, &bridge_cfg);
1083 let out = generator.gen_clear_fn(&spec);
1084
1085 assert!(
1086 out.contains("pub fn clear_all_extractors("),
1087 "must use configured fn name verbatim: {out}"
1088 );
1089 assert!(
1090 out.contains("c.demo_clear_all_extractors("),
1091 "must use configured fn name in C symbol: {out}"
1092 );
1093 }
1094}