* [PATCH perlmod v2 1/5] macro: function: make argument code generation helpers standalone fns
2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
@ 2026-07-24 14:49 ` Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 2/5] macro: function: make argument attribute handling more extendable Max R. Carrara
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
To: pve-devel
There is not really much of a reason to associate them with
`ArgumentAttrs` from a semantic standpoint, so make those helpers
standalone functions instead.
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
perlmod-macro/src/function.rs | 142 +++++++++++++++++-----------------
1 file changed, 72 insertions(+), 70 deletions(-)
diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 8b5d689..9d4a4af 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -86,80 +86,80 @@ impl ArgumentAttrs {
}
}
}
+}
- fn extract_argument_code(
- &self,
- span: Span,
- extracted_name: &Ident,
- none_handling: TokenStream,
- ) -> TokenStream {
- if self.list.is_some() {
- quote_spanned! { span=>
- let #extracted_name = args.map(::perlmod::Value::from);
- }
- } else {
- quote_spanned! { span=>
- let #extracted_name: ::perlmod::Value = match args.next() {
- Some(arg) => ::perlmod::Value::from(arg),
- None => #none_handling
- };
- }
+fn extract_argument_code(
+ arg_attr: &ArgumentAttrs,
+ span: Span,
+ extracted_name: &Ident,
+ none_handling: TokenStream,
+) -> TokenStream {
+ if arg_attr.list.is_some() {
+ quote_spanned! { span=>
+ let #extracted_name = args.map(::perlmod::Value::from);
+ }
+ } else {
+ quote_spanned! { span=>
+ let #extracted_name: ::perlmod::Value = match args.next() {
+ Some(arg) => ::perlmod::Value::from(arg),
+ None => #none_handling
+ };
}
}
+}
- fn deserialized_argument_code(
- &self,
- span: Span,
- arg_type: &syn::Type,
- deserialized_name: &Ident,
- extracted_name: Ident,
- ) -> TokenStream {
- if self.raw {
- quote_spanned! { span=>
- let #deserialized_name = #extracted_name;
- }
- } else if self.try_from_ref {
- quote_spanned! { span=>
- let #deserialized_name: #arg_type =
- match ::std::convert::TryFrom::try_from(&#extracted_name) {
- Ok(arg) => arg,
- Err(err) => {
- return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
- .into_mortal()
- .into_raw());
- }
- };
- }
- } else if self.list.is_some() {
- quote_spanned! { span=>
- let #deserialized_name = {
- let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
- match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
- ::perlmod::__private__::serde::de::value::SeqDeserializer::new(
- #extracted_name
- )
- ) {
- Ok(arg) => arg,
- Err(err) => {
- return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
- .into_mortal()
- .into_raw());
- }
+fn deserialized_argument_code(
+ arg_attr: &ArgumentAttrs,
+ span: Span,
+ arg_type: &syn::Type,
+ deserialized_name: &Ident,
+ extracted_name: Ident,
+) -> TokenStream {
+ if arg_attr.raw {
+ quote_spanned! { span=>
+ let #deserialized_name = #extracted_name;
+ }
+ } else if arg_attr.try_from_ref {
+ quote_spanned! { span=>
+ let #deserialized_name: #arg_type =
+ match ::std::convert::TryFrom::try_from(&#extracted_name) {
+ Ok(arg) => arg,
+ Err(err) => {
+ return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+ .into_mortal()
+ .into_raw());
+ }
+ };
+ }
+ } else if arg_attr.list.is_some() {
+ quote_spanned! { span=>
+ let #deserialized_name = {
+ let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
+ match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
+ ::perlmod::__private__::serde::de::value::SeqDeserializer::new(
+ #extracted_name
+ )
+ ) {
+ Ok(arg) => arg,
+ Err(err) => {
+ return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+ .into_mortal()
+ .into_raw());
+ }
+ }
+ };
+ }
+ } else {
+ quote_spanned! { span=>
+ let #deserialized_name: #arg_type =
+ match ::perlmod::from_ref_value(&#extracted_name) {
+ Ok(data) => data,
+ Err(err) => {
+ return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+ .into_mortal()
+ .into_raw());
}
};
- }
- } else {
- quote_spanned! { span=>
- let #deserialized_name: #arg_type =
- match ::perlmod::from_ref_value(&#extracted_name) {
- Ok(data) => data,
- Err(err) => {
- return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
- .into_mortal()
- .into_raw());
- }
- };
- }
}
}
}
@@ -278,13 +278,15 @@ pub fn handle_function(
}
};
- extract_arguments.extend(argument_attrs.extract_argument_code(
+ extract_arguments.extend(extract_argument_code(
+ &argument_attrs,
span,
&extracted_name,
none_handling,
));
- deserialized_arguments.extend(argument_attrs.deserialized_argument_code(
+ deserialized_arguments.extend(deserialized_argument_code(
+ &argument_attrs,
span,
arg_type,
&deserialized_name,
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH perlmod v2 2/5] macro: function: make argument attribute handling more extendable
2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 1/5] macro: function: make argument code generation helpers standalone fns Max R. Carrara
@ 2026-07-24 14:49 ` Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 3/5] macro: function: explicitly pass identifier for generated arg iter Max R. Carrara
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
To: pve-devel
Introduce a new private `ArgumentAttrType` enum for tracking whether
an argument attribute is `#[raw]`, `#[list]`, etc. and replace the
flags inside `ArgumentAttrs` with it.
Move the reference to `syn::PatType` into `ArgumentAttrs` and make
`ArgumentAttrs` take a corresponding lifetime as parameter.
Replace the existing machinery that has the `for_input()` function as
entrypoint with the new `new_from_fn_arg()` function, which makes use
of the new fields in `ArgumentAttrs`. Replicate the previous logic
inside `new_from_fn_arg()`, but laid out more explicitly -- now there
is no need to bounce between a couple function calls when reading the
code.
Rename `ArgumentAttrs` to `ArgumentAttr`, since we never return more
than one anyway, and all argument attributes are mutually exclusive
anyway.
Finally, adapt the remaining code in `handle_function()`,
`extract_argument_code()`, and `deserialized_argument_code()`
correspondingly by making it use the new struct fields.
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
perlmod-macro/src/function.rs | 217 ++++++++++++++++++++--------------
1 file changed, 131 insertions(+), 86 deletions(-)
diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 9d4a4af..3ce0a17 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -15,112 +15,159 @@ pub struct XSub {
pub prototype: Option<String>,
}
-#[derive(Default)]
-struct ArgumentAttrs {
- /// This is the `CV` pointer.
- cv: Option<Span>,
-
+enum ArgumentAttrType {
/// Skip the deserializer for this argument.
- raw: bool,
+ Raw,
/// Call `TryFrom<&Value>::try_from` for this argument instead of deserializing it.
- try_from_ref: bool,
+ TryFromRef,
+
+ /// This is the `CV` pointer.
+ CVPtr(Span),
/// Slurp the remaining arguments (like an `@rest` at the end).
/// This requires the parameter to implement `FromIterator<T: Deserialize>`.
- list: Option<Span>,
+ TrailingList(Span),
}
-impl ArgumentAttrs {
- fn handle_path(&mut self, path: &syn::Path) -> bool {
+impl ArgumentAttrType {
+ fn from_attr(attr: &syn::Attribute) -> Option<Self> {
+ Self::from_path(attr.path())
+ }
+
+ fn from_path(path: &syn::Path) -> Option<Self> {
if path.is_ident("raw") {
- 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 if path.is_ident("list") {
- self.list = Some(path.span());
- } else {
- return false;
+ return Some(Self::Raw);
}
- true
+ if path.is_ident("try_from_ref") {
+ return Some(Self::TryFromRef);
+ }
+
+ if path.is_ident("cv") {
+ return Some(Self::CVPtr(path.span()));
+ }
+
+ if path.is_ident("list") {
+ return Some(Self::TrailingList(path.span()));
+ }
+
+ None
}
- fn handle_attr(&mut self, attr: &syn::Attribute) -> bool {
- if self.handle_path(attr.path()) {
- if !matches!(attr.meta, Meta::Path(_)) {
- error!(&attr.meta => "attribute does not take any value or parameter");
- }
- true
- } else {
- false
+ fn is_same_variant_as(&self, other: &Self) -> bool {
+ core::mem::discriminant(self) == core::mem::discriminant(other)
+ }
+
+ fn as_str(&self) -> &'static str {
+ match self {
+ Self::Raw => "raw",
+ Self::TryFromRef => "try_from_ref",
+ Self::CVPtr(_) => "cv",
+ Self::TrailingList(_) => "list",
}
}
+}
- fn validate(&self, span: Span) -> Result<(), Error> {
- if self.raw as usize
- + self.try_from_ref as usize
- + self.cv.is_some() as usize
- + self.list.is_some() as usize
- > 1
- {
- bail!(
- span,
- "`raw` and `try_from_ref`, `cv` and `list` attributes are mutually exclusive"
- );
- }
- Ok(())
- }
-
- fn for_input(arg: &mut syn::FnArg) -> Result<(Self, &syn::PatType), Error> {
- let mut this = Self::default();
+struct ArgumentAttr<'s> {
+ attr_type: Option<ArgumentAttrType>,
+ pat_type: &'s syn::PatType,
+}
+impl<'s> ArgumentAttr<'s> {
+ fn new_from_fn_arg(arg: &'s mut syn::FnArg) -> Result<Self, Error> {
match arg {
- syn::FnArg::Receiver(_) => bail!(arg => "cannot export self-taking methods as xsubs"),
- syn::FnArg::Typed(pt) => {
- pt.attrs.retain(|attr| !this.handle_attr(attr));
- this.validate(pt.span())?;
- Ok((this, &*pt))
+ syn::FnArg::Receiver(_) => {
+ bail!(arg => "cannot export self-taking methods as xsubs");
+ }
+ syn::FnArg::Typed(pat_type) => {
+ let mut attr_type: Option<ArgumentAttrType> = None;
+ let mut has_err = false;
+
+ let span = pat_type.span();
+
+ pat_type.attrs.retain(|attr| {
+ let Some(got_attr_type) = ArgumentAttrType::from_attr(attr) else {
+ // Attribute has no matching attribute type => retain it
+ return true;
+ };
+
+ if !matches!(attr.meta, Meta::Path(_)) {
+ error!(&attr.meta => "attribute does not take any value or parameter");
+ has_err = true;
+ return false;
+ }
+
+ let Some(existing_attr_type) = attr_type.as_ref() else {
+ // No existing attribute type => assign and don't retain
+ attr_type = Some(got_attr_type);
+ return false;
+ };
+
+ if existing_attr_type.is_same_variant_as(&got_attr_type) {
+ let attr_str = existing_attr_type.as_str();
+ error!(span, "duplicate attribute '{attr_str}'");
+ has_err = true;
+ return false;
+ }
+
+ // At this point we have two differing attributes
+ error!(
+ span,
+ "`raw`, `try_from_ref`, `cv`, and `list` attributes are mutually exclusive"
+ );
+ has_err = true;
+ false
+ });
+
+ if has_err {
+ bail!(span, "failed to determine argument attribute");
+ }
+
+ Ok(Self {
+ attr_type,
+ pat_type,
+ })
}
}
}
}
fn extract_argument_code(
- arg_attr: &ArgumentAttrs,
+ arg_attr: &ArgumentAttr,
span: Span,
extracted_name: &Ident,
none_handling: TokenStream,
) -> TokenStream {
- if arg_attr.list.is_some() {
- quote_spanned! { span=>
- let #extracted_name = args.map(::perlmod::Value::from);
+ match arg_attr.attr_type {
+ Some(ArgumentAttrType::TrailingList(_)) => {
+ quote_spanned! { span=>
+ let #extracted_name = args.map(::perlmod::Value::from);
+ }
}
- } else {
- quote_spanned! { span=>
- let #extracted_name: ::perlmod::Value = match args.next() {
- Some(arg) => ::perlmod::Value::from(arg),
- None => #none_handling
- };
+ _ => {
+ quote_spanned! { span=>
+ let #extracted_name: ::perlmod::Value = match args.next() {
+ Some(arg) => ::perlmod::Value::from(arg),
+ None => #none_handling
+ };
+ }
}
}
}
fn deserialized_argument_code(
- arg_attr: &ArgumentAttrs,
+ arg_attr: &ArgumentAttr,
span: Span,
arg_type: &syn::Type,
deserialized_name: &Ident,
extracted_name: Ident,
) -> TokenStream {
- if arg_attr.raw {
- quote_spanned! { span=>
+ match &arg_attr.attr_type {
+ Some(ArgumentAttrType::Raw) => quote_spanned! { span=>
let #deserialized_name = #extracted_name;
- }
- } else if arg_attr.try_from_ref {
- quote_spanned! { span=>
+ },
+ Some(ArgumentAttrType::TryFromRef) => quote_spanned! { span=>
let #deserialized_name: #arg_type =
match ::std::convert::TryFrom::try_from(&#extracted_name) {
Ok(arg) => arg,
@@ -130,9 +177,8 @@ fn deserialized_argument_code(
.into_raw());
}
};
- }
- } else if arg_attr.list.is_some() {
- quote_spanned! { span=>
+ },
+ Some(ArgumentAttrType::TrailingList(_)) => quote_spanned! { span=>
let #deserialized_name = {
let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
@@ -148,9 +194,8 @@ fn deserialized_argument_code(
}
}
};
- }
- } else {
- quote_spanned! { span=>
+ },
+ Some(ArgumentAttrType::CVPtr(_)) | None => quote_spanned! { span=>
let #deserialized_name: #arg_type =
match ::perlmod::from_ref_value(&#extracted_name) {
Ok(data) => data,
@@ -160,7 +205,7 @@ fn deserialized_argument_code(
.into_raw());
}
};
- }
+ },
}
}
@@ -214,17 +259,17 @@ pub fn handle_function(
let mut cv_arg_param = TokenStream::new();
let mut had_list_param = false;
for arg in &mut func.sig.inputs {
- let (argument_attrs, pat_ty) = ArgumentAttrs::for_input(arg)?;
+ let arg_attr = ArgumentAttr::new_from_fn_arg(arg)?;
- if had_list_param {
- if let Some(span) = argument_attrs.list {
- bail!(span, "only 1 #[list] parameter allowed");
+ if let Some(ArgumentAttrType::TrailingList(list_span)) = arg_attr.attr_type {
+ if had_list_param {
+ bail!(list_span, "only 1 #[list] parameter allowed");
}
- } else {
- had_list_param = argument_attrs.list.is_some();
+
+ had_list_param = true;
}
- let arg_name = match &*pat_ty.pat {
+ let arg_name = match &*arg_attr.pat_type.pat {
syn::Pat::Ident(ident) => {
if ident.by_ref.is_some() {
bail!(ident => "xsub does not support by-ref parameters");
@@ -234,12 +279,12 @@ pub fn handle_function(
}
&ident.ident
}
- _ => bail!(&pat_ty.pat => "xsub does not support this kind of parameter"),
+ _ => bail!(&arg_attr.pat_type.pat => "xsub does not support this kind of parameter"),
};
- let arg_type = &*pat_ty.ty;
+ let arg_type = &*arg_attr.pat_type.ty;
- if let Some(cv_span) = argument_attrs.cv {
+ if let Some(ArgumentAttrType::CVPtr(cv_span)) = arg_attr.attr_type {
if !cv_arg_param.is_empty() {
bail!(cv_span, "only 1 'cv' parameter allowed");
}
@@ -264,7 +309,7 @@ pub fn handle_function(
let none_handling = if is_option_type(arg_type).is_some() {
trailing_options += 1;
quote_spanned! { span=> ::perlmod::Value::new_undef(), }
- } else if argument_attrs.list.is_some() {
+ } else if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
TokenStream::new()
} else {
// only count the trailing options;
@@ -279,14 +324,14 @@ pub fn handle_function(
};
extract_arguments.extend(extract_argument_code(
- &argument_attrs,
+ &arg_attr,
span,
&extracted_name,
none_handling,
));
deserialized_arguments.extend(deserialized_argument_code(
- &argument_attrs,
+ &arg_attr,
span,
arg_type,
&deserialized_name,
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH perlmod v2 3/5] macro: function: explicitly pass identifier for generated arg iter
2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 1/5] macro: function: make argument code generation helpers standalone fns Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 2/5] macro: function: make argument attribute handling more extendable Max R. Carrara
@ 2026-07-24 14:49 ` Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute Max R. Carrara
4 siblings, 0 replies; 6+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
To: pve-devel
The `args` variable that we define in generated code is used in
multiple places, making them implicitly depend on each other.
Make this explicit instead by defining the identifier as a
`syn::Ident` and passing it to / interpolating it into the respective
places.
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
perlmod-macro/src/function.rs | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 3ce0a17..3ca7bd0 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -136,18 +136,19 @@ impl<'s> ArgumentAttr<'s> {
fn extract_argument_code(
arg_attr: &ArgumentAttr,
span: Span,
+ arguments_name: &Ident,
extracted_name: &Ident,
none_handling: TokenStream,
) -> TokenStream {
match arg_attr.attr_type {
Some(ArgumentAttrType::TrailingList(_)) => {
quote_spanned! { span=>
- let #extracted_name = args.map(::perlmod::Value::from);
+ let #extracted_name = #arguments_name.map(::perlmod::Value::from);
}
}
_ => {
quote_spanned! { span=>
- let #extracted_name: ::perlmod::Value = match args.next() {
+ let #extracted_name: ::perlmod::Value = match #arguments_name.next() {
Some(arg) => ::perlmod::Value::from(arg),
None => #none_handling
};
@@ -252,6 +253,8 @@ pub fn handle_function(
});
let impl_xs_name = Ident::new(&format!("impl_xs_{name}"), name.span());
+ let arguments_name = syn::Ident::new("args", name.span());
+
let mut trailing_options = 0;
let mut extract_arguments = TokenStream::new();
let mut deserialized_arguments = TokenStream::new();
@@ -326,6 +329,7 @@ pub fn handle_function(
extract_arguments.extend(extract_argument_code(
&arg_attr,
span,
+ &arguments_name,
&extracted_name,
none_handling,
));
@@ -377,7 +381,7 @@ pub fn handle_function(
);
quote_spanned! { span=>
- if args.next().is_some() {
+ if #arguments_name.next().is_some() {
return Err(::perlmod::Value::new_string(#too_many_args_error)
.into_mortal()
.into_raw());
@@ -417,8 +421,8 @@ pub fn handle_function(
#visibility_action
let argmark = unsafe { ::perlmod::ffi::pop_arg_mark() };
- let mut args = argmark.iter();
- { let _ = &mut args; }
+ let mut #arguments_name = argmark.iter();
+ { let _ = &mut #arguments_name; }
#extract_arguments
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct
2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
` (2 preceding siblings ...)
2026-07-24 14:49 ` [PATCH perlmod v2 3/5] macro: function: explicitly pass identifier for generated arg iter Max R. Carrara
@ 2026-07-24 14:49 ` Max R. Carrara
2026-07-24 14:49 ` [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute Max R. Carrara
4 siblings, 0 replies; 6+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
To: pve-devel
The handling of function signature inputs adds a lot of mutable state
to the body of the `handle_function()` fn, which makes it somewhat
hard to track when that state is actually mutated throughout the
function.
Therefore, introduce the private `FnInputs` struct and heave all of
the fn signature input handling into `FnInputs::from_signature()` to
limit the scope where mutability occurs. Additionally, use an inline
`State` struct inside `FnInputs::from_signature()` in order to make it
explicit at a glance when mutable state is in fact mutated.
Also introduce the `TrailingType` enum and use it inside `FnInputs`
instead of the `had_list_param` boolean in order to make adding such
proc macro parameter attributes a little easier in the future.
Take the opportunity to improve the overall signature inputs handling
logic by making use of some more pattern matching and using `break`s
from labeled blocks [1] instead of an if-elseif-else expression when
generating the code for the `None` handling.
Adapt the `gen_prototype()` function and make it take a reference to
`&FnInputs` instead, and also use pattern matching when defining the
string for the Perl subroutine prototype. Avoid shadowing the
`arg_count` variable too by renaming the parameter to
`total_arg_count`.
Adapt the `handle_return_kind()` function and make it take a reference
to `&FnInputs` as well, removing obsolete parameters in the process.
Adapt its body accordingly.
[1] https://blog.rust-lang.org/2022/11/03/Rust-1.65.0/#break-from-labeled-blocks
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
perlmod-macro/src/function.rs | 326 ++++++++++++++++++++++------------
1 file changed, 208 insertions(+), 118 deletions(-)
diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index 3ca7bd0..f5415c5 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -210,6 +210,171 @@ fn deserialized_argument_code(
}
}
+enum TrailingType {
+ List,
+}
+
+impl TrailingType {
+ fn as_str(&self) -> &'static str {
+ match self {
+ Self::List => "list",
+ }
+ }
+}
+
+struct FnInputs {
+ trailing_options: usize,
+ extract_arguments: TokenStream,
+ deserialized_arguments: TokenStream,
+ passed_arguments: TokenStream,
+ cv_arg_param: TokenStream,
+ trailing_type: Option<TrailingType>,
+}
+
+impl FnInputs {
+ fn from_signature(
+ signature: &mut syn::Signature,
+ arguments_name: &Ident,
+ ) -> Result<Self, Error> {
+ #[derive(Default)]
+ struct State {
+ trailing_options: usize,
+ extract_arguments: TokenStream,
+ deserialized_arguments: TokenStream,
+ passed_arguments: TokenStream,
+ cv_arg_param: TokenStream,
+ trailing_type: Option<TrailingType>,
+ }
+
+ let mut state = State::default();
+
+ let span = signature.ident.span();
+
+ for arg in &mut signature.inputs {
+ let arg_attr = ArgumentAttr::new_from_fn_arg(arg)?;
+
+ let arg_type: &syn::Type = arg_attr.pat_type.ty.as_ref();
+
+ let arg_name = {
+ let pattern: &syn::Pat = arg_attr.pat_type.pat.as_ref();
+ match pattern {
+ syn::Pat::Ident(ident) => {
+ if ident.by_ref.is_some() {
+ bail!(ident => "xsub does not support by-ref parameters");
+ }
+ if ident.subpat.is_some() {
+ bail!(ident => "xsub does not support sub-patterns on parameters");
+ }
+ &ident.ident
+ }
+ _ => bail!(pattern => "xsub does not support this kind of parameter"),
+ }
+ };
+
+ if let Some(ref attr_type) = arg_attr.attr_type {
+ match attr_type {
+ ArgumentAttrType::Raw => {}
+ ArgumentAttrType::TryFromRef => {}
+ ArgumentAttrType::CVPtr(cv_span) => {
+ if !state.cv_arg_param.is_empty() {
+ bail!(*cv_span, "only 1 'cv' parameter allowed");
+ }
+
+ state.cv_arg_param = quote_spanned! { span=> #arg_name: #arg_type };
+ if state.passed_arguments.is_empty() {
+ state
+ .passed_arguments
+ .extend(quote_spanned! { span=> #arg_name });
+ } else {
+ state
+ .passed_arguments
+ .extend(quote_spanned! { span=> , #arg_name });
+ }
+
+ continue;
+ }
+ ArgumentAttrType::TrailingList(trailing_span) => {
+ if let Some(trailing_type) = state.trailing_type {
+ bail!(
+ *trailing_span,
+ "only 1 parameter for trailing values allowed, already got '{}'",
+ trailing_type.as_str(),
+ );
+ }
+
+ state.trailing_type = Some(TrailingType::List);
+ }
+ }
+ }
+
+ 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());
+
+ let missing_message = syn::LitStr::new(
+ &format!("missing required parameter: '{arg_name}'\n"),
+ arg_name.span(),
+ );
+
+ let none_handling = 'handling: {
+ if is_option_type(arg_type).is_some() {
+ state.trailing_options += 1;
+ break 'handling quote_spanned! { span=> ::perlmod::Value::new_undef(), };
+ }
+
+ if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
+ break 'handling TokenStream::new();
+ }
+
+ // only count the trailing options
+ state.trailing_options = 0;
+ quote_spanned! { span=>
+ {
+ return Err(::perlmod::Value::new_string(#missing_message)
+ .into_mortal()
+ .into_raw());
+ }
+ }
+ };
+
+ let arg_code = extract_argument_code(
+ &arg_attr,
+ span,
+ arguments_name,
+ &extracted_name,
+ none_handling,
+ );
+
+ let de_arg_code = deserialized_argument_code(
+ &arg_attr,
+ span,
+ arg_type,
+ &deserialized_name,
+ extracted_name,
+ );
+
+ let passed_arg = if state.passed_arguments.is_empty() {
+ quote_spanned! { span=> #deserialized_name }
+ } else {
+ quote_spanned! { span=> , #deserialized_name }
+ };
+
+ state.extract_arguments.extend(arg_code);
+ state.deserialized_arguments.extend(de_arg_code);
+ state.passed_arguments.extend(passed_arg)
+ }
+
+ Ok(Self {
+ trailing_options: state.trailing_options,
+ extract_arguments: state.extract_arguments,
+ deserialized_arguments: state.deserialized_arguments,
+ passed_arguments: state.passed_arguments,
+ cv_arg_param: state.cv_arg_param,
+ trailing_type: state.trailing_type,
+ })
+ }
+}
+
struct Return {
result: bool,
value: ReturnValue,
@@ -255,99 +420,7 @@ pub fn handle_function(
let arguments_name = syn::Ident::new("args", 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();
- let mut had_list_param = false;
- for arg in &mut func.sig.inputs {
- let arg_attr = ArgumentAttr::new_from_fn_arg(arg)?;
-
- if let Some(ArgumentAttrType::TrailingList(list_span)) = arg_attr.attr_type {
- if had_list_param {
- bail!(list_span, "only 1 #[list] parameter allowed");
- }
-
- had_list_param = true;
- }
-
- let arg_name = match &*arg_attr.pat_type.pat {
- syn::Pat::Ident(ident) => {
- if ident.by_ref.is_some() {
- bail!(ident => "xsub does not support by-ref parameters");
- }
- if ident.subpat.is_some() {
- bail!(ident => "xsub does not support sub-patterns on parameters");
- }
- &ident.ident
- }
- _ => bail!(&arg_attr.pat_type.pat => "xsub does not support this kind of parameter"),
- };
-
- let arg_type = &*arg_attr.pat_type.ty;
-
- if let Some(ArgumentAttrType::CVPtr(cv_span)) = arg_attr.attr_type {
- if !cv_arg_param.is_empty() {
- bail!(cv_span, "only 1 'cv' parameter allowed");
- }
- cv_arg_param = quote_spanned! { span=> #arg_name: #arg_type };
- if passed_arguments.is_empty() {
- passed_arguments.extend(quote_spanned! { span=> #arg_name });
- } else {
- passed_arguments.extend(quote_spanned! { span=> , #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());
-
- let missing_message = syn::LitStr::new(
- &format!("missing required parameter: '{arg_name}'\n"),
- arg_name.span(),
- );
-
- let none_handling = if is_option_type(arg_type).is_some() {
- trailing_options += 1;
- quote_spanned! { span=> ::perlmod::Value::new_undef(), }
- } else if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
- TokenStream::new()
- } else {
- // only count the trailing options;
- trailing_options = 0;
- quote_spanned! { span=>
- {
- return Err(::perlmod::Value::new_string(#missing_message)
- .into_mortal()
- .into_raw());
- }
- }
- };
-
- extract_arguments.extend(extract_argument_code(
- &arg_attr,
- span,
- &arguments_name,
- &extracted_name,
- none_handling,
- ));
-
- deserialized_arguments.extend(deserialized_argument_code(
- &arg_attr,
- span,
- arg_type,
- &deserialized_name,
- extracted_name,
- ));
-
- if passed_arguments.is_empty() {
- passed_arguments.extend(quote_spanned! { span=> #deserialized_name });
- } else {
- passed_arguments.extend(quote_spanned! { span=> , #deserialized_name });
- }
- }
+ let inputs = FnInputs::from_signature(&mut func.sig, &arguments_name)?;
let has_return_value = match &func.sig.output {
syn::ReturnType::Default => Return {
@@ -370,12 +443,12 @@ pub fn handle_function(
},
};
- let finalize_arguments = if !had_list_param {
+ let finalize_arguments = if !inputs.trailing_type.is_some() {
let too_many_args_error = syn::LitStr::new(
&format!(
"too many parameters for function '{}', (expected {})\n",
name,
- func.sig.inputs.len() - (!cv_arg_param.is_empty()) as usize
+ func.sig.inputs.len() - (!inputs.cv_arg_param.is_empty()) as usize
),
Span::call_site(),
);
@@ -397,13 +470,12 @@ pub fn handle_function(
wrapper_func,
} = handle_return_kind(
&attr,
+ &inputs,
has_return_value,
&name,
&xs_name,
&impl_xs_name,
- passed_arguments,
export_public,
- !cv_arg_param.is_empty(),
)?;
let visibility_action = check_visibility(&func);
@@ -414,6 +486,10 @@ pub fn handle_function(
#wrapper_func
};
+ let cv_arg_param = &inputs.cv_arg_param;
+ let extract_arguments = &inputs.extract_arguments;
+ let deserialized_arguments = &inputs.deserialized_arguments;
+
tokens.extend(quote_spanned! { span=>
#[inline(never)]
#[allow(non_snake_case)]
@@ -451,36 +527,48 @@ 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,
- had_list_param,
- ))
- }),
+ prototype: attr
+ .prototype
+ .or_else(|| Some(gen_prototype(func.sig.inputs.len(), &inputs))),
})
}
-fn gen_prototype(arg_count: usize, trailing_options: usize, had_list_param: bool) -> String {
- let arg_count = arg_count - trailing_options - (had_list_param as usize);
+fn gen_prototype(total_arg_count: usize, inputs: &FnInputs) -> String {
+ let trailing_options = inputs.trailing_options;
+ let trailing_type = inputs.trailing_type.as_ref();
+
+ let arg_count = total_arg_count - trailing_options - (trailing_type.is_some() as usize);
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('$');
+
+ match (trailing_options, trailing_type) {
+ (1.., ty) => {
+ proto.push(';');
+
+ for _ in 0..trailing_options {
+ proto.push('$');
+ }
+
+ match ty {
+ Some(TrailingType::List) => proto.push('@'),
+ None => {}
+ }
+
+ proto
}
- if had_list_param {
- proto.push('@');
+ (0, ty) => {
+ match ty {
+ Some(TrailingType::List) => proto.push_str(";@"),
+ None => {}
+ }
+
+ proto
}
- } else if had_list_param {
- proto.push_str(";@");
}
- proto
}
struct ReturnHandling {
@@ -492,16 +580,17 @@ struct ReturnHandling {
#[allow(clippy::too_many_arguments)]
fn handle_return_kind(
attr: &FunctionAttrs,
+ inputs: &FnInputs,
ret: Return,
name: &Ident,
xs_name: &Ident,
impl_xs_name: &Ident,
- passed_arguments: TokenStream,
export_public: Option<&syn::Visibility>,
- cv_arg: bool,
) -> Result<ReturnHandling, Error> {
let span = name.span();
+ let passed_arguments = &inputs.passed_arguments;
+
let return_type;
let mut handle_return;
let wrapper_func;
@@ -511,7 +600,8 @@ fn handle_return_kind(
None => quote_spanned! { span=> #[allow(non_snake_case)] },
};
- let (cv_arg_name, cv_arg_passed) = if cv_arg {
+ let has_cv_arg = !inputs.cv_arg_param.is_empty();
+ let (cv_arg_name, cv_arg_passed) = if has_cv_arg {
(
quote_spanned! { span=> cv },
quote_spanned! { span=> ::perlmod::Value::from_raw_ref(cv as *mut ::perlmod::ffi::SV) },
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH perlmod v2 5/5] perlmod, macro: add #[hash] parameter attribute
2026-07-24 14:49 [PATCH perlmod v2 0/5] perlmod: add #[hash] parameter attribute Max R. Carrara
` (3 preceding siblings ...)
2026-07-24 14:49 ` [PATCH perlmod v2 4/5] macro: function: move signature inputs handling into helper struct Max R. Carrara
@ 2026-07-24 14:49 ` Max R. Carrara
4 siblings, 0 replies; 6+ messages in thread
From: Max R. Carrara @ 2026-07-24 14:49 UTC (permalink / raw)
To: pve-devel
Like `#[list]`, `#[hash]` allows us to support subs for which any
number of additional key-value pairs may be passed, which is the same
as having an `;%` at the end of the Perl prototype.
The `ParamsIter` type is added as a helper for pairwise iteration that
makes use of the fact that `ffi::StackIter` is now an
`ExactSizeIterator`, returning an error if the number of values on the
stack is odd on creation. It is then directly passed on to
serde's `MapDeserializer`, mirroring how `#[list]` works with
`SeqDeserializer`.
That way we can support additional Perl APIs without new parameters
breaking the Rust side of the API, just like we can with `#[list]`.
Also document the `#[hash]` parameter and add corresponding tests
along the way.
Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
perlmod-macro/src/function.rs | 69 +++++++++++++++++++++++++++++++----
perlmod/src/de.rs | 44 ++++++++++++++++++++++
perlmod/src/lib.rs | 5 +++
testlib-tests/01-hello.t | 46 ++++++++++++++++++++++-
testlib/src/lib.rs | 24 +++++++++++-
5 files changed, 178 insertions(+), 10 deletions(-)
diff --git a/perlmod-macro/src/function.rs b/perlmod-macro/src/function.rs
index f5415c5..aa48cc9 100644
--- a/perlmod-macro/src/function.rs
+++ b/perlmod-macro/src/function.rs
@@ -28,6 +28,11 @@ enum ArgumentAttrType {
/// Slurp the remaining arguments (like an `@rest` at the end).
/// This requires the parameter to implement `FromIterator<T: Deserialize>`.
TrailingList(Span),
+
+ /// Slurp the remaining arguments (like a `%hash` at the end).
+ /// This requires the parameter to implement `FromIterator<(K, V)> where K: Deserialize, V:
+ /// Deserialize`.
+ TrailingHash(Span),
}
impl ArgumentAttrType {
@@ -52,6 +57,10 @@ impl ArgumentAttrType {
return Some(Self::TrailingList(path.span()));
}
+ if path.is_ident("hash") {
+ return Some(Self::TrailingHash(path.span()));
+ }
+
None
}
@@ -65,6 +74,7 @@ impl ArgumentAttrType {
Self::TryFromRef => "try_from_ref",
Self::CVPtr(_) => "cv",
Self::TrailingList(_) => "list",
+ Self::TrailingHash(_) => "hash",
}
}
}
@@ -114,7 +124,7 @@ impl<'s> ArgumentAttr<'s> {
// At this point we have two differing attributes
error!(
span,
- "`raw`, `try_from_ref`, `cv`, and `list` attributes are mutually exclusive"
+ "`raw`, `try_from_ref`, `cv`, `list`, and `hash` attributes are mutually exclusive"
);
has_err = true;
false
@@ -141,7 +151,7 @@ fn extract_argument_code(
none_handling: TokenStream,
) -> TokenStream {
match arg_attr.attr_type {
- Some(ArgumentAttrType::TrailingList(_)) => {
+ Some(ArgumentAttrType::TrailingList(_) | ArgumentAttrType::TrailingHash(_)) => {
quote_spanned! { span=>
let #extracted_name = #arguments_name.map(::perlmod::Value::from);
}
@@ -196,6 +206,32 @@ fn deserialized_argument_code(
}
};
},
+ Some(ArgumentAttrType::TrailingHash(_)) => quote_spanned! { span=>
+ let #deserialized_name = {
+ let _guard = ::perlmod::__private__::InParameterDeserialization::guard();
+ match ::perlmod::de::ParamsIter::new(#extracted_name) {
+ Ok(params_iter) => {
+ match <#arg_type as ::perlmod::__private__::serde::Deserialize>::deserialize(
+ ::perlmod::__private__::serde::de::value::MapDeserializer::new(
+ params_iter
+ )
+ ) {
+ Ok(map) => map,
+ Err(err) => {
+ return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+ .into_mortal()
+ .into_raw());
+ }
+ }
+ },
+ Err(err) => {
+ return Err(::perlmod::Value::new_string(&format!("{err:#}\n"))
+ .into_mortal()
+ .into_raw());
+ }
+ }
+ };
+ },
Some(ArgumentAttrType::CVPtr(_)) | None => quote_spanned! { span=>
let #deserialized_name: #arg_type =
match ::perlmod::from_ref_value(&#extracted_name) {
@@ -212,12 +248,22 @@ fn deserialized_argument_code(
enum TrailingType {
List,
+ Hash,
}
impl TrailingType {
fn as_str(&self) -> &'static str {
match self {
Self::List => "list",
+ Self::Hash => "hash",
+ }
+ }
+
+ fn from_attr_type(attr_type: &ArgumentAttrType) -> Self {
+ match attr_type {
+ ArgumentAttrType::TrailingList(_) => Self::List,
+ ArgumentAttrType::TrailingHash(_) => Self::Hash,
+ _ => unreachable!(),
}
}
}
@@ -272,10 +318,12 @@ impl FnInputs {
};
if let Some(ref attr_type) = arg_attr.attr_type {
+ use ArgumentAttrType as AT;
+
match attr_type {
- ArgumentAttrType::Raw => {}
- ArgumentAttrType::TryFromRef => {}
- ArgumentAttrType::CVPtr(cv_span) => {
+ AT::Raw => {}
+ AT::TryFromRef => {}
+ AT::CVPtr(cv_span) => {
if !state.cv_arg_param.is_empty() {
bail!(*cv_span, "only 1 'cv' parameter allowed");
}
@@ -293,7 +341,7 @@ impl FnInputs {
continue;
}
- ArgumentAttrType::TrailingList(trailing_span) => {
+ AT::TrailingList(trailing_span) | AT::TrailingHash(trailing_span) => {
if let Some(trailing_type) = state.trailing_type {
bail!(
*trailing_span,
@@ -302,7 +350,7 @@ impl FnInputs {
);
}
- state.trailing_type = Some(TrailingType::List);
+ state.trailing_type = Some(TrailingType::from_attr_type(attr_type));
}
}
}
@@ -322,7 +370,10 @@ impl FnInputs {
break 'handling quote_spanned! { span=> ::perlmod::Value::new_undef(), };
}
- if matches!(arg_attr.attr_type, Some(ArgumentAttrType::TrailingList(_))) {
+ if matches!(
+ arg_attr.attr_type,
+ Some(ArgumentAttrType::TrailingList(_) | ArgumentAttrType::TrailingHash(_))
+ ) {
break 'handling TokenStream::new();
}
@@ -555,6 +606,7 @@ fn gen_prototype(total_arg_count: usize, inputs: &FnInputs) -> String {
match ty {
Some(TrailingType::List) => proto.push('@'),
+ Some(TrailingType::Hash) => proto.push('%'),
None => {}
}
@@ -563,6 +615,7 @@ fn gen_prototype(total_arg_count: usize, inputs: &FnInputs) -> String {
(0, ty) => {
match ty {
Some(TrailingType::List) => proto.push_str(";@"),
+ Some(TrailingType::Hash) => proto.push_str(";%"),
None => {}
}
diff --git a/perlmod/src/de.rs b/perlmod/src/de.rs
index cf1601f..1355d0c 100644
--- a/perlmod/src/de.rs
+++ b/perlmod/src/de.rs
@@ -782,3 +782,47 @@ impl<'de> MapAccess<'de> for RawDeserializer<'_> {
}
}
}
+
+pub struct ParamsIter<I> {
+ inner: I,
+}
+
+impl<I> ParamsIter<I>
+where
+ I: Iterator<Item = Value> + ExactSizeIterator,
+{
+ pub fn new<T>(into_iter: T) -> Result<Self, Error>
+ where
+ T: IntoIterator<Item = I::Item, IntoIter = I>,
+ {
+ let iter = into_iter.into_iter();
+
+ if iter.len() % 2 != 0 {
+ Err(Error::new(
+ "odd number of elements for parameter hash - must be even",
+ ))
+ } else {
+ Ok(Self { inner: iter })
+ }
+ }
+}
+
+impl<I> Iterator for ParamsIter<I>
+where
+ I: Iterator<Item = Value> + ExactSizeIterator,
+{
+ type Item = (Value, Value);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(key) = self.inner.next() {
+ let value = self
+ .inner
+ .next()
+ .expect("expected value for key - odd number of elements in params iterator");
+
+ Some((key, value))
+ } else {
+ None
+ }
+ }
+}
diff --git a/perlmod/src/lib.rs b/perlmod/src/lib.rs
index 83b9d8b..2dd1eb2 100644
--- a/perlmod/src/lib.rs
+++ b/perlmod/src/lib.rs
@@ -119,6 +119,11 @@ pub use perlmod_macro::package;
/// `serde::de::value::SeqDeserializer`.) This causes the prototype to end with `;@` (behaves
/// correctly with `Option` parameters in front of it).
///
+/// * `#[hash]`: Like `#[list]`, but instead of deserializing from a sequence, the final parameter
+/// needs to be able to deserialize from a map. (Technically just adapts Perl's argument stack
+/// into a `serde::de::value::MapDeserializer`.) This causes the prototype to end with `;%`
+/// (behaves correctly with `Option` parameters in front of it).
+///
/// For an example on making blessed objects, see [`Value::bless_box`](Value::bless_box()).
pub use perlmod_macro::export;
diff --git a/testlib-tests/01-hello.t b/testlib-tests/01-hello.t
index 535e5fb..9770525 100644
--- a/testlib-tests/01-hello.t
+++ b/testlib-tests/01-hello.t
@@ -1,6 +1,6 @@
use v5.36;
-use Test::More tests => 16;
+use Test::More tests => 24;
use TestLib::Hello;
@@ -63,3 +63,47 @@ is(
'final option unset, empty list',
);
is(TestLib::Hello::sum_list(50, 51, 52), 153, 'pass a deserialized list');
+
+is(
+ TestLib::Hello::trailing_hash(60, 61 => 62),
+ 'first=60, rest is {61: 62}',
+ 'collecting 1 trailing key-value pair',
+);
+is(
+ TestLib::Hello::trailing_hash_ordered(63, 64 => 65, 66 => 67),
+ 'first=63, rest is {64: 65, 66: 67}',
+ 'collecting 2 trailing key-value pairs',
+);
+is(
+ TestLib::Hello::trailing_hash(67),
+ 'first=67, rest is {}',
+ 'collecting 0 trailing key-value pairs',
+);
+
+is(
+ eval { TestLib::Hello::trailing_hash(68, 69) } // $@,
+ "error: odd number of elements for parameter hash - must be even\n",
+ 'failing to collect odd number of trailing hash parameters',
+);
+
+is(
+ TestLib::Hello::trailing_hash_and_options(80),
+ 'first=80, second=None, rest is {}',
+ 'final option unset, collecting 0 trailing key-value pairs',
+);
+is(
+ TestLib::Hello::trailing_hash_and_options(81, 82),
+ 'first=81, second=Some(82), rest is {}',
+ 'final option set, collecting 0 trailing key-value pairs',
+);
+is(
+ TestLib::Hello::trailing_hash_and_options(81, 82, 83 => 84),
+ 'first=81, second=Some(82), rest is {83: 84}',
+ 'final option set, collecting 1 trailing key-value pairs',
+);
+
+is(
+ eval { TestLib::Hello::trailing_hash_and_options(85, 86, 87) } // $@,
+ "error: odd number of elements for parameter hash - must be even\n",
+ 'final option set, collecting 1 trailing key-value pairs',
+);
diff --git a/testlib/src/lib.rs b/testlib/src/lib.rs
index ac3bfde..81879cc 100644
--- a/testlib/src/lib.rs
+++ b/testlib/src/lib.rs
@@ -12,7 +12,10 @@ mod main_lib {}
#[perlmod::package(name = "TestLib::Hello", lib = "testlib", boot = "loaded")]
mod export {
- use std::sync::atomic::{AtomicBool, Ordering};
+ use std::{
+ collections::{BTreeMap, HashMap},
+ sync::atomic::{AtomicBool, Ordering},
+ };
use anyhow::{Error, bail};
use serde::{Deserialize, Serialize};
@@ -137,4 +140,23 @@ mod export {
fn sum_list(#[list] rest: Vec<u32>) -> u32 {
rest.into_iter().sum()
}
+
+ #[export]
+ fn trailing_hash(first: u32, #[hash] rest: HashMap<u32, u32>) -> String {
+ format!("first={first}, rest is {rest:?}")
+ }
+
+ #[export]
+ fn trailing_hash_ordered(first: u32, #[hash] rest: BTreeMap<u32, u32>) -> String {
+ format!("first={first}, rest is {rest:?}")
+ }
+
+ #[export]
+ fn trailing_hash_and_options(
+ first: u32,
+ second: Option<u32>,
+ #[hash] rest: HashMap<u32, u32>,
+ ) -> String {
+ format!("first={first}, second={second:?}, rest is {rest:?}")
+ }
}
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread