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