1
0
Fork 0

Allow nested parameter structs

See the Parameters docstring for the caveats.
This commit is contained in:
Robbert van der Helm 2022-03-03 20:37:01 +01:00
parent 76369ad1e1
commit be4bbd4400
4 changed files with 111 additions and 19 deletions

View file

@ -5,7 +5,7 @@ use quote::quote;
use std::collections::HashSet; use std::collections::HashSet;
use syn::spanned::Spanned; use syn::spanned::Spanned;
#[proc_macro_derive(Params, attributes(id, persist))] #[proc_macro_derive(Params, attributes(id, persist, nested))]
pub fn derive_params(input: TokenStream) -> TokenStream { pub fn derive_params(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input as syn::DeriveInput); let ast = syn::parse_macro_input!(input as syn::DeriveInput);
@ -25,16 +25,21 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
} }
}; };
// We only care about fields with `id` and `persist` attributes. For the `id` fields we'll build // We only care about fields with `id`, `persist`, and `nested` attributes. For the `id` fields
// a mapping function that creates a hashmap containing pointers to those parmaeters. For the // we'll build a mapping function that creates a hashmap containing pointers to those
// `persist` function we'll create functions that serialize and deserialize those fields // parmaeters. For the `persist` function we'll create functions that serialize and deserialize
// individually (so they can be added and removed independently of eachother) using JSON. // those fields individually (so they can be added and removed independently of eachother) using
// JSON. The `nested` fields should also implement the `Params` trait and their fields will be
// inherited and added to this field's lists.
let mut param_mapping_insert_tokens = Vec::new(); let mut param_mapping_insert_tokens = Vec::new();
let mut param_id_string_tokens = Vec::new(); let mut param_id_string_tokens = Vec::new();
let mut field_serialize_tokens = Vec::new(); let mut field_serialize_tokens = Vec::new();
let mut field_deserialize_tokens = Vec::new(); let mut field_deserialize_tokens = Vec::new();
let mut nested_fields_idents = Vec::new();
// We'll also enforce that there are no duplicate keys at compile time // We'll also enforce that there are no duplicate keys at compile time
// TODO: This doesn't work for nested fields since we don't know anything about the fields on
// the nested structs
let mut param_ids = HashSet::new(); let mut param_ids = HashSet::new();
let mut persist_ids = HashSet::new(); let mut persist_ids = HashSet::new();
for field in fields.named { for field in fields.named {
@ -46,6 +51,7 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
// These two attributes are mutually exclusive // These two attributes are mutually exclusive
let mut id_attr: Option<String> = None; let mut id_attr: Option<String> = None;
let mut persist_attr: Option<String> = None; let mut persist_attr: Option<String> = None;
let mut nested = false;
for attr in &field.attrs { for attr in &field.attrs {
if attr.path.is_ident("id") { if attr.path.is_ident("id") {
match attr.parse_meta() { match attr.parse_meta() {
@ -93,6 +99,26 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
.into() .into()
} }
}; };
} else if attr.path.is_ident("nested") {
match attr.parse_meta() {
Ok(syn::Meta::Path(_)) => {
if !nested {
nested = true;
} else {
return syn::Error::new(attr.span(), "Duplicate nested attribute")
.to_compile_error()
.into();
}
}
_ => {
return syn::Error::new(
attr.span(),
"The nested attribute should not have any arguments: #[nested]",
)
.to_compile_error()
.into();
}
};
} }
} }
@ -169,6 +195,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
} }
(None, None) => (), (None, None) => (),
} }
if nested {
nested_fields_idents.push(field_name.clone());
}
} }
quote! { quote! {
@ -180,21 +210,36 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
use ::nih_plug::Param; use ::nih_plug::Param;
let mut param_map = std::collections::HashMap::new(); let mut param_map = std::collections::HashMap::new();
#(#param_mapping_insert_tokens)* #(#param_mapping_insert_tokens)*
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
for nested_params in nested_fields {
unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) };
}
param_map param_map
} }
fn param_ids(self: std::pin::Pin<&Self>) -> &'static [&'static str] { fn param_ids(self: std::pin::Pin<&Self>) -> Vec<&'static str> {
&[#(#param_id_string_tokens)*] let mut ids = vec![#(#param_id_string_tokens)*];
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
for nested_params in nested_fields {
unsafe { ids.append(&mut Pin::new_unchecked(*nested_params).param_ids()) };
}
ids
} }
fn serialize_fields(&self) -> ::std::collections::HashMap<String, String> { fn serialize_fields(&self) -> ::std::collections::HashMap<String, String> {
let mut serialized = ::std::collections::HashMap::new(); let mut serialized = ::std::collections::HashMap::new();
#(#field_serialize_tokens)* #(#field_serialize_tokens)*
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
for nested_params in nested_fields {
unsafe { serialized.extend(Pin::new_unchecked(*nested_params).serialized_fields()) };
}
serialized serialized
} }
@ -202,9 +247,18 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
for (field_name, data) in serialized { for (field_name, data) in serialized {
match field_name.as_str() { match field_name.as_str() {
#(#field_deserialize_tokens)* #(#field_deserialize_tokens)*
_ => nih_log!("Unknown field name: {}", field_name), _ => nih_log!("Unknown serialized field name: {} (this may not be accurate)", field_name),
} }
} }
// FIXME: The above warning will course give false postiives when using nested
// parameter structs. An easy fix would be to use
// https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.drain_filter
// once that gets stabilized.
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
for nested_params in nested_fields {
unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) };
}
} }
} }
} }

