depthai_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::{parse::Parse, parse::ParseStream, parse_macro_input, Ident, ItemStruct, Result, Token};
4
5/// Wrap a native DepthAI node that is created via `Pipeline::create_node_by_name("ClassName")`.
6///
7/// Minimal usage:
8///
9/// ```ignore
10/// use depthai::native_node_wrapper;
11///
12/// #[native_node_wrapper(native = "StereoDepth")]
13/// pub struct StereoDepthNode {
14///     node: depthai::pipeline::Node,
15/// }
16/// ```
17///
18/// Options:
19/// - `native = <LitStr>`: required. The C++ class name of the node.
20/// - `field = <ident>`: optional, defaults to `node`.
21/// - `as_node = true|false`: optional, defaults to `true`.
22/// - `inputs(...)`: optional, list of input port names.
23/// - `outputs(...)`: optional, list of output port names.
24#[proc_macro_attribute]
25pub fn native_node_wrapper(attr: TokenStream, item: TokenStream) -> TokenStream {
26    let args = parse_macro_input!(attr as NativeNodeArgs);
27    let item_struct = parse_macro_input!(item as ItemStruct);
28
29    match expand_native_node(args, item_struct) {
30        Ok(ts) => ts,
31        Err(e) => e.to_compile_error().into(),
32    }
33}
34
35struct NativeNodeArgs {
36    native: syn::LitStr,
37    field: Ident,
38    gen_as_node: bool,
39    inputs: Vec<Ident>,
40    outputs: Vec<Ident>,
41}
42
43impl Parse for NativeNodeArgs {
44    fn parse(input: ParseStream<'_>) -> Result<Self> {
45        let mut native: Option<syn::LitStr> = None;
46        let mut field: Option<Ident> = None;
47        let mut gen_as_node: Option<bool> = None;
48        let mut inputs: Vec<Ident> = Vec::new();
49        let mut outputs: Vec<Ident> = Vec::new();
50
51        while !input.is_empty() {
52            let key: Ident = input.parse()?;
53            
54            if input.peek(Token![=]) {
55                input.parse::<Token![=]>()?;
56                if key == "native" {
57                    native = Some(input.parse()?);
58                } else if key == "field" {
59                    field = Some(input.parse()?);
60                } else if key == "as_node" {
61                    let v: syn::LitBool = input.parse()?;
62                    gen_as_node = Some(v.value);
63                } else {
64                    return Err(syn::Error::new_spanned(key, "unknown argument; expected `native`, `field`, or `as_node`"));
65                }
66            } else if input.peek(syn::token::Paren) {
67                let content;
68                syn::parenthesized!(content in input);
69                while !content.is_empty() {
70                    let id: Ident = content.parse()?;
71                    if key == "inputs" {
72                        inputs.push(id);
73                    } else if key == "outputs" {
74                        outputs.push(id);
75                    } else {
76                        return Err(syn::Error::new_spanned(key, "unknown argument; expected `inputs` or `outputs`"));
77                    }
78                    if content.peek(Token![,]) {
79                        content.parse::<Token![,]>()?;
80                    }
81                }
82            } else {
83                return Err(syn::Error::new_spanned(key, "expected `=` or `(...)`"));
84            }
85
86            if input.peek(Token![,]) {
87                input.parse::<Token![,]>()?;
88            }
89        }
90
91        let native = native.ok_or_else(|| syn::Error::new(input.span(), "missing required argument: `native`"))?;
92
93        Ok(Self {
94            native,
95            field: field.unwrap_or_else(|| Ident::new("node", proc_macro2::Span::call_site())),
96            gen_as_node: gen_as_node.unwrap_or(true),
97            inputs,
98            outputs,
99        })
100    }
101}
102
103fn expand_native_node(args: NativeNodeArgs, item_struct: ItemStruct) -> Result<TokenStream> {
104    // Basic shape validation: named fields.
105    let field_ident = args.field.clone();
106    let has_field = match &item_struct.fields {
107        syn::Fields::Named(named) => named
108            .named
109            .iter()
110            .any(|f| f.ident.as_ref().is_some_and(|id| *id == field_ident)),
111        _ => false,
112    };
113
114    if !has_field {
115        return Err(syn::Error::new_spanned(
116            item_struct.fields.to_token_stream(),
117            format!(
118                "expected a struct with a named field `{}` (or pass `field = ...`)",
119                field_ident
120            ),
121        ));
122    }
123
124    let ty_ident = item_struct.ident.clone();
125    let native_name = args.native;
126    
127    let create_expr = quote! { 
128        ::depthai::pipeline::node::create_node_by_name(pipeline.inner_arc(), #native_name)? 
129    };
130
131    let gen_as_node = args.gen_as_node;
132
133    let as_node_impl = if gen_as_node {
134        quote! {
135            impl #ty_ident {
136                /// View this node as a generic erased pipeline node.
137                pub fn as_node(&self) -> &::depthai::pipeline::Node {
138                    &self.#field_ident
139                }
140            }
141        }
142    } else {
143        quote! {}
144    };
145
146    let inputs = args.inputs;
147    let outputs = args.outputs;
148
149    let input_methods = inputs.iter().map(|id| {
150        let name = id.to_string();
151        quote! {
152            #[allow(non_snake_case)]
153            pub fn #id(&self) -> ::depthai::Result<::depthai::output::Input> {
154                self.as_node().input(#name)
155            }
156        }
157    });
158
159    let output_methods = outputs.iter().map(|id| {
160        let name = id.to_string();
161        quote! {
162            #[allow(non_snake_case)]
163            pub fn #id(&self) -> ::depthai::Result<::depthai::output::Output> {
164                self.as_node().output(#name)
165            }
166        }
167    });
168
169    // Keep existing struct tokens but append impls.
170    let expanded = quote! {
171        #item_struct
172
173        #as_node_impl
174
175        impl #ty_ident {
176            #(#input_methods)*
177            #(#output_methods)*
178        }
179
180        unsafe impl ::depthai::pipeline::DeviceNode for #ty_ident {
181            fn create_in_pipeline(pipeline: &::depthai::pipeline::Pipeline) -> ::depthai::Result<Self> {
182                let node = #create_expr;
183                Ok(Self { #field_ident: node })
184            }
185        }
186    };
187
188    Ok(expanded.into())
189}
190
191/// Attribute macro for defining composite nodes in Rust.
192/// 
193/// A composite node is a Rust struct that wraps one or more native nodes
194/// and provides a higher-level API.
195/// 
196/// This macro implements `crate::pipeline::device_node::CreateInPipeline`
197/// by calling `Self::new(pipeline)`.
198#[proc_macro_attribute]
199pub fn depthai_composite(_args: TokenStream, item: TokenStream) -> TokenStream {
200    let item_struct = parse_macro_input!(item as ItemStruct);
201    let ty_ident = item_struct.ident.clone();
202
203    let expanded = quote! {
204        #item_struct
205
206        impl ::depthai::pipeline::device_node::CreateInPipeline for #ty_ident {
207            fn create(pipeline: &::depthai::pipeline::Pipeline) -> ::depthai::Result<Self> {
208                Self::new(pipeline)
209            }
210        }
211    };
212
213    expanded.into()
214}
215
216/// Attribute macro for defining Rust host nodes.
217///
218/// The annotated struct must implement a `process(&mut self, &MessageGroup) -> Option<Buffer>` method.
219#[proc_macro_attribute]
220pub fn depthai_host_node(_args: TokenStream, item: TokenStream) -> TokenStream {
221    let item_struct = parse_macro_input!(item as ItemStruct);
222    let ty_ident = item_struct.ident.clone();
223
224    let expanded = quote! {
225        #item_struct
226
227        impl ::depthai::host_node::HostNodeImpl for #ty_ident {
228            fn process_group(&mut self, group: &::depthai::host_node::MessageGroup) -> Option<::depthai::host_node::Buffer> {
229                self.process(group)
230            }
231        }
232    };
233
234    expanded.into()
235}
236
237/// Attribute macro for defining threaded host nodes in Rust.
238///
239/// The annotated struct must implement a `run(&mut self, ctx: &ThreadedHostNodeContext)` method.
240#[proc_macro_attribute]
241pub fn depthai_threaded_host_node(_args: TokenStream, item: TokenStream) -> TokenStream {
242    let item_struct = parse_macro_input!(item as ItemStruct);
243    let ty_ident = item_struct.ident.clone();
244
245    let expanded = quote! {
246        #item_struct
247
248        impl ::depthai::threaded_host_node::ThreadedHostNodeImpl for #ty_ident {
249            fn run(&mut self, ctx: &::depthai::threaded_host_node::ThreadedHostNodeContext) {
250                self.run(ctx)
251            }
252        }
253    };
254
255    expanded.into()
256}