mod global_info;
extern crate proc_macro;
use global_info::*;
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::{format_ident, quote};
use syn::{
parse_macro_input,
FnArg,
GenericArgument,
ItemImpl,
ItemTrait,
LitInt,
Meta,
Pat,
PathArguments,
ReturnType,
TraitItem,
TraitItemFn,
Type,
};
const CONNECT: i32 = 2147483647;
const DISCONNECT: i32 = 2147483646;
const CLOSED: i32 = 2147483645;
fn have_tag(method: &TraitItemFn) -> Option<i32> {
for attr in &method.attrs {
if !attr.path().is_ident("tag") {
continue; }
let Meta::List(ref list) = attr.meta else {
panic!(
"netxbuilder: #[tag] must be written as #[tag(...)], \
e.g. #[tag(1000)] or #[tag(connect)]"
);
};
if let Ok(lit) = syn::parse2::<LitInt>(list.tokens.clone()) {
return Some(
lit.base10_parse::<i32>()
.expect("netxbuilder: #[tag] integer value must fit in i32"),
);
}
if let Ok(ident) = syn::parse2::<syn::Ident>(list.tokens.clone()) {
return Some(match ident.to_string().as_str() {
"connect" => CONNECT,
"disconnect" => DISCONNECT,
"closed" => CLOSED,
other => panic!(
"netxbuilder: unknown lifecycle tag '{}' — \
expected one of: connect, disconnect, closed",
other
),
});
}
panic!(
"netxbuilder: #[tag] argument must be either an i32 literal \
(e.g. #[tag(1000)]) or a lifecycle keyword \
(connect | disconnect | closed)"
);
}
None
}
fn get_function_tt(tag_id: i32, func_name: &str, rt: &Type) -> u8 {
let Type::Path(tp) = rt else {
panic!(
"netxbuilder: tag {} fn '{}' — return type must be Result<T> or Result<()>, \
got a non-path type",
tag_id, func_name
)
};
for seg in &tp.path.segments {
if seg.ident != "Result" {
continue;
}
return match &seg.arguments {
PathArguments::AngleBracketed(arg) if arg.args.len() == 1 => {
match &arg.args[0] {
GenericArgument::Type(Type::Tuple(t)) if t.elems.is_empty() => 1,
_ => 2,
}
}
PathArguments::AngleBracketed(_) => panic!(
"netxbuilder: tag {} fn '{}' — Result must have exactly one \
type parameter, e.g. Result<T> or Result<()>",
tag_id, func_name
),
_ => panic!(
"netxbuilder: tag {} fn '{}' — Result must be generic, \
e.g. Result<T> or Result<()>",
tag_id, func_name
),
};
}
panic!(
"netxbuilder: tag {} fn '{}' — could not find a `Result` type in the \
return type; tagged methods must return Result<T> or Result<()>",
tag_id, func_name
)
}
fn get_impl_func(funcs: &[FuncInfo], call_macro: &Ident) -> Vec<proc_macro2::TokenStream> {
funcs
.iter()
.map(|func| {
let fn_name = format_ident!("{}", func.func_name);
let inputs = &func.inputs; let output = &func.output; let input_names = &func.input_names; let tag = func.tag;
match func.tt {
0 => quote! {
async fn #fn_name(#inputs) {
#call_macro!(@run_not_err self.client=>#tag;#(#input_names ,)*);
}
},
1 => quote! {
async fn #fn_name(#inputs) #output {
#call_macro!(@checkrun self.client=>#tag;#(#input_names ,)*);
Ok(())
}
},
2 => quote! {
async fn #fn_name(#inputs) #output {
Ok(#call_macro!(self.client=>#tag;#(#input_names ,)*))
}
},
tt => panic!(
"netxbuilder: internal error — unexpected call type tt={} \
for fn '{}'",
tt, func.func_name
),
}
})
.collect()
}
fn make_dispatch_arms(
funcs: &[FuncInfo],
interface_name: &syn::Ident,
) -> Vec<proc_macro2::TokenStream> {
funcs
.iter()
.map(|func| {
let func_name = format_ident!("{}", func.func_name);
let tag = func.tag;
let mut arg_names: Vec<proc_macro2::Ident> = Vec::new();
let mut read_token: Vec<proc_macro2::TokenStream> = Vec::new();
for (index, token) in func.args_type.iter().enumerate() {
let arg_name = format_ident!("arg{}", index);
read_token.push(quote! {
let #arg_name = data.pack_deserialize::<#token>()?;
});
arg_names.push(arg_name);
}
let args_len = func.args_type.len();
let call = match func.tt {
0 => quote! {
::anyhow::ensure!(tt == 0, "cmd:{} tt:{} !=0", #tag, tt);
let args_len = data.read_fixed::<u32>()? as usize;
::anyhow::ensure!(
args_len == #args_len,
"cmd:{} args len error: expected {}, got {}",
#tag, #args_len, args_len
);
#(#read_token)*
#interface_name::#func_name(self, #(#arg_names,)*).await;
Ok(RetResult::success())
},
1 => quote! {
::anyhow::ensure!(tt == 1, "cmd:{} tt:{} !=1", #tag, tt);
let args_len = data.read_fixed::<u32>()? as usize;
::anyhow::ensure!(
args_len == #args_len,
"cmd:{} args len error: expected {}, got {}",
#tag, #args_len, args_len
);
#(#read_token)*
#interface_name::#func_name(self, #(#arg_names,)*).await?;
Ok(RetResult::success())
},
2 => quote! {
::anyhow::ensure!(tt == 2, "cmd:{} tt:{} !=2", #tag, tt);
let args_len = data.read_fixed::<u32>()? as usize;
::anyhow::ensure!(
args_len == #args_len,
"cmd:{} args len error: expected {}, got {}",
#tag, #args_len, args_len
);
#(#read_token)*
let ret = #interface_name::#func_name(self, #(#arg_names,)*).await?;
let mut result = RetResult::success();
result.add_arg_buff(ret);
Ok(result)
},
_ => quote! { unimplemented!() },
};
quote! {
#tag => { #call }
}
})
.collect()
}
fn make_special_fallback() -> proc_macro2::TokenStream {
quote! {
_ => match cmd_tag {
#CONNECT => Ok(RetResult::success()),
#DISCONNECT => Ok(RetResult::success()),
#CLOSED => Ok(RetResult::success()),
_ => ::anyhow::bail!("not found cmd tag:{}", cmd_tag),
}
}
}
fn get_funcs_info(ast: &ItemTrait) -> Vec<FuncInfo> {
let mut funcs = Vec::new();
for item in &ast.items {
let TraitItem::Fn(method) = item else {
continue; };
let Some(tag_id) = have_tag(method) else {
continue; };
let sig = &method.sig;
assert!(
sig.asyncness.is_some(),
"netxbuilder: method '{}' is tagged with #[tag({})] but is not async — \
remote-callable methods must be declared `async fn`",
sig.ident,
tag_id
);
let func_name = sig.ident.to_string();
let mut args_type = Vec::new();
let mut input_names = Vec::new();
for arg in &sig.inputs {
let FnArg::Typed(pat_type) = arg else {
continue;
};
let ty = &pat_type.ty;
args_type.push(quote!(#ty));
let Pat::Ident(a) = &*pat_type.pat else {
panic!(
"netxbuilder: method '{}' — all parameters must use plain \
identifier patterns, e.g. `name: String` not `(a, b): (i32, i32)`",
func_name
)
};
input_names.push(a.ident.clone());
}
let tt = match &sig.output {
ReturnType::Default => 0, ReturnType::Type(_, ty) => get_function_tt(tag_id, &func_name, ty),
};
funcs.push(FuncInfo {
tag: tag_id,
tt,
func_name,
args_type,
inputs: sig.inputs.clone(),
input_names,
output: sig.output.clone(),
});
}
funcs
}
#[proc_macro_attribute]
pub fn build_client(args: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as ItemTrait);
let funcs = get_funcs_info(&ast);
let controller_name = args.to_string();
let interface_name = &ast.ident;
let proxy_name = format_ident!("___impl_{}_call", interface_name);
let call_macro = format_ident!("call");
let impl_func = get_impl_func(&funcs, &call_macro);
let impl_interface = quote! {
#[allow(non_camel_case_types)]
pub struct #proxy_name<T> {
client: T,
}
impl<T> #proxy_name<T> {
pub fn new(client: T) -> #proxy_name<T> {
#proxy_name { client }
}
}
impl<T: SessionSave + 'static> #proxy_name<std::sync::Arc<Actor<NetXClient<T>>>> {
pub fn new_impl(
client: std::sync::Arc<Actor<NetXClient<T>>>,
) -> impl #interface_name {
#proxy_name { client }
}
}
impl<'a, T: SessionSave + 'static>
#proxy_name<&'a std::sync::Arc<Actor<NetXClient<T>>>>
{
pub fn new_impl_ref(
client: &'a std::sync::Arc<Actor<NetXClient<T>>>,
) -> Self {
#proxy_name { client }
}
}
#[async_trait::async_trait]
impl<T: SessionSave + 'static> #interface_name
for #proxy_name<std::sync::Arc<Actor<NetXClient<T>>>>
{
#(#impl_func)*
}
#[async_trait::async_trait]
impl<'a, T: SessionSave + 'static> #interface_name
for #proxy_name<&'a std::sync::Arc<Actor<NetXClient<T>>>>
{
#(#impl_func)*
}
};
if !controller_name.is_empty() {
let controller = format_ident!("{}", controller_name);
let make = make_dispatch_arms(&funcs, interface_name);
let fallback = make_special_fallback();
let expanded = quote! {
#[async_trait::async_trait]
#ast
#[async_trait::async_trait]
impl IController for #controller {
#[inline]
async fn call(
&self,
tt: u8,
cmd_tag: i32,
mut data: data_rw::DataOwnedReader,
) -> ::anyhow::Result<RetResult> {
match cmd_tag {
#(#make)*
#fallback
}
}
}
};
TokenStream::from(expanded)
} else {
let expanded = quote! {
#[async_trait::async_trait]
#ast
#impl_interface
};
TokenStream::from(expanded)
}
}
#[proc_macro_attribute]
pub fn build_server(args: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as ItemTrait);
let funcs = get_funcs_info(&ast);
let controller_name = args.to_string();
let interface_name = &ast.ident;
let proxy_name = format_ident!("___impl_{}_call", interface_name);
let call_macro = format_ident!("call_peer");
let impl_func = get_impl_func(&funcs, &call_macro);
let impl_interface = quote! {
#[allow(non_camel_case_types)]
pub struct #proxy_name<T> {
client: T,
}
impl<T> #proxy_name<T> {
pub fn new(client: T) -> #proxy_name<T> {
#proxy_name { client }
}
}
impl<'a, T: IController + 'static> #proxy_name<&'a NetxToken<T>> {
pub fn new_ref(client: &'a NetxToken<T>) -> Self {
#proxy_name { client }
}
}
impl<T: IController + 'static> #interface_name for #proxy_name<NetxToken<T>> {
#(#impl_func)*
}
impl<'a, T: IController + 'static> #interface_name
for #proxy_name<&'a NetxToken<T>>
{
#(#impl_func)*
}
};
if !controller_name.is_empty() {
let controller = format_ident!("{}", controller_name);
let make = make_dispatch_arms(&funcs, interface_name);
let fallback = make_special_fallback();
let expanded = quote! {
#ast
impl IController for #controller {
#[inline]
async fn call(
&self,
tt: u8,
cmd_tag: i32,
mut data: data_rw::DataOwnedReader,
) -> ::anyhow::Result<RetResult> {
match cmd_tag {
#(#make)*
#fallback
}
}
}
};
TokenStream::from(expanded)
} else {
let expanded = quote! {
#ast
#impl_interface
};
TokenStream::from(expanded)
}
}
#[proc_macro_attribute]
pub fn build_impl(_: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as ItemImpl);
TokenStream::from(quote! { #ast })
}
#[proc_macro_attribute]
pub fn build_impl_client(_: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as ItemImpl);
TokenStream::from(quote! {
#[async_trait::async_trait]
#ast
})
}
#[proc_macro_attribute]
pub fn tag(_: TokenStream, input: TokenStream) -> TokenStream {
input
}