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 }
697 }
698
699 #[test]
700 fn single_method_trait_emits_vtable_and_register() {
701 let trait_def = make_trait_def(
702 "Validator",
703 vec![make_method(
704 "validate",
705 vec![make_param("input", TypeRef::String)],
706 TypeRef::Primitive(PrimitiveType::Bool),
707 None,
708 )],
709 );
710 let bridge_cfg = make_bridge_cfg("Validator", None);
711
712 let mut out = String::new();
713 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
714
715 assert!(
717 out.contains("pub const IValidator = extern struct {"),
718 "missing vtable struct: {out}"
719 );
720 assert!(out.contains("validate:"), "missing validate slot: {out}");
722 assert!(out.contains("user_data: ?*anyopaque"), "missing user_data: {out}");
724 assert!(out.contains("callconv(.C)"), "missing callconv: {out}");
726 assert!(out.contains("free_user_data:"), "missing free_user_data: {out}");
728 assert!(out.contains("pub fn register_validator("), "missing register fn: {out}");
730 assert!(out.contains("c.demo_register_validator("), "wrong C symbol: {out}");
731 assert!(
733 out.contains("pub fn unregister_validator("),
734 "missing unregister fn: {out}"
735 );
736 assert!(
737 out.contains("c.demo_unregister_validator("),
738 "wrong unregister C symbol: {out}"
739 );
740 assert!(
742 !out.contains("name_fn:"),
743 "should not emit name_fn without super_trait: {out}"
744 );
745 }
746
747 #[test]
748 fn multi_method_trait_with_super_trait_emits_lifecycle_slots() {
749 let trait_def = make_trait_def(
750 "OcrBackend",
751 vec![
752 make_method(
753 "process_image",
754 vec![
755 make_param("image_bytes", TypeRef::Bytes),
756 make_param("config", TypeRef::String),
757 ],
758 TypeRef::String,
759 Some("OcrError"),
760 ),
761 make_method(
762 "supports_language",
763 vec![make_param("lang", TypeRef::String)],
764 TypeRef::Primitive(PrimitiveType::Bool),
765 None,
766 ),
767 ],
768 );
769 let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::plugins::Plugin"));
770
771 let mut out = String::new();
772 emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
773
774 assert!(
776 out.contains("pub const IOcrBackend = extern struct {"),
777 "missing vtable: {out}"
778 );
779 assert!(out.contains("name_fn:"), "missing name_fn: {out}");
781 assert!(out.contains("version_fn:"), "missing version_fn: {out}");
782 assert!(out.contains("initialize_fn:"), "missing initialize_fn: {out}");
783 assert!(out.contains("shutdown_fn:"), "missing shutdown_fn: {out}");
784 assert!(out.contains("process_image:"), "missing process_image slot: {out}");
786 assert!(
787 out.contains("supports_language:"),
788 "missing supports_language slot: {out}"
789 );
790 assert!(out.contains("image_bytes_ptr:"), "missing bytes ptr expansion: {out}");
792 assert!(out.contains("image_bytes_len:"), "missing bytes len expansion: {out}");
793 assert!(
795 out.contains("out_error:"),
796 "missing out_error for fallible method: {out}"
797 );
798 assert!(
800 out.contains("c.kreuzberg_register_ocr_backend("),
801 "wrong register symbol: {out}"
802 );
803 assert!(
804 out.contains("c.kreuzberg_unregister_ocr_backend("),
805 "wrong unregister symbol: {out}"
806 );
807 assert!(
809 out.contains("pub fn register_ocr_backend("),
810 "missing register_ocr_backend fn: {out}"
811 );
812 }
813
814 #[test]
819 fn make_vtable_emits_comptime_function_and_thunk() {
820 let trait_def = make_trait_def(
821 "Validator",
822 vec![make_method(
823 "validate",
824 vec![make_param("input", TypeRef::String)],
825 TypeRef::Primitive(PrimitiveType::Bool),
826 None,
827 )],
828 );
829 let bridge_cfg = make_bridge_cfg("Validator", None);
830
831 let mut out = String::new();
832 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
833
834 assert!(
836 out.contains("pub fn make_validator_vtable(comptime T: type, instance: *T)"),
837 "missing make_validator_vtable: {out}"
838 );
839 assert!(out.contains("IValidator{"), "missing vtable literal: {out}");
841 assert!(out.contains("@ptrCast(@alignCast(ud))"), "missing @ptrCast cast: {out}");
843 assert!(out.contains("callconv(.C)"), "missing callconv(.C) in thunk: {out}");
845 assert!(out.contains(".validate ="), "missing .validate thunk field: {out}");
847 assert!(
849 out.contains(".free_user_data ="),
850 "missing .free_user_data thunk: {out}"
851 );
852 assert!(
854 !out.contains(".name_fn ="),
855 "must not emit .name_fn without super_trait: {out}"
856 );
857 }
858
859 #[test]
860 fn make_vtable_with_super_trait_emits_lifecycle_stubs() {
861 let trait_def = make_trait_def("OcrBackend", vec![]);
862 let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::Plugin"));
863
864 let mut out = String::new();
865 emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
866
867 assert!(
868 out.contains("pub fn make_ocr_backend_vtable(comptime T: type, instance: *T)"),
869 "missing make_ocr_backend_vtable: {out}"
870 );
871 assert!(out.contains(".name_fn ="), "missing .name_fn stub: {out}");
872 assert!(out.contains(".version_fn ="), "missing .version_fn stub: {out}");
873 assert!(out.contains(".initialize_fn ="), "missing .initialize_fn stub: {out}");
874 assert!(out.contains(".shutdown_fn ="), "missing .shutdown_fn stub: {out}");
875 }
876
877 #[test]
878 fn make_vtable_bytes_param_reconstructs_slice_in_thunk() {
879 let trait_def = make_trait_def(
880 "Processor",
881 vec![make_method(
882 "process",
883 vec![make_param("data", TypeRef::Bytes)],
884 TypeRef::Unit,
885 None,
886 )],
887 );
888 let bridge_cfg = make_bridge_cfg("Processor", None);
889
890 let mut out = String::new();
891 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
892
893 assert!(out.contains("data_ptr: [*c]const u8"), "missing data_ptr param: {out}");
895 assert!(out.contains("data_len: usize"), "missing data_len param: {out}");
896 assert!(
898 out.contains("data_ptr[0..data_len]"),
899 "thunk must reconstruct slice from ptr+len: {out}"
900 );
901 assert!(
903 out.contains("self.process(data_slice)"),
904 "thunk must call self.process: {out}"
905 );
906 }
907
908 #[test]
909 fn make_vtable_fallible_method_returns_i32_error_code() {
910 let trait_def = make_trait_def(
911 "Parser",
912 vec![make_method("parse", vec![], TypeRef::Unit, Some("ParseError"))],
913 );
914 let bridge_cfg = make_bridge_cfg("Parser", None);
915
916 let mut out = String::new();
917 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
918
919 assert!(
921 out.contains("callconv(.C) i32"),
922 "fallible thunk must return i32: {out}"
923 );
924 assert!(out.contains("return 0;"), "must return 0 on success: {out}");
926 assert!(out.contains("return 1;"), "must return 1 on error: {out}");
928 assert!(out.contains("out_error"), "must write to out_error: {out}");
930 }
931
932 #[test]
933 fn make_vtable_primitive_return_passes_through() {
934 let trait_def = make_trait_def(
935 "Counter",
936 vec![make_method(
937 "count",
938 vec![],
939 TypeRef::Primitive(PrimitiveType::I32),
940 None,
941 )],
942 );
943 let bridge_cfg = make_bridge_cfg("demo", None);
944
945 let mut out = String::new();
946 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
947
948 assert!(
950 out.contains("return self.count()"),
951 "primitive return must be forwarded directly: {out}"
952 );
953 }
954
955 fn make_spec<'a>(trait_def: &'a TypeDef, bridge_cfg: &'a TraitBridgeConfig) -> TraitBridgeSpec<'a> {
960 use alef_codegen::generators::trait_bridge::TraitBridgeSpec;
961 use std::collections::HashMap;
962 TraitBridgeSpec {
963 trait_def,
964 bridge_config: bridge_cfg,
965 core_import: "kreuzberg",
966 wrapper_prefix: "Zig",
967 type_paths: HashMap::new(),
968 error_type: "KreuzbergError".to_string(),
969 error_constructor: "KreuzbergError::msg({msg})".to_string(),
970 }
971 }
972
973 #[test]
974 fn gen_unregistration_fn_emits_wrapper_when_configured() {
975 let trait_def = make_trait_def("OcrBackend", vec![]);
976 let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
977 bridge_cfg.unregister_fn = Some("unregister_ocr_backend".to_string());
978
979 let generator = ZigTraitBridgeGenerator::new("kreuzberg");
980 let spec = make_spec(&trait_def, &bridge_cfg);
981 let out = generator.gen_unregistration_fn(&spec);
982
983 assert!(!out.is_empty(), "expected non-empty output when unregister_fn is set");
984 assert!(
985 out.contains("pub fn unregister_ocr_backend("),
986 "wrong function name: {out}"
987 );
988 assert!(
989 out.contains("c.kreuzberg_unregister_ocr_backend("),
990 "wrong C symbol: {out}"
991 );
992 assert!(
993 out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
994 "missing out_error param: {out}"
995 );
996 assert!(out.contains("return "), "missing return statement: {out}");
997 assert!(out.ends_with("}\n"), "missing closing brace: {out}");
998 }
999
1000 #[test]
1001 fn gen_unregistration_fn_returns_empty_when_not_configured() {
1002 let trait_def = make_trait_def("OcrBackend", vec![]);
1003 let bridge_cfg = make_bridge_cfg("OcrBackend", None); let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1006 let spec = make_spec(&trait_def, &bridge_cfg);
1007 let out = generator.gen_unregistration_fn(&spec);
1008
1009 assert!(
1010 out.is_empty(),
1011 "expected empty output when unregister_fn is None, got: {out}"
1012 );
1013 }
1014
1015 #[test]
1016 fn gen_clear_fn_emits_wrapper_when_configured() {
1017 let trait_def = make_trait_def("OcrBackend", vec![]);
1018 let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
1019 bridge_cfg.clear_fn = Some("clear_ocr_backends".to_string());
1020
1021 let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1022 let spec = make_spec(&trait_def, &bridge_cfg);
1023 let out = generator.gen_clear_fn(&spec);
1024
1025 assert!(!out.is_empty(), "expected non-empty output when clear_fn is set");
1026 assert!(out.contains("pub fn clear_ocr_backends("), "wrong function name: {out}");
1027 assert!(out.contains("c.kreuzberg_clear_ocr_backends("), "wrong C symbol: {out}");
1028 assert!(
1029 out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
1030 "missing out_error param: {out}"
1031 );
1032 assert!(out.contains("return "), "missing return statement: {out}");
1033 assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1034 }
1035
1036 #[test]
1037 fn gen_clear_fn_returns_empty_when_not_configured() {
1038 let trait_def = make_trait_def("OcrBackend", vec![]);
1039 let bridge_cfg = make_bridge_cfg("OcrBackend", None); let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1042 let spec = make_spec(&trait_def, &bridge_cfg);
1043 let out = generator.gen_clear_fn(&spec);
1044
1045 assert!(
1046 out.is_empty(),
1047 "expected empty output when clear_fn is None, got: {out}"
1048 );
1049 }
1050
1051 #[test]
1052 fn gen_unregistration_fn_uses_snake_case_function_name_verbatim() {
1053 let trait_def = make_trait_def("DocumentExtractor", vec![]);
1055 let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1056 bridge_cfg.unregister_fn = Some("unregister_extractor".to_string());
1057
1058 let generator = ZigTraitBridgeGenerator::new("demo");
1059 let spec = make_spec(&trait_def, &bridge_cfg);
1060 let out = generator.gen_unregistration_fn(&spec);
1061
1062 assert!(
1063 out.contains("pub fn unregister_extractor("),
1064 "must use configured fn name verbatim: {out}"
1065 );
1066 assert!(
1067 out.contains("c.demo_unregister_extractor("),
1068 "must use configured fn name in C symbol: {out}"
1069 );
1070 }
1071
1072 #[test]
1073 fn gen_clear_fn_uses_configured_fn_name_verbatim() {
1074 let trait_def = make_trait_def("DocumentExtractor", vec![]);
1075 let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1076 bridge_cfg.clear_fn = Some("clear_all_extractors".to_string());
1077
1078 let generator = ZigTraitBridgeGenerator::new("demo");
1079 let spec = make_spec(&trait_def, &bridge_cfg);
1080 let out = generator.gen_clear_fn(&spec);
1081
1082 assert!(
1083 out.contains("pub fn clear_all_extractors("),
1084 "must use configured fn name verbatim: {out}"
1085 );
1086 assert!(
1087 out.contains("c.demo_clear_all_extractors("),
1088 "must use configured fn name in C symbol: {out}"
1089 );
1090 }
1091}