1use minijinja::context;
7
8use alef_codegen::generators::trait_bridge::{
9 BridgeOutput, TraitBridgeGenerator, TraitBridgeSpec, bridge_param_type as param_type, gen_bridge_all,
10 visitor_param_type,
11};
12use alef_core::config::TraitBridgeConfig;
13use alef_core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
14use std::collections::HashMap;
15
16pub use alef_codegen::generators::trait_bridge::find_bridge_param;
21
22pub struct PhpBridgeGenerator {
25 pub core_import: String,
27 pub type_paths: HashMap<String, String>,
29 pub error_type: String,
31}
32
33impl TraitBridgeGenerator for PhpBridgeGenerator {
34 fn foreign_object_type(&self) -> &str {
35 "*mut ext_php_rs::types::ZendObject"
36 }
37
38 fn bridge_imports(&self) -> Vec<String> {
39 vec!["std::sync::Arc".to_string()]
40 }
41
42 fn gen_sync_method_body(&self, method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
43 let name = &method.name;
44
45 let has_args = !method.params.is_empty();
46 let args_expr = if has_args {
47 let mut args_parts = Vec::new();
48 for p in &method.params {
49 let arg_expr = match &p.ty {
50 TypeRef::String => format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name),
51 TypeRef::Path => format!(
52 "ext_php_rs::types::Zval::try_from({}.to_string_lossy().to_string()).unwrap_or_default()",
53 p.name
54 ),
55 TypeRef::Bytes => format!(
56 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
57 p.name
58 ),
59 TypeRef::Named(_) => {
60 format!(
61 "ext_php_rs::types::Zval::try_from(serde_json::to_string(&{}).unwrap_or_default()).unwrap_or_default()",
62 p.name
63 )
64 }
65 TypeRef::Primitive(_) => {
66 format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name)
67 }
68 _ => format!(
69 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
70 p.name
71 ),
72 };
73 args_parts.push(arg_expr);
74 }
75 let args_array = format!("[{}]", args_parts.join(", "));
76 format!(
77 "{}.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()",
78 args_array
79 )
80 } else {
81 "vec![]".to_string()
82 };
83
84 let is_result_type = method.error_type.is_some();
85 let is_unit_return = matches!(method.return_type, TypeRef::Unit);
86
87 crate::template_env::render(
88 "sync_method_body.jinja",
89 context! {
90 method_name => name,
91 args_expr => args_expr,
92 is_result_type => is_result_type,
93 is_unit_return => is_unit_return,
94 core_import => &self.core_import,
95 },
96 )
97 }
98
99 fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
100 let name = &method.name;
101
102 let string_params: Vec<String> = method
103 .params
104 .iter()
105 .filter(|p| matches!(&p.ty, TypeRef::String))
106 .map(|p| p.name.clone())
107 .collect();
108
109 let has_args = !method.params.is_empty();
110 let args_expr = if has_args {
111 let mut args_parts = Vec::new();
112 for p in &method.params {
113 let arg_expr = match &p.ty {
114 TypeRef::String => format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name),
115 TypeRef::Path => format!(
116 "ext_php_rs::types::Zval::try_from({}.to_string_lossy().to_string()).unwrap_or_default()",
117 p.name
118 ),
119 TypeRef::Bytes => format!(
120 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
121 p.name
122 ),
123 TypeRef::Named(_) => {
124 format!(
125 "ext_php_rs::types::Zval::try_from(serde_json::to_string(&{}).unwrap_or_default()).unwrap_or_default()",
126 p.name
127 )
128 }
129 TypeRef::Primitive(_) => {
130 format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name)
131 }
132 _ => format!(
133 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
134 p.name
135 ),
136 };
137 args_parts.push(arg_expr);
138 }
139 let args_array = format!("[{}]", args_parts.join(", "));
140 format!(
141 "{}.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()",
142 args_array
143 )
144 } else {
145 "vec![]".to_string()
146 };
147
148 let is_result_type = method.error_type.is_some();
149
150 crate::template_env::render(
151 "async_method_body.jinja",
152 context! {
153 method_name => name,
154 args_expr => args_expr,
155 string_params => string_params,
156 is_result_type => is_result_type,
157 core_import => &spec.core_import,
158 },
159 )
160 }
161
162 fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
163 let wrapper = spec.wrapper_name();
164
165 crate::template_env::render(
166 "bridge_constructor.jinja",
167 context! {
168 wrapper => &wrapper,
169 },
170 )
171 }
172
173 fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
174 let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
175 return String::new();
176 };
177 let host_path = alef_codegen::generators::trait_bridge::host_function_path(spec, unregister_fn);
178
179 crate::template_env::render(
180 "bridge_unregister_fn.jinja",
181 context! {
182 unregister_fn => unregister_fn,
183 host_path => &host_path,
184 },
185 )
186 }
187
188 fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
189 let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
190 return String::new();
191 };
192 let host_path = alef_codegen::generators::trait_bridge::host_function_path(spec, clear_fn);
193
194 crate::template_env::render(
195 "bridge_clear_fn.jinja",
196 context! {
197 clear_fn => clear_fn,
198 host_path => &host_path,
199 },
200 )
201 }
202
203 fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
204 let Some(register_fn) = spec.bridge_config.register_fn.as_deref() else {
205 return String::new();
206 };
207 let Some(registry_getter) = spec.bridge_config.registry_getter.as_deref() else {
208 return String::new();
209 };
210 let wrapper = spec.wrapper_name();
211 let trait_path = spec.trait_path();
212
213 let req_methods: Vec<&MethodDef> = spec.required_methods();
214 let required_methods: Vec<minijinja::Value> = req_methods
215 .iter()
216 .map(|m| {
217 minijinja::context! {
218 name => m.name.as_str(),
219 }
220 })
221 .collect();
222
223 let extra_args = spec
224 .bridge_config
225 .register_extra_args
226 .as_deref()
227 .map(|a| format!(", {a}"))
228 .unwrap_or_default();
229
230 crate::template_env::render(
231 "bridge_registration_fn.jinja",
232 context! {
233 register_fn => register_fn,
234 required_methods => required_methods,
235 wrapper => &wrapper,
236 trait_path => &trait_path,
237 registry_getter => registry_getter,
238 extra_args => &extra_args,
239 },
240 )
241 }
242}
243
244pub fn gen_trait_bridge(
246 trait_type: &TypeDef,
247 bridge_cfg: &TraitBridgeConfig,
248 core_import: &str,
249 error_type: &str,
250 error_constructor: &str,
251 api: &ApiSurface,
252) -> BridgeOutput {
253 let type_paths: HashMap<String, String> = api
255 .types
256 .iter()
257 .map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
258 .chain(
259 api.enums
260 .iter()
261 .map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
262 )
263 .chain(
266 api.excluded_type_paths
267 .iter()
268 .map(|(name, path)| (name.clone(), path.replace('-', "_"))),
269 )
270 .collect();
271
272 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
274 && bridge_cfg.register_fn.is_none()
275 && bridge_cfg.super_trait.is_none()
276 && trait_type.methods.iter().all(|m| m.has_default_impl);
277
278 if is_visitor_bridge {
279 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
280 let trait_path = trait_type.rust_path.replace('-', "_");
281 let code = gen_visitor_bridge(trait_type, bridge_cfg, &struct_name, &trait_path, &type_paths);
282 BridgeOutput { imports: vec![], code }
283 } else {
284 let generator = PhpBridgeGenerator {
286 core_import: core_import.to_string(),
287 type_paths: type_paths.clone(),
288 error_type: error_type.to_string(),
289 };
290 let spec = TraitBridgeSpec {
291 trait_def: trait_type,
292 bridge_config: bridge_cfg,
293 core_import,
294 wrapper_prefix: "Php",
295 type_paths,
296 error_type: error_type.to_string(),
297 error_constructor: error_constructor.to_string(),
298 };
299 gen_bridge_all(&spec, &generator)
300 }
301}
302
303fn gen_visitor_bridge(
308 trait_type: &TypeDef,
309 bridge_cfg: &TraitBridgeConfig,
310 struct_name: &str,
311 trait_path: &str,
312 type_paths: &HashMap<String, String>,
313) -> String {
314 let mut out = String::with_capacity(4096);
315 let core_crate = trait_path
316 .split("::")
317 .next()
318 .filter(|s| !s.is_empty())
319 .unwrap_or_else(|| panic!("trait_path '{trait_path}' must be a qualified path of the form 'crate_name::...'; configure extension_name in alef.toml"))
320 .to_string();
321
322 out.push_str(&crate::template_env::render(
324 "visitor_nodecontext_helper.jinja",
325 context! {
326 core_crate => &core_crate,
327 },
328 ));
329 out.push('\n');
330
331 out.push_str(&crate::template_env::render(
333 "visitor_zval_to_visitresult.jinja",
334 context! {
335 core_crate => &core_crate,
336 },
337 ));
338 out.push('\n');
339
340 out.push_str(&crate::template_env::render(
342 "php_visit_result_with_template.jinja",
343 context! {
344 core_crate => &core_crate,
345 },
346 ));
347 out.push_str("\n\n");
348
349 out.push_str(&crate::template_env::render(
351 "visitor_bridge_struct.jinja",
352 context! {
353 struct_name => struct_name,
354 },
355 ));
356 out.push('\n');
357
358 out.push_str(&crate::template_env::render(
360 "php_trait_impl_start.jinja",
361 context! {
362 trait_path => &trait_path,
363 struct_name => struct_name,
364 },
365 ));
366 for method in &trait_type.methods {
367 if method.trait_source.is_some() {
368 continue;
369 }
370 gen_visitor_method_php(&mut out, method, bridge_cfg, type_paths);
371 }
372 out.push_str("}\n");
373 out.push('\n');
374
375 out
376}
377
378fn gen_visitor_method_php(
380 out: &mut String,
381 method: &MethodDef,
382 bridge_cfg: &TraitBridgeConfig,
383 type_paths: &HashMap<String, String>,
384) {
385 let name = &method.name;
386
387 let mut sig_parts = vec!["&mut self".to_string()];
388 for p in &method.params {
389 let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
390 sig_parts.push(format!("{}: {}", p.name, ty_str));
391 }
392 let sig = sig_parts.join(", ");
393
394 let ret_ty = match &method.return_type {
395 TypeRef::Named(n) => type_paths.get(n).cloned().unwrap_or_else(|| n.clone()),
396 other => param_type(other, "", false, type_paths),
397 };
398
399 out.push_str(&crate::template_env::render(
400 "php_visitor_method_signature.jinja",
401 context! {
402 name => name,
403 sig => &sig,
404 ret_ty => &ret_ty,
405 },
406 ));
407
408 out.push_str(" // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n");
410 out.push_str(" let php_obj_ref = unsafe { &mut *self.php_obj };\n");
411
412 let has_args = !method.params.is_empty();
414 if has_args {
415 out.push_str(" let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n");
416 for p in &method.params {
417 if let TypeRef::Named(n) = &p.ty {
418 if Some(n.as_str()) == bridge_cfg.context_type.as_deref() {
419 out.push_str(&crate::template_env::render(
420 "php_visitor_arg_nodecontext.jinja",
421 context! {
422 name => &p.name,
423 ref => if p.is_ref { "" } else { "&" },
424 },
425 ));
426 out.push('\n');
427 continue;
428 }
429 }
430 if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
433 out.push_str(&crate::template_env::render(
434 "php_visitor_arg_optional_string_ref.jinja",
435 context! {
436 name => &p.name,
437 },
438 ));
439 out.push('\n');
440 continue;
441 }
442 if matches!(&p.ty, TypeRef::String) {
443 if p.is_ref {
444 out.push_str(&crate::template_env::render(
445 "php_visitor_arg_string_ref.jinja",
446 context! {
447 name => &p.name,
448 },
449 ));
450 } else {
451 out.push_str(&crate::template_env::render(
452 "php_visitor_arg_string_owned.jinja",
453 context! {
454 name => &p.name,
455 },
456 ));
457 }
458 out.push('\n');
459 continue;
460 }
461 if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
462 out.push_str(&crate::template_env::render(
463 "php_visitor_arg_bool.jinja",
464 context! {
465 name => &p.name,
466 },
467 ));
468 out.push('\n');
469 continue;
470 }
471 out.push_str(&crate::template_env::render(
473 "php_visitor_arg_default.jinja",
474 context! {
475 name => &p.name,
476 },
477 ));
478 out.push('\n');
479 }
480 }
481
482 if has_args {
486 out.push_str(" let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect();\n");
487 }
488 let args_expr = if has_args { "dyn_args" } else { "vec![]" };
489 out.push_str(&crate::template_env::render(
490 "php_visitor_method_php_call.jinja",
491 context! {
492 name => name,
493 args_expr => args_expr,
494 },
495 ));
496
497 let mut tmpl_var_names: Vec<String> = Vec::new();
500 for p in &method.params {
501 if let TypeRef::Named(n) = &p.ty {
502 if Some(n.as_str()) == bridge_cfg.context_type.as_deref() {
503 continue;
504 }
505 }
506 if matches!(&p.ty, TypeRef::Vec(_)) {
508 continue;
509 }
510 let key = p.name.strip_prefix('_').unwrap_or(&p.name);
512 let owned_var = format!("_{key}_s");
513 let expr: String = if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
514 format!("{}.map(|s| s.to_string()).unwrap_or_default()", p.name)
515 } else if matches!(&p.ty, TypeRef::String) && p.is_ref {
516 format!("{}.to_string()", p.name)
517 } else if matches!(&p.ty, TypeRef::String) {
518 format!("{}.clone()", p.name)
519 } else if matches!(&p.ty, TypeRef::Optional(_)) {
520 format!("{}.map(|v| v.to_string()).unwrap_or_default()", p.name)
521 } else {
522 format!("{}.to_string()", p.name)
523 };
524 out.push_str(&crate::template_env::render(
525 "php_visitor_template_var_let_binding.jinja",
526 context! {
527 owned_var => &owned_var,
528 expr => &expr,
529 },
530 ));
531 out.push('\n');
532 tmpl_var_names.push(format!("(\"{key}\", {owned_var}.as_str())"));
533 }
534 let tmpl_vars_expr = if tmpl_var_names.is_empty() {
535 "&[]".to_string()
536 } else {
537 format!("&[{}]", tmpl_var_names.join(", "))
538 };
539
540 out.push_str(&crate::template_env::render(
542 "php_visitor_method_result_match.jinja",
543 context! {
544 ret_ty => &ret_ty,
545 tmpl_vars_expr => &tmpl_vars_expr,
546 },
547 ));
548 out.push('\n');
549}
550
551#[allow(clippy::too_many_arguments)]
554pub fn gen_bridge_function(
555 func: &alef_core::ir::FunctionDef,
556 bridge_param_idx: usize,
557 bridge_cfg: &TraitBridgeConfig,
558 mapper: &dyn alef_codegen::type_mapper::TypeMapper,
559 opaque_types: &ahash::AHashSet<String>,
560 core_import: &str,
561) -> String {
562 use alef_core::ir::TypeRef;
563
564 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
565 let handle_path = format!("{core_import}::visitor::VisitorHandle");
566 let param_name = &func.params[bridge_param_idx].name;
567 let bridge_param = &func.params[bridge_param_idx];
568 let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
569
570 let mut sig_parts = Vec::new();
572 for (idx, p) in func.params.iter().enumerate() {
573 if idx == bridge_param_idx {
574 let php_obj_ty = "&mut ext_php_rs::types::ZendObject";
578 if is_optional {
579 sig_parts.push(format!("{}: Option<{php_obj_ty}>", p.name));
580 } else {
581 sig_parts.push(format!("{}: {php_obj_ty}", p.name));
582 }
583 } else {
584 let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
585 let base = mapper.map_type(&p.ty);
586 let ty = match &p.ty {
589 TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
590 if p.optional || promoted {
591 format!("Option<&mut {base}>")
592 } else {
593 format!("&mut {base}")
594 }
595 }
596 TypeRef::Optional(inner) => {
597 if let TypeRef::Named(n) = inner.as_ref() {
598 if !opaque_types.contains(n.as_str()) {
599 format!("Option<&mut {base}>")
600 } else if p.optional || promoted {
601 format!("Option<{base}>")
602 } else {
603 base
604 }
605 } else if p.optional || promoted {
606 format!("Option<{base}>")
607 } else {
608 base
609 }
610 }
611 _ => {
612 if p.optional || promoted {
613 format!("Option<{base}>")
614 } else {
615 base
616 }
617 }
618 };
619 sig_parts.push(format!("{}: {}", p.name, ty));
620 }
621 }
622
623 let params_str = sig_parts.join(", ");
624 let return_type = mapper.map_type(&func.return_type);
625 let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
626
627 let err_conv = ".map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))";
628
629 let bridge_wrap = if is_optional {
631 format!(
632 "let {param_name} = {param_name}.map(|v| {{\n \
633 let bridge = {struct_name}::new(v);\n \
634 std::sync::Arc::new(std::sync::Mutex::new(bridge)) as {handle_path}\n \
635 }});"
636 )
637 } else {
638 format!(
639 "let {param_name} = {{\n \
640 let bridge = {struct_name}::new({param_name});\n \
641 std::sync::Arc::new(std::sync::Mutex::new(bridge)) as {handle_path}\n \
642 }};"
643 )
644 };
645
646 let serde_bindings: String = func
648 .params
649 .iter()
650 .enumerate()
651 .filter(|(idx, p)| {
652 if *idx == bridge_param_idx {
653 return false;
654 }
655 let named = match &p.ty {
656 TypeRef::Named(n) => Some(n.as_str()),
657 TypeRef::Optional(inner) => {
658 if let TypeRef::Named(n) = inner.as_ref() {
659 Some(n.as_str())
660 } else {
661 None
662 }
663 }
664 _ => None,
665 };
666 named.is_some_and(|n| !opaque_types.contains(n))
667 })
668 .map(|(_, p)| {
669 let name = &p.name;
670 let core_path = format!(
671 "{core_import}::{}",
672 match &p.ty {
673 TypeRef::Named(n) => n.clone(),
674 TypeRef::Optional(inner) =>
675 if let TypeRef::Named(n) = inner.as_ref() {
676 n.clone()
677 } else {
678 String::new()
679 },
680 _ => String::new(),
681 }
682 );
683 if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
684 format!(
685 "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n \
686 let json = serde_json::to_string(&v){err_conv}?;\n \
687 serde_json::from_str(&json){err_conv}\n \
688 }}).transpose()?;\n "
689 )
690 } else {
691 format!(
692 "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n \
693 let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n "
694 )
695 }
696 })
697 .collect();
698
699 let call_args: Vec<String> = func
701 .params
702 .iter()
703 .enumerate()
704 .map(|(idx, p)| {
705 if idx == bridge_param_idx {
706 return p.name.clone();
707 }
708 match &p.ty {
709 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
710 if p.optional {
711 format!("{}.as_ref().map(|v| &v.inner)", p.name)
712 } else {
713 format!("&{}.inner", p.name)
714 }
715 }
716 TypeRef::Named(_) => format!("{}_core", p.name),
717 TypeRef::Optional(inner) => {
718 if let TypeRef::Named(n) = inner.as_ref() {
719 if opaque_types.contains(n.as_str()) {
720 format!("{}.as_ref().map(|v| &v.inner)", p.name)
721 } else {
722 format!("{}_core", p.name)
723 }
724 } else {
725 p.name.clone()
726 }
727 }
728 TypeRef::String | TypeRef::Char => {
729 if p.is_ref {
730 format!("&{}", p.name)
731 } else {
732 p.name.clone()
733 }
734 }
735 _ => p.name.clone(),
736 }
737 })
738 .collect();
739 let call_args_str = call_args.join(", ");
740
741 let core_fn_path = {
742 let path = func.rust_path.replace('-', "_");
743 if path.starts_with(core_import) {
744 path
745 } else {
746 format!("{core_import}::{}", func.name)
747 }
748 };
749 let core_call = format!("{core_fn_path}({call_args_str})");
750
751 let return_wrap = match &func.return_type {
752 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
753 format!("{name} {{ inner: std::sync::Arc::new(val) }}")
754 }
755 TypeRef::Named(_) => "val.into()".to_string(),
756 TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
757 _ => "val".to_string(),
758 };
759
760 let body = if func.error_type.is_some() {
761 if return_wrap == "val" {
762 format!("{bridge_wrap}\n {serde_bindings}{core_call}{err_conv}")
763 } else {
764 format!("{bridge_wrap}\n {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
765 }
766 } else {
767 format!("{bridge_wrap}\n {serde_bindings}{core_call}")
768 };
769
770 let func_name = &func.name;
771 let mut out = String::with_capacity(1024);
772 if func.error_type.is_some() {
773 out.push_str("#[allow(clippy::missing_errors_doc)]\n");
774 }
775 out.push_str(&crate::template_env::render(
776 "php_bridge_function_definition.jinja",
777 context! {
778 func_name => func_name,
779 params_str => ¶ms_str,
780 ret => &ret,
781 body => &body,
782 },
783 ));
784
785 out
786}