rustsynth_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{self, parse_macro_input, DeriveInput, Ident, ItemMod};
4
5/// Derive macro generating an impl of `rustsynth::map::IntoOwnedMap`.
6///
7/// This macro automatically generates an implementation that converts a struct
8/// into a VapourSynth map by storing each field as a map entry.
9///
10/// # Example
11/// ```
12/// use rustsynth::IntoOwnedMap;
13///
14/// #[derive(IntoOwnedMap)]
15/// struct MyStruct {
16///    field1: i32,
17///     field2: String,
18/// }
19/// let s = MyStruct { field1: 42, field2: "Hello".to_string() };
20/// let map = s.into_owned_map();
21/// assert_eq!(map.get::<i32>("field1").unwrap(), &42);
22/// assert_eq!(map.get::<String>("field2").unwrap(), &"Hello".to_string());
23/// ```
24#[proc_macro_derive(IntoOwnedMap)]
25pub fn into_owned_map_derive(input: TokenStream) -> TokenStream {
26    // Construct a representation of Rust code as a syntax tree
27    // that we can manipulate
28    let ast = syn::parse(input).unwrap();
29
30    // Build the From implementation
31    impl_map_macro(&ast)
32}
33
34fn impl_map_macro(ast: &syn::DeriveInput) -> TokenStream {
35    let name = &ast.ident;
36    let fields: Vec<Ident> = match &ast.data {
37        syn::Data::Struct(ds) => match &ds.fields {
38            syn::Fields::Named(named) => named
39                .named
40                .iter()
41                .map(|x| x.ident.clone().unwrap())
42                .collect(),
43            _ => panic!("Must have named fields"),
44        },
45        _ => panic!("Must be a data struct"),
46    };
47    let gen = quote! {
48        impl rustsynth::map::IntoOwnedMap for #name {
49            fn into_owned_map<'elem>(self) -> rustsynth::map::Map<'elem> {
50                // Ensure API is initialized before creating a map
51                // This is required for Map::new() to work correctly
52                let mut map = rustsynth::map::Map::new()
53                    .expect("Failed to create map - ensure VapourSynth API is initialized with init_api() or use within a VapourSynth plugin context");
54                #(
55                    map.set(stringify!(#fields), &self.#fields)
56                        .expect(&format!("Failed to set field '{}' in map", stringify!(#fields)));
57                )*
58                map
59            }
60        }
61    };
62    gen.into()
63}
64
65/// Macro to define a VapourSynth plugin containing multiple filters
66#[proc_macro_attribute]
67pub fn vapoursynth_plugin(_args: TokenStream, input: TokenStream) -> TokenStream {
68    let input = parse_macro_input!(input as ItemMod);
69
70    match generate_vs_plugin(input) {
71        Ok(tokens) => tokens.into(),
72        Err(err) => err.to_compile_error().into(),
73    }
74}
75
76/// Macro to define individual filters within a plugin
77#[proc_macro_attribute]
78pub fn vapoursynth_filter(arg: TokenStream, input: TokenStream) -> TokenStream {
79    let input = parse_macro_input!(input as DeriveInput);
80    match generate_vs_filter(input, arg) {
81        Ok(tokens) => tokens.into(),
82        Err(err) => err.to_compile_error().into(),
83    }
84}
85
86fn generate_vs_plugin(input: ItemMod) -> syn::Result<proc_macro2::TokenStream> {
87    let items = if let Some((_, items)) = &input.content {
88        items
89    } else {
90        return Err(syn::Error::new_spanned(&input, "Module must have content"));
91    };
92
93    let expanded = quote! {
94            #( #items )*
95
96            // Plugin entry point - registers all filters
97            #[no_mangle]
98            pub unsafe extern "C" fn VapourSynthPluginInit2(
99                plugin: *mut rustsynth::ffi::VSPlugin,
100                vspapi: *const rustsynth::ffi::VSPLUGINAPI,
101            ) {
102                let api = &*vspapi;
103
104                // Configure the plugin
105                let identifier = std::ffi::CString::new(ID).unwrap();
106                let namespace = std::ffi::CString::new(NAMESPACE).unwrap();
107                let name = std::ffi::CString::new(NAME).unwrap();
108                let plugin_version = PLUGIN_VER;
109                let api_version = API_VER;
110                let flags = FLAGS;
111
112                api.configPlugin.expect("configPlugin is null")(
113                    identifier.as_ptr(),
114                    namespace.as_ptr(),
115                    name.as_ptr(),
116                    plugin_version,
117                    api_version,
118                    flags,
119                    plugin
120                );
121                // Register all filters in this plugin
122                __register_filters(plugin, vspapi);
123            }
124    };
125
126    Ok(expanded)
127}
128
129fn generate_vs_filter(
130    input: DeriveInput,
131    arg: TokenStream,
132) -> syn::Result<proc_macro2::TokenStream> {
133    let struct_name = &input.ident;
134
135    // Create a clean version of the input without the vapoursynth_filter attribute
136    let mut clean_input = input.clone();
137    clean_input
138        .attrs
139        .retain(|attr| !attr.path().is_ident("vapoursynth_filter"));
140
141    // Extract lifetime parameters
142    let lifetimes = &input.generics.params;
143    let has_lifetime = !lifetimes.is_empty();
144
145    // Create the struct type with lifetimes
146    let struct_type = if has_lifetime {
147        quote! { #struct_name<'_> }
148    } else {
149        quote! { #struct_name }
150    };
151
152    // Generate unique C function names based on struct name
153    let create_name = format!("{}Create", struct_name);
154    let getframe_name = format!("{}GetFrame", struct_name);
155    let free_name = format!("{}Free", struct_name);
156
157    let create_ident = syn::Ident::new(&create_name, struct_name.span());
158    let getframe_ident = syn::Ident::new(&getframe_name, struct_name.span());
159    let free_ident = syn::Ident::new(&free_name, struct_name.span());
160
161    // Common function signature for both video and audio filters
162    let function_signature = quote! {
163        #[no_mangle]
164        pub unsafe extern "C" fn #create_ident(
165            in_: *const rustsynth::ffi::VSMap,
166            out: *mut rustsynth::ffi::VSMap,
167            user_data: *mut std::os::raw::c_void,
168            core: *mut rustsynth::ffi::VSCore,
169            vsapi: *const rustsynth::ffi::VSAPI,
170        )
171    };
172
173    let create = match arg.to_string().as_str() {
174        "video" => {
175            quote! {
176                #function_signature {
177                    rustsynth::init_api(vsapi);
178                    let api = &*vsapi;
179                    std::panic::catch_unwind(|| {
180                        let core_ref = rustsynth::core::CoreRef::from_ptr(core);
181                        let in_map = rustsynth::map::MapRef::from_ptr(in_);
182                        // Create filter instance from arguments
183                        match <#struct_type>::from_args(&in_map, &core_ref) {
184                            Ok(filter_data) => {
185                                let deps = filter_data.get_dependencies();
186                                let deps_ffi: Vec<rustsynth::ffi::VSFilterDependency> = deps.iter()
187                                    .map(|d| d.as_ffi())
188                                    .collect();
189
190                                // Get filter mode from const
191                                let filter_mode = <#struct_type>::MODE;
192                                let media_info = match filter_data.get_video_info() {
193                                    Ok(ai) => ai,
194                                    Err(error_msg) => {
195                                        let error_cstr = std::ffi::CString::new(error_msg).unwrap_or_else(|_| {
196                                            std::ffi::CString::new("Failed to get video info").unwrap()
197                                        });
198                                        api.mapSetError.unwrap()(out, error_cstr.as_ptr());
199                                        return;
200                                    }
201                                };
202
203                                // Allocate filter data on heap
204                                let data_ptr = Box::into_raw(Box::new(filter_data)) as *mut std::os::raw::c_void;
205                                let filter_name = std::ffi::CString::new(<#struct_type>::NAME).unwrap();
206
207                                api.createVideoFilter.unwrap()(
208                                    out,
209                                    filter_name.as_ptr(),
210                                    &media_info.as_ffi() as *const rustsynth::ffi::VSVideoInfo,
211                                    Some(#getframe_ident),
212                                    Some(#free_ident),
213                                    filter_mode.as_ffi() as i32,
214                                    deps_ffi.as_ptr(),
215                                    deps_ffi.len() as i32,
216                                    data_ptr,
217                                    core,
218                                );
219                            },
220                            Err(error_msg) => {
221                                let error_cstr = std::ffi::CString::new(error_msg).unwrap_or_else(|_| {
222                                    std::ffi::CString::new("Filter creation failed").unwrap()
223                                });
224                                api.mapSetError.unwrap()(out, error_cstr.as_ptr());
225                            }
226                        }
227                    }).unwrap_or_else(|_| {
228                        api.mapSetError.unwrap()(out, b"Filter creation panicked\0".as_ptr() as *const std::os::raw::c_char);
229                    });
230                }
231            }
232        }
233        "audio" => {
234            quote! {
235                #function_signature {
236                    rustsynth::init_api(vsapi);
237                    let api = &*vsapi;
238                    std::panic::catch_unwind(|| {
239                        let core_ref = rustsynth::core::CoreRef::from_ptr(core);
240                        let in_map = rustsynth::map::Map::from_ptr(in_);
241                        // Create filter instance from arguments
242                        match <#struct_type>::from_args(&in_map, &core_ref) {
243                            Ok(filter_data) => {
244                                let deps = filter_data.get_dependencies();
245                                let deps_ffi: Vec<rustsynth::ffi::VSFilterDependency> = deps.iter()
246                                    .map(|d| d.as_ffi())
247                                    .collect();
248
249                                // Get filter mode from const
250                                let filter_mode = <#struct_type>::MODE;
251                                let media_info = match filter_data.get_audio_info() {
252                                    Ok(ai) => ai,
253                                    Err(error_msg) => {
254                                        let error_cstr = std::ffi::CString::new(error_msg).unwrap_or_else(|_| {
255                                            std::ffi::CString::new("Failed to get audio info").unwrap()
256                                        });
257                                        api.mapSetError.unwrap()(out, error_cstr.as_ptr());
258                                        return;
259                                    }
260                                };
261
262                                // Allocate filter data on heap
263                                let data_ptr = Box::into_raw(Box::new(filter_data)) as *mut std::os::raw::c_void;
264                                let filter_name = std::ffi::CString::new(<#struct_type>::NAME).unwrap();
265
266                                api.createAudioFilter.unwrap()(
267                                    out,
268                                    filter_name.as_ptr(),
269                                    &media_info,
270                                    Some(#getframe_ident),
271                                    Some(#free_ident),
272                                    filter_mode.as_ffi(),
273                                    deps_ffi.as_ptr(),
274                                    deps_ffi.len() as i32,
275                                    data_ptr,
276                                    core,
277                                );
278                            },
279                            Err(error_msg) => {
280                                let error_cstr = std::ffi::CString::new(error_msg).unwrap_or_else(|_| {
281                                    std::ffi::CString::new("Filter creation failed").unwrap()
282                                });
283                                api.mapSetError.unwrap()(out, error_cstr.as_ptr());
284                            }
285                        }
286                    }).unwrap_or_else(|_| {
287                        api.mapSetError.unwrap()(out, b"Filter creation panicked\0".as_ptr() as *const std::os::raw::c_char);
288                    });
289                }
290            }
291        }
292        _ => {
293            return Err(syn::Error::new_spanned(
294                arg.to_string(),
295                "Unsupported filter type. Use 'video' or 'audio'",
296            ))
297        }
298    };
299
300    let expanded = quote! {
301        // Original struct definition
302        #input
303
304        #create
305
306        // Frame processing function
307        #[no_mangle]
308        pub unsafe extern "C" fn #getframe_ident(
309            n: i32,
310            activation_reason: i32,
311            instance_data: *mut std::os::raw::c_void,
312            frame_data: *mut *mut std::os::raw::c_void,
313            frame_ctx: *mut rustsynth::ffi::VSFrameContext,
314            core: *mut rustsynth::ffi::VSCore,
315            vsapi: *const rustsynth::ffi::VSAPI,
316        ) -> *const rustsynth::ffi::VSFrame {
317            let api = &*vsapi;
318
319            std::panic::catch_unwind(|| {
320                let filter = &mut *(instance_data as *mut #struct_type);
321                let core_ref = rustsynth::core::CoreRef::from_ptr(core);
322                let frame_ctx_wrapper = rustsynth::frame::FrameContext::from_ptr(frame_ctx);
323                let activation = rustsynth::filter::ActivationReason::from_ffi(activation_reason);
324
325                match activation {
326                    rustsynth::filter::ActivationReason::Initial => {
327                        // Request the frames we need
328                        filter.request_input_frames(n, &frame_ctx_wrapper);
329                        std::ptr::null()
330                    },
331                    rustsynth::filter::ActivationReason::AllFramesReady => {
332                        // All frames ready - do the processing
333                        // Convert frame_data to the expected format
334                        let frame_data_array: &[u8; 4] = if (*frame_data).is_null() {
335                            &[0; 4]
336                        } else {
337                            std::slice::from_raw_parts(*frame_data as *const u8, 4).try_into().unwrap_or(&[0; 4])
338                        };
339
340                        match filter.process_frame(n, frame_data_array, &frame_ctx_wrapper, core_ref) {
341                            Ok(output_frame) => {
342                                output_frame.as_ptr()
343                            },
344                            Err(error_msg) => {
345                                let error_cstr = std::ffi::CString::new(error_msg).unwrap_or_else(|_| {
346                                    std::ffi::CString::new("Frame processing failed").unwrap()
347                                });
348                                api.setFilterError.unwrap()(error_cstr.as_ptr(), frame_ctx);
349
350                                // Clean up frame data if needed
351                                if !(*frame_data).is_null() {
352                                    filter.cleanup_frame_data(frame_data_array);
353                                    *frame_data = std::ptr::null_mut();
354                                }
355                                std::ptr::null()
356                            }
357                        }
358                    },
359                    rustsynth::filter::ActivationReason::Error => {
360                        // Error occurred - clean up
361                        if !(*frame_data).is_null() {
362                            let frame_data_array: &[u8; 4] = std::slice::from_raw_parts(*frame_data as *const u8, 4).try_into().unwrap_or(&[0; 4]);
363                            filter.cleanup_frame_data(frame_data_array);
364                            *frame_data = std::ptr::null_mut();
365                        }
366                        std::ptr::null()
367                    }
368                }
369            }).unwrap_or_else(|_| {
370                api.setFilterError.unwrap()(
371                    b"Frame processing panicked\0".as_ptr() as *const std::os::raw::c_char,
372                    frame_ctx
373                );
374
375                if !(*frame_data).is_null() {
376                    *frame_data = std::ptr::null_mut();
377                }
378                std::ptr::null()
379            })
380        }
381
382        // Filter cleanup function
383        #[no_mangle]
384        pub unsafe extern "C" fn #free_ident(
385            instance_data: *mut std::os::raw::c_void,
386            core: *mut rustsynth::ffi::VSCore,
387            vsapi: *const rustsynth::ffi::VSAPI,
388        ) {
389            if !instance_data.is_null() {
390                let _ = std::panic::catch_unwind(|| {
391                    let filter = Box::from_raw(instance_data as *mut #struct_type);
392                    filter.cleanup();
393                    // Box drop handles memory cleanup
394                });
395            }
396        }
397
398        // Register this filter in the plugin
399        impl<#lifetimes> #struct_name<#lifetimes> {
400            fn register_filter(
401                plugin: *mut rustsynth::ffi::VSPlugin,
402                vspapi: *const rustsynth::ffi::VSPLUGINAPI
403            ) {
404                unsafe {
405                    let api = &*vspapi;
406                    let filter_name = std::ffi::CString::new(Self::NAME).unwrap();
407                    let args_spec = std::ffi::CString::new(Self::ARGS).unwrap();
408                    let return_spec = std::ffi::CString::new(Self::RETURNTYPE).unwrap();
409
410                    if let Some(register_fn) = api.registerFunction {
411                        let ret = register_fn(
412                            filter_name.as_ptr(),
413                            args_spec.as_ptr(),
414                            return_spec.as_ptr(),
415                            Some(#create_ident),
416                            std::ptr::null_mut(),
417                            plugin
418                        );
419                        if ret == 0 {
420                            eprintln!("Failed to register filter '{}'", Self::NAME);
421                        }
422                    } else {
423                        eprintln!("registerFunction API is NULL - cannot register filter '{}'", Self::NAME);
424                    }
425                }
426            }
427        }
428    };
429
430    Ok(expanded)
431}