Skip to main content

lingxia_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::parse::Parser;
4use syn::punctuated::Punctuated;
5use syn::{
6    Expr, FnArg, GenericArgument, ItemFn, Lit, LitStr, PatType, Path, PathArguments, Token, Type,
7    parse_macro_input,
8};
9
10#[proc_macro_attribute]
11pub fn host(attr: TokenStream, item: TokenStream) -> TokenStream {
12    let parser = Punctuated::<Expr, Token![,]>::parse_terminated;
13    let args = match parser.parse(attr) {
14        Ok(args) => args,
15        Err(err) => return err.to_compile_error().into(),
16    };
17
18    let (route_lit, mode) = match parse_host_attr(args) {
19        Ok(parsed) => parsed,
20        Err(err) => return err.to_compile_error().into(),
21    };
22
23    let route = route_lit.value();
24    let Some((namespace, method)) = route.rsplit_once('.') else {
25        return syn::Error::new(
26            route_lit.span(),
27            "host route must look like \"namespace.method\"",
28        )
29        .to_compile_error()
30        .into();
31    };
32    if namespace.trim().is_empty() || method.trim().is_empty() {
33        return syn::Error::new(
34            route_lit.span(),
35            "host route must contain non-empty namespace and method",
36        )
37        .to_compile_error()
38        .into();
39    }
40    if namespace == "channel" {
41        return syn::Error::new(
42            route_lit.span(),
43            "host namespace 'channel' is reserved by the JS API; choose a different namespace",
44        )
45        .to_compile_error()
46        .into();
47    }
48
49    let input_fn = parse_macro_input!(item as ItemFn);
50    match mode {
51        HostMode::Stream => expand_stream(route_lit.clone(), namespace, method, input_fn).into(),
52        HostMode::Channel => expand_channel(route_lit.clone(), namespace, method, input_fn).into(),
53        HostMode::Unary => expand_host(route_lit.clone(), namespace, method, mode, input_fn).into(),
54    }
55}
56
57#[proc_macro]
58pub fn register_hosts(input: TokenStream) -> TokenStream {
59    let parser = Punctuated::<Path, Token![,]>::parse_terminated;
60    let paths = parse_macro_input!(input with parser);
61
62    let registrations = match paths
63        .iter()
64        .map(expand_register_host_path)
65        .collect::<syn::Result<Vec<_>>>()
66    {
67        Ok(registrations) => registrations,
68        Err(err) => return err.to_compile_error().into(),
69    };
70
71    quote!({
72        #(#registrations)*
73    })
74    .into()
75}
76
77fn parse_host_attr(args: Punctuated<Expr, Token![,]>) -> syn::Result<(LitStr, HostMode)> {
78    let Some(first) = args.first() else {
79        return Err(syn::Error::new(
80            proc_macro2::Span::call_site(),
81            "expected #[host(\"namespace.method\")]",
82        ));
83    };
84    let Expr::Lit(first_lit) = first else {
85        return Err(syn::Error::new_spanned(
86            first,
87            "expected #[host(\"namespace.method\")]",
88        ));
89    };
90    let Lit::Str(route_lit) = &first_lit.lit else {
91        return Err(syn::Error::new_spanned(
92            &first_lit.lit,
93            "expected #[host(\"namespace.method\")]",
94        ));
95    };
96
97    let mut mode = HostMode::Unary;
98    for arg in args.iter().skip(1) {
99        match arg {
100            Expr::Path(path) if path.path.is_ident("stream") => {
101                if !matches!(mode, HostMode::Unary) {
102                    return Err(syn::Error::new_spanned(
103                        arg,
104                        "duplicate or conflicting mode flag in #[host(...)]",
105                    ));
106                }
107                mode = HostMode::Stream;
108            }
109            Expr::Path(path) if path.path.is_ident("channel") => {
110                if !matches!(mode, HostMode::Unary) {
111                    return Err(syn::Error::new_spanned(
112                        arg,
113                        "duplicate or conflicting mode flag in #[host(...)]",
114                    ));
115                }
116                mode = HostMode::Channel;
117            }
118            _ => {
119                return Err(syn::Error::new_spanned(
120                    arg,
121                    "expected only #[host(\"namespace.method\")], #[host(\"namespace.method\", stream)], or #[host(\"namespace.method\", channel)]",
122                ));
123            }
124        }
125    }
126
127    Ok((route_lit.clone(), mode))
128}
129
130#[derive(Clone, Copy)]
131enum HostMode {
132    Unary,
133    Stream,
134    Channel,
135}
136
137fn expand_register_host_path(path: &Path) -> syn::Result<proc_macro2::TokenStream> {
138    let mut helper_path = path.clone();
139    let Some(last_segment) = helper_path.segments.last_mut() else {
140        return Err(syn::Error::new_spanned(
141            path,
142            "register_hosts! expects a handler function path",
143        ));
144    };
145
146    if !matches!(last_segment.arguments, PathArguments::None) {
147        return Err(syn::Error::new_spanned(
148            &last_segment.arguments,
149            "register_hosts! does not accept generic arguments",
150        ));
151    }
152
153    last_segment.ident = format_ident!("{}_host", last_segment.ident);
154    Ok(quote! {
155        ::lingxia::register_host_entry(#helper_path());
156    })
157}
158
159fn expand_host(
160    route_lit: LitStr,
161    namespace: &str,
162    method: &str,
163    mode: HostMode,
164    input_fn: ItemFn,
165) -> proc_macro2::TokenStream {
166    let fn_ident = input_fn.sig.ident.clone();
167    let helper_ident = format_ident!("{}_host", fn_ident);
168    let handler_ident = format_ident!("__LingxiaHostHandler_{}", fn_ident);
169    let namespace_lit = LitStr::new(namespace, route_lit.span());
170    let method_lit = LitStr::new(method, route_lit.span());
171
172    let call_plan = match HostFnPlan::from_fn(&input_fn) {
173        Ok(plan) => plan,
174        Err(err) => return err.to_compile_error(),
175    };
176
177    let call_expr = call_plan.call_expr(&fn_ident, input_fn.sig.asyncness.is_some());
178    let ctor_ident = match mode {
179        HostMode::Unary => format_ident!("new"),
180        HostMode::Stream => format_ident!("stream"),
181        HostMode::Channel => unreachable!("channel mode is handled by expand_channel"),
182    };
183    let serialize_expr = match mode {
184        HostMode::Unary => quote! {
185            ::lingxia::host::serialize_result(__lingxia_result)
186        },
187        HostMode::Stream => unreachable!("stream mode is handled by expand_stream"),
188        HostMode::Channel => unreachable!("channel mode is handled by expand_channel"),
189    };
190    quote! {
191        #input_fn
192
193        #[doc(hidden)]
194        #[allow(non_camel_case_types)]
195        pub struct #handler_ident;
196
197        impl ::lingxia::host::HostHandler for #handler_ident {
198            fn call<'a>(
199                &'a self,
200                __lingxia_lxapp: std::sync::Arc<::lingxia::LxApp>,
201                __lingxia_input: Option<String>,
202                __lingxia_cancel: ::lingxia::host::HostCancel,
203            ) -> ::lingxia::host::HostFuture<'a> {
204                Box::pin(async move {
205                    let __lingxia_result = #call_expr;
206                    #serialize_expr
207                })
208            }
209        }
210
211        #[doc(hidden)]
212        pub fn #helper_ident() -> ::lingxia::host::HostRegistrationEntry {
213            ::lingxia::host::HostRegistrationEntry::Handler(
214                ::lingxia::host::HostRegistration::#ctor_ident(
215                    #namespace_lit,
216                    #method_lit,
217                    std::sync::Arc::new(#handler_ident),
218                )
219            )
220        }
221    }
222}
223
224struct HostFnPlan {
225    has_lxapp: bool,
226    input_ty: Option<Type>,
227    has_cancel: bool,
228}
229
230impl HostFnPlan {
231    fn from_fn(input_fn: &ItemFn) -> syn::Result<Self> {
232        let mut has_lxapp = false;
233        let mut input_ty = None;
234        let mut has_cancel = false;
235        let input_count = input_fn.sig.inputs.len();
236
237        for (index, arg) in input_fn.sig.inputs.iter().enumerate() {
238            let FnArg::Typed(arg) = arg else {
239                return Err(syn::Error::new_spanned(
240                    arg,
241                    "#[host] does not support methods with a receiver",
242                ));
243            };
244
245            if index == 0 && is_lxapp_arg(arg) {
246                has_lxapp = true;
247                continue;
248            }
249
250            if is_host_cancel_arg(arg) {
251                if index + 1 != input_count {
252                    return Err(syn::Error::new_spanned(
253                        arg,
254                        "HostCancel must be the last argument in a #[host] function",
255                    ));
256                }
257                if has_cancel {
258                    return Err(syn::Error::new_spanned(
259                        arg,
260                        "#[host] functions can only take one HostCancel argument",
261                    ));
262                }
263                has_cancel = true;
264                continue;
265            }
266
267            if input_ty.is_some() {
268                return Err(syn::Error::new_spanned(
269                    arg,
270                    "#[host] functions support at most one JSON payload argument",
271                ));
272            }
273            input_ty = Some((*arg.ty).clone());
274        }
275
276        Ok(Self {
277            has_lxapp,
278            input_ty,
279            has_cancel,
280        })
281    }
282
283    fn call_expr(&self, fn_ident: &syn::Ident, is_async: bool) -> proc_macro2::TokenStream {
284        let mut args = Vec::new();
285        let mut prelude = Vec::new();
286
287        if self.has_lxapp {
288            args.push(quote! { __lingxia_lxapp });
289        }
290
291        if let Some(input_ty) = &self.input_ty {
292            prelude.push(quote! {
293                let __lingxia_payload: #input_ty =
294                    ::lingxia::host::parse_input(__lingxia_input.as_deref())?;
295            });
296            args.push(quote! { __lingxia_payload });
297        }
298
299        if self.has_cancel {
300            args.push(quote! { __lingxia_cancel });
301        }
302
303        let invoke = if is_async {
304            quote! { #fn_ident(#(#args),*).await }
305        } else {
306            quote! { #fn_ident(#(#args),*) }
307        };
308
309        quote! {
310            {
311                #(#prelude)*
312                #invoke
313            }
314        }
315    }
316}
317
318fn is_lxapp_arg(arg: &PatType) -> bool {
319    type_is_arc_lxapp(&arg.ty)
320}
321
322fn is_host_cancel_arg(arg: &PatType) -> bool {
323    type_is_host_cancel(&arg.ty)
324}
325
326fn type_is_arc_lxapp(ty: &Type) -> bool {
327    let Type::Path(type_path) = ty else {
328        return false;
329    };
330    let Some(last_segment) = type_path.path.segments.last() else {
331        return false;
332    };
333    if last_segment.ident != "Arc" {
334        return false;
335    }
336    let PathArguments::AngleBracketed(args) = &last_segment.arguments else {
337        return false;
338    };
339    let Some(GenericArgument::Type(inner_ty)) = args.args.first() else {
340        return false;
341    };
342    type_is_lxapp(inner_ty)
343}
344
345fn type_is_lxapp(ty: &Type) -> bool {
346    let Type::Path(type_path) = ty else {
347        return false;
348    };
349    type_path
350        .path
351        .segments
352        .last()
353        .map(|segment| segment.ident == "LxApp")
354        .unwrap_or(false)
355}
356
357fn type_is_host_cancel(ty: &Type) -> bool {
358    let Type::Path(type_path) = ty else {
359        return false;
360    };
361    type_path
362        .path
363        .segments
364        .last()
365        .map(|segment| segment.ident == "HostCancel")
366        .unwrap_or(false)
367}
368
369fn type_is_stream_context(ty: &Type) -> bool {
370    let Type::Path(type_path) = ty else {
371        return false;
372    };
373    type_path
374        .path
375        .segments
376        .last()
377        .map(|segment| segment.ident == "StreamContext")
378        .unwrap_or(false)
379}
380
381fn type_is_channel_context(ty: &Type) -> bool {
382    let Type::Path(type_path) = ty else {
383        return false;
384    };
385    type_path
386        .path
387        .segments
388        .last()
389        .map(|segment| segment.ident == "ChannelContext")
390        .unwrap_or(false)
391}
392
393fn context_type_args(ty: &Type, expected_ident: &str) -> syn::Result<Vec<Type>> {
394    let Type::Path(type_path) = ty else {
395        return Err(syn::Error::new_spanned(
396            ty,
397            format!("expected `{expected_ident}`"),
398        ));
399    };
400    let Some(last_segment) = type_path.path.segments.last() else {
401        return Err(syn::Error::new_spanned(
402            ty,
403            format!("expected `{expected_ident}`"),
404        ));
405    };
406    if last_segment.ident != expected_ident {
407        return Err(syn::Error::new_spanned(
408            ty,
409            format!("expected `{expected_ident}`"),
410        ));
411    }
412
413    let PathArguments::AngleBracketed(args) = &last_segment.arguments else {
414        return Ok(Vec::new());
415    };
416
417    let mut out = Vec::new();
418    for arg in &args.args {
419        let GenericArgument::Type(ty) = arg else {
420            return Err(syn::Error::new_spanned(
421                arg,
422                format!("`{expected_ident}` only supports type generic arguments"),
423            ));
424        };
425        out.push(ty.clone());
426    }
427    Ok(out)
428}
429
430fn parse_stream_context_types(ty: &Type) -> syn::Result<(Type, Type)> {
431    let args = context_type_args(ty, "StreamContext")?;
432    Ok(match args.len() {
433        0 => (
434            syn::parse_quote!(::lingxia::host::JsonValue),
435            syn::parse_quote!(()),
436        ),
437        1 => (args[0].clone(), syn::parse_quote!(())),
438        2 => (args[0].clone(), args[1].clone()),
439        _ => {
440            return Err(syn::Error::new_spanned(
441                ty,
442                "`StreamContext` supports at most two generic arguments",
443            ));
444        }
445    })
446}
447
448fn parse_channel_context_types(ty: &Type) -> syn::Result<(Type, Type)> {
449    let args = context_type_args(ty, "ChannelContext")?;
450    Ok(match args.len() {
451        0 => (
452            syn::parse_quote!(::lingxia::host::JsonValue),
453            syn::parse_quote!(::lingxia::host::JsonValue),
454        ),
455        1 => (args[0].clone(), args[0].clone()),
456        2 => (args[0].clone(), args[1].clone()),
457        _ => {
458            return Err(syn::Error::new_spanned(
459                ty,
460                "`ChannelContext` supports at most two generic arguments",
461            ));
462        }
463    })
464}
465
466// ===== Stream expansion =====
467
468struct StreamFnPlan {
469    has_lxapp: bool,
470    input_ty: Option<Type>,
471    event_ty: Type,
472    result_ty: Type,
473}
474
475impl StreamFnPlan {
476    fn from_fn(input_fn: &ItemFn) -> syn::Result<Self> {
477        let inputs = &input_fn.sig.inputs;
478
479        let Some(last) = inputs.last() else {
480            return Err(syn::Error::new(
481                proc_macro2::Span::call_site(),
482                "#[host(..., stream)] function must take `StreamContext` as its last argument",
483            ));
484        };
485        let FnArg::Typed(last_arg) = last else {
486            return Err(syn::Error::new_spanned(
487                last,
488                "#[host] does not support methods with a receiver",
489            ));
490        };
491        if !type_is_stream_context(&last_arg.ty) {
492            return Err(syn::Error::new_spanned(
493                last,
494                "last argument of a #[host(..., stream)] function must be `StreamContext`",
495            ));
496        }
497
498        let (event_ty, result_ty) = parse_stream_context_types(&last_arg.ty)?;
499        let mut has_lxapp = false;
500        let mut input_ty = None;
501        let prefix_count = inputs.len() - 1;
502
503        for (index, arg) in inputs.iter().take(prefix_count).enumerate() {
504            let FnArg::Typed(arg) = arg else {
505                return Err(syn::Error::new_spanned(
506                    arg,
507                    "#[host] does not support methods with a receiver",
508                ));
509            };
510            if index == 0 && is_lxapp_arg(arg) {
511                has_lxapp = true;
512                continue;
513            }
514            if input_ty.is_some() {
515                return Err(syn::Error::new_spanned(
516                    arg,
517                    "#[host(stream)] functions support at most one JSON payload argument",
518                ));
519            }
520            input_ty = Some((*arg.ty).clone());
521        }
522
523        Ok(Self {
524            has_lxapp,
525            input_ty,
526            event_ty,
527            result_ty,
528        })
529    }
530
531    fn call_expr(&self, fn_ident: &syn::Ident, is_async: bool) -> proc_macro2::TokenStream {
532        let mut args: Vec<proc_macro2::TokenStream> = Vec::new();
533        let mut prelude: Vec<proc_macro2::TokenStream> = Vec::new();
534
535        if self.has_lxapp {
536            args.push(quote! { __lingxia_lxapp });
537        }
538
539        if let Some(input_ty) = &self.input_ty {
540            prelude.push(quote! {
541                let __lingxia_payload: #input_ty =
542                    ::lingxia::host::parse_input(__lingxia_input.as_deref())?;
543            });
544            args.push(quote! { __lingxia_payload });
545        }
546
547        args.push(quote! { __lingxia_stream });
548
549        let invoke = if is_async {
550            quote! { #fn_ident(#(#args),*).await }
551        } else {
552            quote! { #fn_ident(#(#args),*) }
553        };
554
555        quote! {
556            {
557                #(#prelude)*
558                #invoke
559            }
560        }
561    }
562}
563
564fn expand_stream(
565    route_lit: LitStr,
566    namespace: &str,
567    method: &str,
568    input_fn: ItemFn,
569) -> proc_macro2::TokenStream {
570    let fn_ident = input_fn.sig.ident.clone();
571    let helper_ident = format_ident!("{}_host", fn_ident);
572    let handler_ident = format_ident!("__LingxiaStreamHandler_{}", fn_ident);
573    let namespace_lit = LitStr::new(namespace, route_lit.span());
574    let method_lit = LitStr::new(method, route_lit.span());
575
576    let plan = match StreamFnPlan::from_fn(&input_fn) {
577        Ok(p) => p,
578        Err(err) => return err.to_compile_error(),
579    };
580    let call_expr = plan.call_expr(&fn_ident, input_fn.sig.asyncness.is_some());
581    let event_ty = &plan.event_ty;
582    let result_ty = &plan.result_ty;
583
584    quote! {
585        #input_fn
586
587        #[doc(hidden)]
588        #[allow(non_camel_case_types)]
589        pub struct #handler_ident;
590
591        impl ::lingxia::host::HostHandler for #handler_ident {
592            fn call<'a>(
593                &'a self,
594                __lingxia_lxapp: std::sync::Arc<::lingxia::LxApp>,
595                __lingxia_input: Option<String>,
596                __lingxia_cancel: ::lingxia::host::HostCancel,
597            ) -> ::lingxia::host::HostFuture<'a> {
598                Box::pin(async move {
599                    let (__lingxia_stream, __lingxia_rx) =
600                        ::lingxia::host::new_stream_context::<#event_ty, #result_ty>(__lingxia_cancel);
601                    let __lingxia_error_tx = __lingxia_stream.error_sender();
602
603                    ::lingxia::tokio::task::spawn(async move {
604                        let __lingxia_result: ::lingxia::host::HostResult<()> = {
605                            let __lingxia_lxapp = __lingxia_lxapp;
606                            let __lingxia_input = __lingxia_input;
607                            let __lingxia_stream = __lingxia_stream;
608                            #call_expr
609                        };
610                        if let Err(err) = __lingxia_result {
611                            let _ = __lingxia_error_tx.send(Err(err));
612                        }
613                    });
614
615                    Ok(::lingxia::host::stream_output_from_rx(__lingxia_rx))
616                })
617            }
618        }
619
620        #[doc(hidden)]
621        pub fn #helper_ident() -> ::lingxia::host::HostRegistrationEntry {
622            ::lingxia::host::HostRegistrationEntry::Handler(
623                ::lingxia::host::HostRegistration::stream(
624                    #namespace_lit,
625                    #method_lit,
626                    std::sync::Arc::new(#handler_ident),
627                )
628            )
629        }
630    }
631}
632
633// ===== Channel expansion =====
634
635struct ChannelFnPlan {
636    has_lxapp: bool,
637    input_ty: Option<Type>,
638    inbound_ty: Type,
639    outbound_ty: Type,
640}
641
642impl ChannelFnPlan {
643    fn from_fn(input_fn: &ItemFn) -> syn::Result<Self> {
644        let inputs = &input_fn.sig.inputs;
645
646        // Last argument must be ChannelContext.
647        let Some(last) = inputs.last() else {
648            return Err(syn::Error::new(
649                proc_macro2::Span::call_site(),
650                "#[host(..., channel)] function must take `ChannelContext` as its last argument",
651            ));
652        };
653        let FnArg::Typed(last_arg) = last else {
654            return Err(syn::Error::new_spanned(
655                last,
656                "#[host] does not support methods with a receiver",
657            ));
658        };
659        if !type_is_channel_context(&last_arg.ty) {
660            return Err(syn::Error::new_spanned(
661                last,
662                "last argument of a #[host(..., channel)] function must be `ChannelContext`",
663            ));
664        }
665
666        let (inbound_ty, outbound_ty) = parse_channel_context_types(&last_arg.ty)?;
667
668        let mut has_lxapp = false;
669        let mut input_ty = None;
670        let prefix_count = inputs.len() - 1;
671
672        for (index, arg) in inputs.iter().take(prefix_count).enumerate() {
673            let FnArg::Typed(arg) = arg else {
674                return Err(syn::Error::new_spanned(
675                    arg,
676                    "#[host] does not support methods with a receiver",
677                ));
678            };
679            if index == 0 && is_lxapp_arg(arg) {
680                has_lxapp = true;
681                continue;
682            }
683            if input_ty.is_some() {
684                return Err(syn::Error::new_spanned(
685                    arg,
686                    "#[host(channel)] functions support at most one JSON payload argument",
687                ));
688            }
689            input_ty = Some((*arg.ty).clone());
690        }
691
692        Ok(Self {
693            has_lxapp,
694            input_ty,
695            inbound_ty,
696            outbound_ty,
697        })
698    }
699
700    fn call_expr(&self, fn_ident: &syn::Ident, is_async: bool) -> proc_macro2::TokenStream {
701        let mut args: Vec<proc_macro2::TokenStream> = Vec::new();
702        let mut prelude: Vec<proc_macro2::TokenStream> = Vec::new();
703
704        if self.has_lxapp {
705            args.push(quote! { __lingxia_lxapp });
706        }
707
708        if let Some(input_ty) = &self.input_ty {
709            prelude.push(quote! {
710                let __lingxia_payload: #input_ty =
711                    match ::lingxia::host::parse_input(__lingxia_input.as_deref()) {
712                        Ok(v) => v,
713                        Err(e) => {
714                            __lingxia_ctx.close_with("INVALID_PARAMS", e.to_string());
715                            return;
716                        }
717                    };
718            });
719            args.push(quote! { __lingxia_payload });
720        }
721
722        args.push(quote! { __lingxia_ctx });
723
724        if is_async {
725            quote! {
726                {
727                    #(#prelude)*
728                    #fn_ident(#(#args),*).await
729                }
730            }
731        } else {
732            quote! {
733                {
734                    #(#prelude)*
735                    #fn_ident(#(#args),*)
736                }
737            }
738        }
739    }
740}
741
742fn expand_channel(
743    route_lit: LitStr,
744    namespace: &str,
745    method: &str,
746    input_fn: ItemFn,
747) -> proc_macro2::TokenStream {
748    let fn_ident = input_fn.sig.ident.clone();
749    let helper_ident = format_ident!("{}_host", fn_ident);
750    let handler_ident = format_ident!("__LingxiaChannelHandler_{}", fn_ident);
751    let namespace_lit = LitStr::new(namespace, route_lit.span());
752    let method_lit = LitStr::new(method, route_lit.span());
753
754    let plan = match ChannelFnPlan::from_fn(&input_fn) {
755        Ok(p) => p,
756        Err(err) => return err.to_compile_error(),
757    };
758
759    let call_expr = plan.call_expr(&fn_ident, input_fn.sig.asyncness.is_some());
760    let inbound_ty = &plan.inbound_ty;
761    let outbound_ty = &plan.outbound_ty;
762
763    quote! {
764        #input_fn
765
766        #[doc(hidden)]
767        #[allow(non_camel_case_types)]
768        pub struct #handler_ident;
769
770        impl ::lingxia::host::ChannelHandler for #handler_ident {
771            #[allow(unused_variables)]
772            fn on_open(
773                &self,
774                __lingxia_lxapp: std::sync::Arc<::lingxia::LxApp>,
775                __lingxia_ctx: ::lingxia::host::ChannelContext,
776                __lingxia_input: Option<String>,
777            ) {
778                ::lingxia::tokio::task::spawn(async move {
779                    let __lingxia_ctx =
780                        __lingxia_ctx.with_types::<#inbound_ty, #outbound_ty>();
781                    #call_expr
782                });
783            }
784        }
785
786        #[doc(hidden)]
787        pub fn #helper_ident() -> ::lingxia::host::HostRegistrationEntry {
788            ::lingxia::host::HostRegistrationEntry::Channel(
789                ::lingxia::host::ChannelRegistration::new(
790                    #namespace_lit,
791                    #method_lit,
792                    std::sync::Arc::new(#handler_ident),
793                )
794            )
795        }
796    }
797}