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 for line in method.doc.lines() {
381 out.push_str(&crate::template_env::render(
382 "trait_method_doc.jinja",
383 minijinja::context! {
384 line => line,
385 },
386 ));
387 }
388 }
389
390 let ret = vtable_return_type(method);
391 let method_snake = method.name.to_snake_case();
392
393 let mut params = vec!["user_data: ?*anyopaque".to_string()];
395 for p in &method.params {
396 let ty = vtable_param_type(&p.ty);
397 if matches!(p.ty, TypeRef::Bytes) {
399 params.push(format!("{}_ptr: [*c]const u8", p.name));
400 params.push(format!("{}_len: usize", p.name));
401 } else {
402 params.push(format!("{}: {ty}", p.name));
403 }
404 }
405
406 if method.error_type.is_some() {
408 if !matches!(method.return_type, TypeRef::Unit) {
409 params.push("out_result: ?*?[*c]u8".to_string());
410 }
411 params.push("out_error: ?*?[*c]u8".to_string());
412 } else if !matches!(method.return_type, TypeRef::Unit) {
413 params.push("out_result: ?*?[*c]u8".to_string());
415 }
416
417 let params_str = params.join(", ");
418 out.push_str(&crate::template_env::render(
419 "trait_method_signature.jinja",
420 minijinja::context! {
421 method_snake => &method_snake,
422 params_str => ¶ms_str,
423 ret => &ret,
424 },
425 ));
426 }
427
428 out.push_str(" /// Called by the Rust runtime when the bridge is dropped.\n");
430 out.push_str(" /// Use this to release any Zig-side state held via `user_data`.\n");
431 out.push_str(" free_user_data: ?*const fn (user_data: ?*anyopaque) callconv(.C) void = null,\n");
432
433 out.push_str("};\n");
434 out.push('\n');
435
436 let c_register = format!("c.{prefix}_register_{snake}");
440 let c_unregister = format!("c.{prefix}_unregister_{snake}");
441
442 out.push_str(&crate::template_env::render(
443 "register_fn_doc1.jinja",
444 minijinja::context! {
445 trait_name => trait_name,
446 snake => &snake,
447 },
448 ));
449 out.push_str(&crate::template_env::render(
450 "register_fn_signature.jinja",
451 minijinja::context! {
452 snake => &snake,
453 trait_name => trait_name,
454 },
455 ));
456 out.push_str(&crate::template_env::render(
457 "register_fn_body.jinja",
458 minijinja::context! {
459 c_register => &c_register,
460 },
461 ));
462 out.push_str("}\n");
463 out.push('\n');
464
465 out.push_str(&crate::template_env::render(
469 "unregister_fn_doc.jinja",
470 minijinja::context! {
471 trait_name => trait_name,
472 },
473 ));
474 out.push_str(&crate::template_env::render(
475 "unregister_fn_signature.jinja",
476 minijinja::context! {
477 snake => &snake,
478 },
479 ));
480 out.push_str(&crate::template_env::render(
481 "unregister_fn_body.jinja",
482 minijinja::context! {
483 c_unregister => &c_unregister,
484 },
485 ));
486 out.push_str("}\n");
487 out.push('\n');
488
489 emit_make_vtable(trait_name, has_super_trait, trait_def, out);
493}
494
495pub struct ZigTraitBridgeGenerator {
509 pub prefix: String,
511}
512
513impl ZigTraitBridgeGenerator {
514 pub fn new(prefix: impl Into<String>) -> Self {
516 Self { prefix: prefix.into() }
517 }
518}
519
520impl TraitBridgeGenerator for ZigTraitBridgeGenerator {
521 fn foreign_object_type(&self) -> &str {
526 ""
527 }
528
529 fn bridge_imports(&self) -> Vec<String> {
530 Vec::new()
531 }
532
533 fn gen_sync_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
534 String::new()
535 }
536
537 fn gen_async_method_body(&self, _method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
538 String::new()
539 }
540
541 fn gen_constructor(&self, _spec: &TraitBridgeSpec) -> String {
542 String::new()
543 }
544
545 fn gen_registration_fn(&self, _spec: &TraitBridgeSpec) -> String {
546 String::new()
547 }
548
549 fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
557 let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
558 return String::new();
559 };
560 let c_unregister = format!("c.{}_{}", self.prefix, unregister_fn);
561
562 let mut out = String::new();
563 out.push_str(&crate::template_env::render(
564 "unregister_fn_doc.jinja",
565 minijinja::context! {
566 trait_name => spec.trait_def.name.as_str(),
567 },
568 ));
569 out.push_str(&format!(
572 "pub fn {unregister_fn}(name: [*c]const u8, out_error: ?*?[*c]u8) i32 {{\n"
573 ));
574 out.push_str(&crate::template_env::render(
575 "unregister_fn_body.jinja",
576 minijinja::context! {
577 c_unregister => &c_unregister,
578 },
579 ));
580 out.push_str("}\n");
581 out
582 }
583
584 fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
588 let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
589 return String::new();
590 };
591 let c_clear = format!("c.{}_{}", self.prefix, clear_fn);
592
593 let mut out = String::new();
594 out.push_str(&crate::template_env::render(
595 "clear_fn_doc.jinja",
596 minijinja::context! {
597 trait_name => spec.trait_def.name.as_str(),
598 },
599 ));
600 out.push_str(&crate::template_env::render(
601 "clear_fn_signature.jinja",
602 minijinja::context! {
603 clear_fn => clear_fn,
604 },
605 ));
606 out.push_str(&crate::template_env::render(
607 "clear_fn_body.jinja",
608 minijinja::context! {
609 c_clear => &c_clear,
610 },
611 ));
612 out.push_str("}\n");
613 out
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use alef_core::ir::{FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeRef};
621
622 fn make_trait_def(name: &str, methods: Vec<MethodDef>) -> TypeDef {
623 TypeDef {
624 name: name.to_string(),
625 rust_path: format!("demo::{name}"),
626 original_rust_path: String::new(),
627 fields: Vec::<FieldDef>::new(),
628 methods,
629 is_opaque: true,
630 is_clone: false,
631 is_copy: false,
632 is_trait: true,
633 has_default: false,
634 has_stripped_cfg_fields: false,
635 is_return_type: false,
636 serde_rename_all: None,
637 has_serde: false,
638 super_traits: vec![],
639 doc: String::new(),
640 cfg: None,
641 }
642 }
643
644 fn make_method(name: &str, params: Vec<ParamDef>, return_type: TypeRef, error_type: Option<&str>) -> MethodDef {
645 MethodDef {
646 name: name.to_string(),
647 params,
648 return_type,
649 is_async: false,
650 is_static: false,
651 error_type: error_type.map(|s| s.to_string()),
652 doc: String::new(),
653 receiver: Some(ReceiverKind::Ref),
654 sanitized: false,
655 trait_source: None,
656 returns_ref: false,
657 returns_cow: false,
658 return_newtype_wrapper: None,
659 has_default_impl: false,
660 }
661 }
662
663 fn make_param(name: &str, ty: TypeRef) -> ParamDef {
664 ParamDef {
665 name: name.to_string(),
666 ty,
667 optional: false,
668 default: None,
669 sanitized: false,
670 typed_default: None,
671 is_ref: false,
672 is_mut: false,
673 newtype_wrapper: None,
674 original_type: None,
675 }
676 }
677
678 fn make_bridge_cfg(trait_name: &str, super_trait: Option<&str>) -> TraitBridgeConfig {
679 TraitBridgeConfig {
680 trait_name: trait_name.to_string(),
681 super_trait: super_trait.map(|s| s.to_string()),
682 registry_getter: None,
683 register_fn: None,
684
685 unregister_fn: None,
686
687 clear_fn: None,
688 type_alias: None,
689 param_name: None,
690 register_extra_args: None,
691 exclude_languages: vec![],
692 bind_via: alef_core::config::BridgeBinding::FunctionParam,
693 options_type: None,
694 options_field: None,
695 }
696 }
697
698 #[test]
699 fn single_method_trait_emits_vtable_and_register() {
700 let trait_def = make_trait_def(
701 "Validator",
702 vec![make_method(
703 "validate",
704 vec![make_param("input", TypeRef::String)],
705 TypeRef::Primitive(PrimitiveType::Bool),
706 None,
707 )],
708 );
709 let bridge_cfg = make_bridge_cfg("Validator", None);
710
711 let mut out = String::new();
712 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
713
714 assert!(
716 out.contains("pub const IValidator = extern struct {"),
717 "missing vtable struct: {out}"
718 );
719 assert!(out.contains("validate:"), "missing validate slot: {out}");
721 assert!(out.contains("user_data: ?*anyopaque"), "missing user_data: {out}");
723 assert!(out.contains("callconv(.C)"), "missing callconv: {out}");
725 assert!(out.contains("free_user_data:"), "missing free_user_data: {out}");
727 assert!(out.contains("pub fn register_validator("), "missing register fn: {out}");
729 assert!(out.contains("c.demo_register_validator("), "wrong C symbol: {out}");
730 assert!(
732 out.contains("pub fn unregister_validator("),
733 "missing unregister fn: {out}"
734 );
735 assert!(
736 out.contains("c.demo_unregister_validator("),
737 "wrong unregister C symbol: {out}"
738 );
739 assert!(
741 !out.contains("name_fn:"),
742 "should not emit name_fn without super_trait: {out}"
743 );
744 }
745
746 #[test]
747 fn multi_method_trait_with_super_trait_emits_lifecycle_slots() {
748 let trait_def = make_trait_def(
749 "OcrBackend",
750 vec![
751 make_method(
752 "process_image",
753 vec![
754 make_param("image_bytes", TypeRef::Bytes),
755 make_param("config", TypeRef::String),
756 ],
757 TypeRef::String,
758 Some("OcrError"),
759 ),
760 make_method(
761 "supports_language",
762 vec![make_param("lang", TypeRef::String)],
763 TypeRef::Primitive(PrimitiveType::Bool),
764 None,
765 ),
766 ],
767 );
768 let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::plugins::Plugin"));
769
770 let mut out = String::new();
771 emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
772
773 assert!(
775 out.contains("pub const IOcrBackend = extern struct {"),
776 "missing vtable: {out}"
777 );
778 assert!(out.contains("name_fn:"), "missing name_fn: {out}");
780 assert!(out.contains("version_fn:"), "missing version_fn: {out}");
781 assert!(out.contains("initialize_fn:"), "missing initialize_fn: {out}");
782 assert!(out.contains("shutdown_fn:"), "missing shutdown_fn: {out}");
783 assert!(out.contains("process_image:"), "missing process_image slot: {out}");
785 assert!(
786 out.contains("supports_language:"),
787 "missing supports_language slot: {out}"
788 );
789 assert!(out.contains("image_bytes_ptr:"), "missing bytes ptr expansion: {out}");
791 assert!(out.contains("image_bytes_len:"), "missing bytes len expansion: {out}");
792 assert!(
794 out.contains("out_error:"),
795 "missing out_error for fallible method: {out}"
796 );
797 assert!(
799 out.contains("c.kreuzberg_register_ocr_backend("),
800 "wrong register symbol: {out}"
801 );
802 assert!(
803 out.contains("c.kreuzberg_unregister_ocr_backend("),
804 "wrong unregister symbol: {out}"
805 );
806 assert!(
808 out.contains("pub fn register_ocr_backend("),
809 "missing register_ocr_backend fn: {out}"
810 );
811 }
812
813 #[test]
818 fn make_vtable_emits_comptime_function_and_thunk() {
819 let trait_def = make_trait_def(
820 "Validator",
821 vec![make_method(
822 "validate",
823 vec![make_param("input", TypeRef::String)],
824 TypeRef::Primitive(PrimitiveType::Bool),
825 None,
826 )],
827 );
828 let bridge_cfg = make_bridge_cfg("Validator", None);
829
830 let mut out = String::new();
831 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
832
833 assert!(
835 out.contains("pub fn make_validator_vtable(comptime T: type, instance: *T)"),
836 "missing make_validator_vtable: {out}"
837 );
838 assert!(out.contains("IValidator{"), "missing vtable literal: {out}");
840 assert!(out.contains("@ptrCast(@alignCast(ud))"), "missing @ptrCast cast: {out}");
842 assert!(out.contains("callconv(.C)"), "missing callconv(.C) in thunk: {out}");
844 assert!(out.contains(".validate ="), "missing .validate thunk field: {out}");
846 assert!(
848 out.contains(".free_user_data ="),
849 "missing .free_user_data thunk: {out}"
850 );
851 assert!(
853 !out.contains(".name_fn ="),
854 "must not emit .name_fn without super_trait: {out}"
855 );
856 }
857
858 #[test]
859 fn make_vtable_with_super_trait_emits_lifecycle_stubs() {
860 let trait_def = make_trait_def("OcrBackend", vec![]);
861 let bridge_cfg = make_bridge_cfg("OcrBackend", Some("kreuzberg::Plugin"));
862
863 let mut out = String::new();
864 emit_trait_bridge("kreuzberg", &bridge_cfg, &trait_def, &mut out);
865
866 assert!(
867 out.contains("pub fn make_ocr_backend_vtable(comptime T: type, instance: *T)"),
868 "missing make_ocr_backend_vtable: {out}"
869 );
870 assert!(out.contains(".name_fn ="), "missing .name_fn stub: {out}");
871 assert!(out.contains(".version_fn ="), "missing .version_fn stub: {out}");
872 assert!(out.contains(".initialize_fn ="), "missing .initialize_fn stub: {out}");
873 assert!(out.contains(".shutdown_fn ="), "missing .shutdown_fn stub: {out}");
874 }
875
876 #[test]
877 fn make_vtable_bytes_param_reconstructs_slice_in_thunk() {
878 let trait_def = make_trait_def(
879 "Processor",
880 vec![make_method(
881 "process",
882 vec![make_param("data", TypeRef::Bytes)],
883 TypeRef::Unit,
884 None,
885 )],
886 );
887 let bridge_cfg = make_bridge_cfg("Processor", None);
888
889 let mut out = String::new();
890 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
891
892 assert!(out.contains("data_ptr: [*c]const u8"), "missing data_ptr param: {out}");
894 assert!(out.contains("data_len: usize"), "missing data_len param: {out}");
895 assert!(
897 out.contains("data_ptr[0..data_len]"),
898 "thunk must reconstruct slice from ptr+len: {out}"
899 );
900 assert!(
902 out.contains("self.process(data_slice)"),
903 "thunk must call self.process: {out}"
904 );
905 }
906
907 #[test]
908 fn make_vtable_fallible_method_returns_i32_error_code() {
909 let trait_def = make_trait_def(
910 "Parser",
911 vec![make_method("parse", vec![], TypeRef::Unit, Some("ParseError"))],
912 );
913 let bridge_cfg = make_bridge_cfg("Parser", None);
914
915 let mut out = String::new();
916 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
917
918 assert!(
920 out.contains("callconv(.C) i32"),
921 "fallible thunk must return i32: {out}"
922 );
923 assert!(out.contains("return 0;"), "must return 0 on success: {out}");
925 assert!(out.contains("return 1;"), "must return 1 on error: {out}");
927 assert!(out.contains("out_error"), "must write to out_error: {out}");
929 }
930
931 #[test]
932 fn make_vtable_primitive_return_passes_through() {
933 let trait_def = make_trait_def(
934 "Counter",
935 vec![make_method(
936 "count",
937 vec![],
938 TypeRef::Primitive(PrimitiveType::I32),
939 None,
940 )],
941 );
942 let bridge_cfg = make_bridge_cfg("demo", None);
943
944 let mut out = String::new();
945 emit_trait_bridge("demo", &bridge_cfg, &trait_def, &mut out);
946
947 assert!(
949 out.contains("return self.count()"),
950 "primitive return must be forwarded directly: {out}"
951 );
952 }
953
954 fn make_spec<'a>(trait_def: &'a TypeDef, bridge_cfg: &'a TraitBridgeConfig) -> TraitBridgeSpec<'a> {
959 use alef_codegen::generators::trait_bridge::TraitBridgeSpec;
960 use std::collections::HashMap;
961 TraitBridgeSpec {
962 trait_def,
963 bridge_config: bridge_cfg,
964 core_import: "kreuzberg",
965 wrapper_prefix: "Zig",
966 type_paths: HashMap::new(),
967 error_type: "KreuzbergError".to_string(),
968 error_constructor: "KreuzbergError::msg({msg})".to_string(),
969 }
970 }
971
972 #[test]
973 fn gen_unregistration_fn_emits_wrapper_when_configured() {
974 let trait_def = make_trait_def("OcrBackend", vec![]);
975 let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
976 bridge_cfg.unregister_fn = Some("unregister_ocr_backend".to_string());
977
978 let generator = ZigTraitBridgeGenerator::new("kreuzberg");
979 let spec = make_spec(&trait_def, &bridge_cfg);
980 let out = generator.gen_unregistration_fn(&spec);
981
982 assert!(!out.is_empty(), "expected non-empty output when unregister_fn is set");
983 assert!(
984 out.contains("pub fn unregister_ocr_backend("),
985 "wrong function name: {out}"
986 );
987 assert!(
988 out.contains("c.kreuzberg_unregister_ocr_backend("),
989 "wrong C symbol: {out}"
990 );
991 assert!(
992 out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
993 "missing out_error param: {out}"
994 );
995 assert!(out.contains("return "), "missing return statement: {out}");
996 assert!(out.ends_with("}\n"), "missing closing brace: {out}");
997 }
998
999 #[test]
1000 fn gen_unregistration_fn_returns_empty_when_not_configured() {
1001 let trait_def = make_trait_def("OcrBackend", vec![]);
1002 let bridge_cfg = make_bridge_cfg("OcrBackend", None); let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1005 let spec = make_spec(&trait_def, &bridge_cfg);
1006 let out = generator.gen_unregistration_fn(&spec);
1007
1008 assert!(
1009 out.is_empty(),
1010 "expected empty output when unregister_fn is None, got: {out}"
1011 );
1012 }
1013
1014 #[test]
1015 fn gen_clear_fn_emits_wrapper_when_configured() {
1016 let trait_def = make_trait_def("OcrBackend", vec![]);
1017 let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
1018 bridge_cfg.clear_fn = Some("clear_ocr_backends".to_string());
1019
1020 let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1021 let spec = make_spec(&trait_def, &bridge_cfg);
1022 let out = generator.gen_clear_fn(&spec);
1023
1024 assert!(!out.is_empty(), "expected non-empty output when clear_fn is set");
1025 assert!(out.contains("pub fn clear_ocr_backends("), "wrong function name: {out}");
1026 assert!(out.contains("c.kreuzberg_clear_ocr_backends("), "wrong C symbol: {out}");
1027 assert!(
1028 out.contains("out_error: ?*?[*c]u8") || out.contains("out_error"),
1029 "missing out_error param: {out}"
1030 );
1031 assert!(out.contains("return "), "missing return statement: {out}");
1032 assert!(out.ends_with("}\n"), "missing closing brace: {out}");
1033 }
1034
1035 #[test]
1036 fn gen_clear_fn_returns_empty_when_not_configured() {
1037 let trait_def = make_trait_def("OcrBackend", vec![]);
1038 let bridge_cfg = make_bridge_cfg("OcrBackend", None); let generator = ZigTraitBridgeGenerator::new("kreuzberg");
1041 let spec = make_spec(&trait_def, &bridge_cfg);
1042 let out = generator.gen_clear_fn(&spec);
1043
1044 assert!(
1045 out.is_empty(),
1046 "expected empty output when clear_fn is None, got: {out}"
1047 );
1048 }
1049
1050 #[test]
1051 fn gen_unregistration_fn_uses_snake_case_function_name_verbatim() {
1052 let trait_def = make_trait_def("DocumentExtractor", vec![]);
1054 let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1055 bridge_cfg.unregister_fn = Some("unregister_extractor".to_string());
1056
1057 let generator = ZigTraitBridgeGenerator::new("demo");
1058 let spec = make_spec(&trait_def, &bridge_cfg);
1059 let out = generator.gen_unregistration_fn(&spec);
1060
1061 assert!(
1062 out.contains("pub fn unregister_extractor("),
1063 "must use configured fn name verbatim: {out}"
1064 );
1065 assert!(
1066 out.contains("c.demo_unregister_extractor("),
1067 "must use configured fn name in C symbol: {out}"
1068 );
1069 }
1070
1071 #[test]
1072 fn gen_clear_fn_uses_configured_fn_name_verbatim() {
1073 let trait_def = make_trait_def("DocumentExtractor", vec![]);
1074 let mut bridge_cfg = make_bridge_cfg("DocumentExtractor", None);
1075 bridge_cfg.clear_fn = Some("clear_all_extractors".to_string());
1076
1077 let generator = ZigTraitBridgeGenerator::new("demo");
1078 let spec = make_spec(&trait_def, &bridge_cfg);
1079 let out = generator.gen_clear_fn(&spec);
1080
1081 assert!(
1082 out.contains("pub fn clear_all_extractors("),
1083 "must use configured fn name verbatim: {out}"
1084 );
1085 assert!(
1086 out.contains("c.demo_clear_all_extractors("),
1087 "must use configured fn name in C symbol: {out}"
1088 );
1089 }
1090}