View file

@ -23,7 +23,8 @@ pub mod widgets;
/// contains the GUI's intitial size, and this is kept in sync whenever the GUI gets resized. You /// contains the GUI's intitial size, and this is kept in sync whenever the GUI gets resized. You
/// can also use this to know if the GUI is open, so you can avoid performing potentially expensive /// can also use this to know if the GUI is open, so you can avoid performing potentially expensive
/// calculations while the GUI is not open. If you want this size to be persisted when restoring a /// calculations while the GUI is not open. If you want this size to be persisted when restoring a
/// plugin instance, then you can store it in a `#[persist]` field on your parameters struct. /// plugin instance, then you can store it in a `#[persist = "key"]` field on your parameters
/// struct.
/// ///
/// See [EguiState::from_size()]. /// See [EguiState::from_size()].
// //

View file

@ -19,14 +19,25 @@ struct GainParams {
#[id = "gain"] #[id = "gain"]
pub gain: FloatParam, pub gain: FloatParam,
#[id = "as_long_as_this_name_stays_constant"] #[id = "stable"]
pub the_field_name_can_change: BoolParam, pub but_field_names_can_change: BoolParam,
/// This field isn't used in this exampleq, but anything written to the vector would be restored /// This field isn't used in this exampleq, but anything written to the vector would be restored
/// together with a preset/state file saved for this plugin. This can be useful for storign /// together with a preset/state file saved for this plugin. This can be useful for storign
/// things like sample data. /// things like sample data.
#[persist = "industry_secrets"] #[persist = "industry_secrets"]
pub random_data: RwLock<Vec<f32>>, pub random_data: RwLock<Vec<f32>>,
/// You can also nest parameter structs. This is only for your own organization: they will still
/// appear as a flat list to the host.
#[nested]
pub sub_params: SubParams,
}
#[derive(Params)]
struct SubParams {
#[id = "thing"]
pub nested_parameter: FloatParam,
} }
impl Default for Gain { impl Default for Gain {
@ -63,7 +74,7 @@ impl Default for GainParams {
// // ..Default::default(), // // ..Default::default(),
}, },
// ...or use the builder interface: // ...or use the builder interface:
the_field_name_can_change: BoolParam::new("Important value", false).with_callback( but_field_names_can_change: BoolParam::new("Important value", false).with_callback(
Arc::new(|_new_value: bool| { Arc::new(|_new_value: bool| {
// If, for instance, updating this parameter would require other parts of the // If, for instance, updating this parameter would require other parts of the
// plugin's internal state to be updated other values to also be updated, then // plugin's internal state to be updated other values to also be updated, then
@ -73,6 +84,18 @@ impl Default for GainParams {
// Persisted fields can be intialized like any other fields, and they'll keep their when // Persisted fields can be intialized like any other fields, and they'll keep their when
// restoring the plugin's state. // restoring the plugin's state.
random_data: RwLock::new(Vec::new()), random_data: RwLock::new(Vec::new()),
sub_params: SubParams {
nested_parameter: FloatParam::new(
"Unused Nested Parameter",
0.5,
FloatRange::Skewed {
min: 2.0,
max: 2.4,
factor: FloatRange::skew_factor(2.0),
},
)
.with_value_to_string(formatters::f32_rounded(2)),
},
} }
} }
} }

View file

@ -15,10 +15,21 @@ pub use serde_json::to_string as serialize_field;
/// assigning a unique identifier to each parameter. We can then build a mapping from those /// assigning a unique identifier to each parameter. We can then build a mapping from those
/// parameter IDs to the parameters using the [Params::param_map()] function. That way we can have /// parameter IDs to the parameters using the [Params::param_map()] function. That way we can have
/// easy to work with JUCE-style parameter objects in the plugin without needing to manually /// easy to work with JUCE-style parameter objects in the plugin without needing to manually
/// register each parameter, like you would in JUCE. /// register each parameter, like you would in JUCE. When deriving this trait, any of those
/// parameters should have the `#[id = "stable"]` attribute, where `stable` is an up to 6 character
/// (to avoid collisions) string that will be used for the parameter's internal identifier.
/// ///
/// The other persistent parameters should be [PersistentField]s containing types that can be /// The other persistent parameters should be [PersistentField]s containing types that can be
/// serialized and deserialized with Serde. /// serialized and deserialized with Serde. When deriving this trait, any of those fields should be
/// marked with `#[persist = "key"]`.
///
/// And finally when deriving this trait, it is also possible to inherit the parameters from other
/// `Params` objects by adding the `#[nested]` attribute to those fields. Parameter IDs and
/// persisting keys still need to be **unique** when usting nested parameter structs. This currently
/// has the following caveats:
///
/// - Enforcing that parameter IDs and persist keys are unique does not work across nested structs.
/// - Deserializing persisted fields will give false positives about fields not existing.
/// ///
/// Take a look at the example gain plugin to see how this should be used. /// Take a look at the example gain plugin to see how this should be used.
/// ///
@ -26,8 +37,6 @@ pub use serde_json::to_string as serialize_field;
/// ///
/// This implementation is safe when using from the wrapper because the plugin object needs to be /// This implementation is safe when using from the wrapper because the plugin object needs to be
/// pinned, and it can never outlive the wrapper. /// pinned, and it can never outlive the wrapper.
//
// TODO: Add a `#[nested]` attribute for nested params objects
pub trait Params { pub trait Params {
/// Create a mapping from unique parameter IDs to parameters. This is done for every parameter /// 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 /// field marked with `#[id = "stable_name"]`. Dereferencing the pointers stored in the values
@ -36,7 +45,12 @@ pub trait Params {
/// All parameter IDs from `param_map`, in a stable order. This order will be used to display /// All parameter IDs from `param_map`, in a stable order. This order will be used to display
/// the parameters. /// the parameters.
fn param_ids(self: Pin<&Self>) -> &'static [&'static str]; ///
/// TODO: This used to be a static slice, but now that we supported nested parameter objects
/// that's become a bit more difficult since Rust does not have a convenient way to
/// concatenate an arbitrary number of static slices. There's probably a better way to do
/// this.
fn param_ids(self: Pin<&Self>) -> Vec<&'static str>;
/// Serialize all fields marked with `#[persist = "stable_name"]` into a hash map containing /// 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 /// JSON-representations of those fields so they can be written to the plugin's state and