]> git.proxmox.com Git - perlmod.git/blobdiff - perlmod-macro/src/function.rs
add ability to set the errno value
[perlmod.git] / perlmod-macro / src / function.rs
index 2f1a014c1656642a9c50fc723441b7564f7e2659..52aec1f1a6e97eb90db8b571890aeb428821abeb 100644 (file)
@@ -1,8 +1,8 @@
-use anyhow::Error;
-
 use proc_macro2::{Ident, Span, TokenStream};
 
 use quote::quote;
+use syn::spanned::Spanned;
+use syn::Error;
 
 use crate::attribs::FunctionAttrs;
 
@@ -11,11 +11,18 @@ pub struct XSub {
     pub perl_name: Option<Ident>,
     pub xs_name: Ident,
     pub tokens: TokenStream,
+    pub prototype: Option<String>,
 }
 
 #[derive(Default)]
 struct ArgumentAttrs {
+    /// This is the `CV` pointer.
+    cv: Option<Span>,
+
+    /// Skip the deserializer for this argument.
     raw: bool,
+
+    /// Call `TryFrom<&Value>::try_from` for this argument instead of deserializing it.
     try_from_ref: bool,
 }
 
@@ -25,6 +32,8 @@ impl ArgumentAttrs {
             self.raw = true;
         } else if path.is_ident("try_from_ref") {
             self.try_from_ref = true;
+        } else if path.is_ident("cv") {
+            self.cv = Some(path.span());
         } else {
             return false;
         }
@@ -33,31 +42,37 @@ impl ArgumentAttrs {
     }
 
     fn validate(&self, span: Span) -> Result<(), Error> {
-        if self.raw && self.try_from_ref {
+        if self.raw as usize + self.try_from_ref as usize + self.cv.is_some() as usize > 1 {
             bail!(
                 span,
-                "`raw` and `try_from_ref` attributes are mutually exclusive"
+                "`raw` and `try_from_ref` and `cv` attributes are mutually exclusive"
             );
         }
         Ok(())
     }
 }
 
