From 8ac036f6cc5549997b969d4c43c9d3897f3f5383 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 30 Jan 2022 16:14:52 +0100 Subject: [PATCH] Add an API for persisting arbitrary fields --- nih_plug_derive/src/lib.rs | 118 ++++++++++++++++++++++++++++++++----- src/params.rs | 25 +++++--- 2 files changed, 121 insertions(+), 22 deletions(-) diff --git a/nih_plug_derive/src/lib.rs b/nih_plug_derive/src/lib.rs index b65c1328..d859dd0b 100644 --- a/nih_plug_derive/src/lib.rs +++ b/nih_plug_derive/src/lib.rs @@ -4,7 +4,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::spanned::Spanned; -#[proc_macro_derive(Params, attributes(id))] +#[proc_macro_derive(Params, attributes(id, persist))] pub fn derive_params(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as syn::DeriveInput); @@ -24,19 +24,23 @@ pub fn derive_params(input: TokenStream) -> TokenStream { } }; - // We only care about fields with an `id` attribute. We'll build a that creates a hashmap - // containing pointers to those parmaeters. - let mut param_insert_tokens = Vec::new(); + // We only care about fields with `id` and `persist` attributes. For the `id` fields we'll build + // a mapping function that creates a hashmap containing pointers to those parmaeters. For the + // `persist` function we'll create functions that serialize and deserialize those fields + // individually (so they can be added and removed independently of eachother) using JSON. + let mut param_mapping_insert_tokens = Vec::new(); + let mut field_serialize_tokens = Vec::new(); + let mut field_deserialize_tokens = Vec::new(); for field in fields.named { let field_name = match &field.ident { Some(ident) => ident, _ => continue, }; - // We'll add another attribute for persistent fields later, and that's going to be mutually - // exclusive with this id attribute - let mut id_attr = None; - for attr in field.attrs { + // These two attributes are mutually exclusive + let mut id_attr: Option = None; + let mut persist_attr: Option = None; + for attr in &field.attrs { if attr.path.is_ident("id") { match attr.parse_meta() { Ok(syn::Meta::NameValue(syn::MetaNameValue { @@ -60,14 +64,83 @@ pub fn derive_params(input: TokenStream) -> TokenStream { .into() } }; + } else if attr.path.is_ident("persist") { + match attr.parse_meta() { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Str(s), + .. + })) => { + if persist_attr.is_none() { + persist_attr = Some(s.value()); + } else { + return syn::Error::new(attr.span(), "Duplicate persist attribute") + .to_compile_error() + .into(); + } + } + _ => { + return syn::Error::new( + attr.span(), + "The persist attribute should be a key-value pair with a string argument: #[persist = \"foo_bar\"]", + ) + .to_compile_error() + .into() + } + }; } } - if let Some(param_id) = id_attr { - // The specific parameter types know how to convert themselves into the correct ParamPtr - // variant - param_insert_tokens - .push(quote! { param_map.insert(#param_id, self.#field_name.as_ptr()); }); + match (id_attr, persist_attr) { + (Some(param_id), None) => { + // The specific parameter types know how to convert themselves into the correct ParamPtr + // variant + param_mapping_insert_tokens + .push(quote! { param_map.insert(#param_id, self.#field_name.as_ptr()); }); + } + (None, Some(stable_name)) => { + // We don't know anything about the field types, but because we can generate this + // function we get type erasure for free since we only need to worry about byte + // vectors + field_serialize_tokens.push(quote! { + match ::nih_plug::params::serialize_field(&self.#field_name) { + Ok(data) => { + serialized.insert(String::from(#stable_name), data); + } + Err(err) => { + ::nih_plug::nih_log!( + "Could not serialize '{}': {}", + #stable_name, + err + ) + } + }; + }); + field_deserialize_tokens.push(quote! { + #stable_name => { + match ::nih_plug::params::deserialize_field(&data) { + Ok(deserialized) => { + self.#field_name = deserialized; + } + Err(err) => { + ::nih_plug::nih_log!( + "Could not deserialize '{}': {}", + #stable_name, + err + ) + } + }; + } + }); + } + (Some(_), Some(_)) => { + return syn::Error::new( + field.span(), + "The id and persist attributes are mutually exclusive", + ) + .to_compile_error() + .into(); + } + (None, None) => (), } } @@ -78,10 +151,27 @@ pub fn derive_params(input: TokenStream) -> TokenStream { ) -> std::collections::HashMap<&'static str, nih_plug::params::ParamPtr> { let mut param_map = std::collections::HashMap::new(); - #(#param_insert_tokens)* + #(#param_mapping_insert_tokens)* param_map } + + fn serialize_fields(&self) -> ::std::collections::HashMap> { + let mut serialized = ::std::collections::HashMap::new(); + + #(#field_serialize_tokens)* + + serialized + } + + fn deserialize_fields(&mut self, serialized: ::std::collections::HashMap<&str, Vec>) { + for (field_name, data) in serialized { + match field_name { + #(#field_deserialize_tokens)* + _ => nih_log!("Unknown field name: {}", field_name), + } + } + } } } .into() diff --git a/src/params.rs b/src/params.rs index bd14bd10..3416030b 100644 --- a/src/params.rs +++ b/src/params.rs @@ -21,6 +21,11 @@ use std::pin::Pin; pub type FloatParam = PlainParam; pub type IntParam = PlainParam; +/// Re-export for use in the [Params] proc-macro. +pub use serde_json::from_slice as deserialize_field; +/// Re-export for use in the [Params] proc-macro. +pub use serde_json::to_vec as serialize_field; + /// A distribution for a parameter's range. Probably need to add some forms of skewed ranges and /// maybe a callback based implementation at some point. #[derive(Debug)] @@ -292,16 +297,20 @@ impl NormalizebleRange for Range { /// This implementation is safe when using from the wrapper because the plugin object needs to be /// pinned, and it can never outlive the wrapper. pub trait Params { - /// Create a mapping from unique parameter IDs to parameters. Dereferencing the pointers stored - /// in the values is only valid as long as this pinned object is valid. + /// Create a mapping from unique parameter IDs to parameters. This is done for every parameter + /// field marked with `#[id = "stable_name"]`. Dereferencing the pointers stored in the values + /// is only valid as long as this pinned object is valid. fn param_map(self: Pin<&Self>) -> HashMap<&'static str, ParamPtr>; - // TODO: Also handle custom state persistence in this state. Instead o having a callback for - // custom state loading and restoring, it will be way nicer to use serde for this. Another - // attribute `#[persist]` can mark fields that are `Serialize + Deserialize`. They these - // fields can then be serialized alongside the `param_id: normalized_value` pairs for - // normal parameters. - // fn persistent_fields(self: Pin<&mut Self>) -> Vec<&mut (dyn Serialize + Deserialize + Send + Sync)>; + /// Serialize all fields marked with `#[persist = "stable_name"]` into a hash map containing + /// JSON-representations of those fields so they can be written to the plugin's state and + /// recalled later. This uses [serialize_field] under the hood. + fn serialize_fields(&self) -> HashMap>; + + /// Restore all fields marked with `#[persist = "stable_name"]` from a hashmap created by + /// [Self::serialize_fields]. This gets called when the plugin's state is being restored. This + /// uses [deserialize_field] under the hood. + fn deserialize_fields(&mut self, serialized: HashMap<&str, Vec>); } /// Internal pointers to parameters. This is an implementation detail used by the wrappers.