-enum Return {
+struct Return {
+    result: bool,
+    value: ReturnValue,
+}
+
+enum ReturnValue {
     /// Return nothing. (This is different from returning an implicit undef!)
-    None(bool),
+    None,
 
     /// Return a single element.
-    Single(bool),
+    Single,
 
     /// We support tuple return types. They act like "list" return types in perl.
-    Tuple(bool, usize),
+    Tuple(usize),
 }
 
 pub fn handle_function(
     attr: FunctionAttrs,
     mut func: syn::ItemFn,
     mangled_package_name: Option<&str>,
+    export_public: bool,
 ) -> Result<XSub, Error> {
     if !func.sig.generics.params.is_empty() {
         bail!(&func.sig.generics => "generic functions cannot be exported as xsubs");
@@ -68,18 +83,21 @@ pub fn handle_function(
     }
 
     let name = func.sig.ident.clone();
+    let export_public = export_public.then_some(&func.vis);
     let xs_name = attr
         .xs_name
         .clone()
         .unwrap_or_else(|| match mangled_package_name {
-            None => Ident::new(&format!("xs_{}", name), name.span()),
-            Some(prefix) => Ident::new(&format!("xs_{}_{}", prefix, name), name.span()),
+            None => Ident::new(&format!("xs_{name}"), name.span()),
+            Some(prefix) => Ident::new(&format!("xs_{prefix}_{name}"), name.span()),
         });
-    let impl_xs_name = Ident::new(&format!("impl_xs_{}", name), name.span());
+    let impl_xs_name = Ident::new(&format!("impl_xs_{name}"), name.span());
 
+    let mut trailing_options = 0;
     let mut extract_arguments = TokenStream::new();
     let mut deserialized_arguments = TokenStream::new();
     let mut passed_arguments = TokenStream::new();
+    let mut cv_arg_param = TokenStream::new();
     for arg in &mut func.sig.inputs {
         let mut argument_attrs = ArgumentAttrs::default();
 
@@ -88,7 +106,6 @@ pub fn handle_function(
             syn::FnArg::Typed(ref mut pt) => {
                 pt.attrs
                     .retain(|attr| !argument_attrs.handle_path(&attr.path));
-                use syn::spanned::Spanned;
                 argument_attrs.validate(pt.span())?;
                 &*pt
             }
@@ -109,21 +126,47 @@ pub fn handle_function(
 
         let arg_type = &*pat_ty.ty;
 
-        let extracted_name = Ident::new(&format!("extracted_arg_{}", arg_name), arg_name.span());
+        if let Some(cv_span) = argument_attrs.cv {
+            if !cv_arg_param.is_empty() {
+                bail!(cv_span, "only 1 'cv' parameter allowed");
+            }
+            cv_arg_param = quote! { #arg_name: #arg_type };
+            if passed_arguments.is_empty() {
+                passed_arguments.extend(quote! { #arg_name });
+            } else {
+                passed_arguments.extend(quote! {, #arg_name });
+            }
+            continue;
+        }
+
+        let extracted_name = Ident::new(&format!("extracted_arg_{arg_name}"), arg_name.span());
         let deserialized_name =
-            Ident::new(&format!("deserialized_arg_{}", arg_name), arg_name.span());
+            Ident::new(&format!("deserialized_arg_{arg_name}"), arg_name.span());
 
-        let missing_message =
-            syn::LitStr::new("missing required parameter: '{}'\n", arg_name.span());
+        let missing_message = syn::LitStr::new(
+            &format!("missing required parameter: '{arg_name}'\n"),
+            arg_name.span(),
+        );
 
-        extract_arguments.extend(quote! {
-            let #extracted_name: ::perlmod::Value = match args.next() {
-                Some(arg) => ::perlmod::Value::from(arg),
-                None => {
+        let none_handling = if is_option_type(arg_type).is_some() {
+            trailing_options += 1;
+            quote! { ::perlmod::Value::new_undef(), }
+        } else {
+            // only cound the trailing options;
+            trailing_options = 0;
+            quote! {
+                {
                     return Err(::perlmod::Value::new_string(#missing_message)
                         .into_mortal()
                         .into_raw());
                 }
+            }
+        };
+
+        extract_arguments.extend(quote! {
+            let #extracted_name: ::perlmod::Value = match args.next() {
+                Some(arg) => ::perlmod::Value::from(arg),
+                None => #none_handling
             };
         });
 
@@ -165,11 +208,23 @@ pub fn handle_function(
     }
 
     let has_return_value = match &func.sig.output {
-        syn::ReturnType::Default => Return::None(false),
-        syn::ReturnType::Type(_arrow, ty) => match get_result_type(&**ty) {
-            (syn::Type::Tuple(tuple), result) if tuple.elems.is_empty() => Return::None(result),
-            (syn::Type::Tuple(tuple), result) => Return::Tuple(result, tuple.elems.len()),
-            (_, result) => Return::Single(result),
+        syn::ReturnType::Default => Return {
+            result: false,
+            value: ReturnValue::None,
+        },
+        syn::ReturnType::Type(_arrow, ty) => match get_result_type(ty) {
+            (syn::Type::Tuple(tuple), result) if tuple.elems.is_empty() => Return {
+                result,
+                value: ReturnValue::None,
+            },
+            (syn::Type::Tuple(tuple), result) => Return {
+                result,
+                value: ReturnValue::Tuple(tuple.elems.len()),
+            },
+            (_, result) => Return {
+                result,
+                value: ReturnValue::Single,
+            },
         },
     };
 
@@ -177,7 +232,7 @@ pub fn handle_function(
         &format!(
             "too many parameters for function '{}', (expected {})\n",
             name,
-            func.sig.inputs.len()
+            func.sig.inputs.len() - (!cv_arg_param.is_empty()) as usize
         ),
         Span::call_site(),
     );
@@ -193,6 +248,8 @@ pub fn handle_function(
         &xs_name,
         &impl_xs_name,
         passed_arguments,
+        export_public,
+        !cv_arg_param.is_empty(),
     )?;
 
     let tokens = quote! {
@@ -202,9 +259,7 @@ pub fn handle_function(
 
         #[inline(never)]
         #[allow(non_snake_case)]
-        fn #impl_xs_name(
-            _cv: &::perlmod::ffi::CV,
-        ) -> Result<#return_type, *mut ::perlmod::ffi::SV> {
+        fn #impl_xs_name(#cv_arg_param) -> Result<#return_type, *mut ::perlmod::ffi::SV> {
             let argmark = unsafe { ::perlmod::ffi::pop_arg_mark() };
             let mut args = argmark.iter();
 
@@ -216,7 +271,7 @@ pub fn handle_function(
                     .into_raw());
             }
 
-            drop(args);
+            //drop(args);
 
             #deserialized_arguments
 
@@ -233,9 +288,29 @@ pub fn handle_function(
         perl_name: attr.perl_name,
         xs_name,
         tokens,
+        prototype: attr
+            .prototype
+            .or_else(|| Some(gen_prototype(func.sig.inputs.len(), trailing_options))),
     })
 }
 
+fn gen_prototype(arg_count: usize, trailing_options: usize) -> String {
+    let arg_count = arg_count - trailing_options;
+
+    let mut proto = String::with_capacity(arg_count + trailing_options + 1);
+
+    for _ in 0..arg_count {
+        proto.push('$');
+    }
+    if trailing_options > 0 {
+        proto.push(';');
+        for _ in 0..trailing_options {
+            proto.push('$');
+        }
+    }
+    proto
+}
+
 struct ReturnHandling {
     return_type: TokenStream,
     handle_return: TokenStream,
@@ -249,28 +324,70 @@ fn handle_return_kind(
     xs_name: &Ident,
     impl_xs_name: &Ident,
     passed_arguments: TokenStream,
+    export_public: Option<&syn::Visibility>,
+    cv_arg: bool,
 ) -> Result<ReturnHandling, Error> {
     let return_type;
     let mut handle_return;
     let wrapper_func;
 
-    match ret {
-        Return::None(result) => {
+    let vis = match export_public {
+        Some(vis) => quote! { #[no_mangle] #vis },
+        None => quote! { #[allow(non_snake_case)] },
+    };
+
+    let (cv_arg_name, cv_arg_passed) = if cv_arg {
+        (
+            quote! { cv },
+            quote! { ::perlmod::Value::from_raw_ref(cv as *mut ::perlmod::ffi::SV) },
+        )
+    } else {
+        (quote! { _cv }, TokenStream::new())
+    };
+
+    let return_error = if ret.result {
+        if attr.serialize_error {
+            quote! {
+                match ::perlmod::to_value(&err) {
+                    Ok(err) => return Err(err.into_mortal().into_raw()),
+                    Err(err) => {
+                        return Err(::perlmod::Value::new_string(&format!("{}\n", err))
+                            .into_mortal()
+                            .into_raw());
+                    }
+                }
+            }
+        } else {
+            quote! {
+                return Err(::perlmod::Value::new_string(&format!("{}\n", err))
+                    .into_mortal()
+                    .into_raw());
+            }
+        }
+    } else {
+        TokenStream::new()
+    };
+
+    let copy_errno = if attr.errno {
+        quote! { ::perlmod::error::copy_errno_to_libc(); }
+    } else {
+        TokenStream::new()
+    };
+
+    let pthx = crate::pthx_param();
+    match ret.value {
+        ReturnValue::None => {
             return_type = quote! { () };
 
             if attr.raw_return {
                 bail!(&attr.raw_return => "raw_return attribute is illegal without a return value");
             }
 
-            if result {
+            if ret.result {
                 handle_return = quote! {
                     match #name(#passed_arguments) {
                         Ok(()) => (),
-                        Err(err) => {
-                            return Err(::perlmod::Value::new_string(&format!("{}\n", err))
-                                .into_mortal()
-                                .into_raw());
-                        }
+                        Err(err) => { #return_error }
                     }
 
                     Ok(())
@@ -284,11 +401,12 @@ fn handle_return_kind(
             }
 
             wrapper_func = quote! {
-                #[no_mangle]
                 #[doc(hidden)]
-                pub extern "C" fn #xs_name(cv: &::perlmod::ffi::CV) {
+                #vis extern "C" fn #xs_name(#pthx #cv_arg_name: *mut ::perlmod::ffi::CV) {
                     unsafe {
-                        match #impl_xs_name(cv) {
+                        let res = #impl_xs_name(#cv_arg_passed);
+                        #copy_errno
+                        match res {
                             Ok(()) => (),
                             Err(sv) => ::perlmod::ffi::croak(sv),
                         }
@@ -296,18 +414,14 @@ fn handle_return_kind(
                 }
             };
         }
-        Return::Single(result) => {
+        ReturnValue::Single => {
             return_type = quote! { *mut ::perlmod::ffi::SV };
 
-            if result {
+            if ret.result {
                 handle_return = quote! {
                     let result = match #name(#passed_arguments) {
                         Ok(output) => output,
-                        Err(err) => {
-                            return Err(::perlmod::Value::new_string(&format!("{}\n", err))
-                                .into_mortal()
-                                .into_raw());
-                        }
+                        Err(err) => { #return_error }
                     };
                 };
             } else {
@@ -332,11 +446,12 @@ fn handle_return_kind(
             };
 
             wrapper_func = quote! {
-                #[no_mangle]
                 #[doc(hidden)]
-                pub extern "C" fn #xs_name(cv: &::perlmod::ffi::CV) {
+                #vis extern "C" fn #xs_name(#pthx #cv_arg_name: *mut ::perlmod::ffi::CV) {
                     unsafe {
-                        match #impl_xs_name(cv) {
+                        let res = #impl_xs_name(#cv_arg_passed);
+                        #copy_errno
+                        match res {
                             Ok(sv) => ::perlmod::ffi::stack_push_raw(sv),
                             Err(sv) => ::perlmod::ffi::croak(sv),
                         }
@@ -344,7 +459,7 @@ fn handle_return_kind(
                 }
             };
         }
-        Return::Tuple(result, count) => {
+        ReturnValue::Tuple(count) => {
             return_type = {
                 let mut rt = TokenStream::new();
                 for _ in 0..count {
@@ -353,20 +468,16 @@ fn handle_return_kind(
                 quote! { (#rt) }
             };
 
-            if result {
+            if ret.result {
                 handle_return = quote! {
                     let result = match #name(#passed_arguments) {
                         Ok(output) => output,
-                        Err(err) => {
-                            return Err(::perlmod::Value::new_string(&format!("{}\n", err))
-                                .into_mortal()
-                                .into_raw());
-                        }
+                        Err(err) => { #return_error }
                     };
                 };
             } else {
                 handle_return = quote! {
-                    let result = match #name(#passed_arguments);
+                    let result = #name(#passed_arguments);
                 };
             }
 
@@ -419,11 +530,12 @@ fn handle_return_kind(
             //}
 
             wrapper_func = quote! {
-                #[no_mangle]
                 #[doc(hidden)]
-                pub extern "C" fn #xs_name(cv: &::perlmod::ffi::CV) {
+                #vis extern "C" fn #xs_name(#pthx #cv_arg_name: *mut ::perlmod::ffi::CV) {
                     unsafe {
-                        match #impl_xs_name(cv) {
+                        let res = #impl_xs_name(#cv_arg_passed);
+                        #copy_errno
+                        match res {
                             Ok(sv) => { #push },
                             Err(sv) => ::perlmod::ffi::croak(sv),
                         }
@@ -448,8 +560,8 @@ pub fn is_result_type(ty: &syn::Type) -> Option<&syn::Type> {
         }
         let segs = &p.path.segments;
         let is_result = match segs.len() {
-            1 => segs.last().unwrap().ident == "Result",
-            2 => segs.first().unwrap().ident == "std" && segs.last().unwrap().ident == "Result",
+            1 => segs[0].ident == "Result",
+            3 => segs[0].ident == "std" && segs[1].ident == "result" && segs[2].ident == "Result",
             _ => false,
         };
         if !is_result {
@@ -481,5 +593,34 @@ pub fn get_result_type(ty: &syn::Type) -> (&syn::Type, bool) {
 
 /// Get a non-suffixed integer from an usize.
 fn simple_usize(i: usize, span: Span) -> syn::LitInt {
-    syn::LitInt::new(&format!("{}", i), span)
+    syn::LitInt::new(&format!("{i}"), span)
+}
+
+/// Note that we cannot handle renamed imports at all here...
+pub fn is_option_type(ty: &syn::Type) -> Option<&syn::Type> {
+    if let syn::Type::Path(p) = ty {
+        if p.qself.is_some() {
+            return None;
+        }
+        let segs = &p.path.segments;
+        let is_option = match segs.len() {
+            1 => segs[0].ident == "Option",
+            3 => segs[0].ident == "std" && segs[1].ident == "option" && segs[2].ident == "Option",
+            _ => false,
+        };
+        if !is_option {
+            return None;
+        }
+
+        if let syn::PathArguments::AngleBracketed(generic) = &segs.last().unwrap().arguments {
+            if generic.args.len() != 1 {
+                return None;
+            }
+
+            if let syn::GenericArgument::Type(ty) = generic.args.first().unwrap() {
+                return Some(ty);
+            }
+        }
+    }
+    None
 }