diff --git a/Cargo.toml b/Cargo.toml index 74eaa89..7408617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ exclude = [ [workspace.package] authors = ["Antonio Yang "] -version = "0.12.6" +version = "0.12.7" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/README.md b/README.md index 778f357..92504a7 100644 --- a/README.md +++ b/README.md @@ -7,25 +7,26 @@ A library to help you modify config structs. It provides: - `Patch` derive macro for partial updates - `Filler` derive macro to fill empty fields -- `Substrate` and `Catalyst` derive macros to extend with extra fields +- `Substrate` and `Catalyst` derive macros to extend a struct with extra fields ## Introduction -This crate provides the `Patch`, `Filler`, `Substrate`, `Catalyst` and `Complex` traits with accompanying derive macros in following 3 using cases. +This crate provides the `Patch`, `Filler`, `Substrate`, `Catalyst` and `Complex` traits with accompanying derive macros in the following three use cases. -- If any field in `Patch` is `Some`, it will overwrite the corresponding field when applied. -- If any field in the instance is `None`, `Filler` will try to fill it, only supports `Option` and `Vec` fields. -- With `catalyst` feature, `Substrate`, `Catalyst` and `Complex` traits with accompanying derive macros help you extend struct with extra fields. +- If any field in a `Patch` is `Some`, it overwrites the corresponding field when applied. +- If any field in the instance is empty (`None` or an empty collection), `Filler` will try to fill it. It supports `Option`, `Vec`, `VecDeque`, `LinkedList`, `HashMap`, `BTreeMap`, `HashSet`, `BTreeSet`, `BinaryHeap` fields, as well as custom types via `#[filler(extendable)]` and any type via `#[filler(empty_value = ...)]`. +- With the `catalyst` feature, `Substrate`, `Catalyst` and `Complex` traits with accompanying derive macros help you extend a struct with extra fields from another crate. This crate supports `no_std` — check the [no-std-examples](./no-std-examples). -Following are more specific case, help you to learn the details. -- A project with config from environments, files, command line, you can easy to use `Patch` make your config organized. Please check this [template](https://github.com/yanganto/ConfigTemplate). -- A project extended from another project, and only some fields of config are different. We can use `catalyst` feature to expose the base struct in the build script with `Substrate`, then a `Catalyst` struct can bind to it and produce a complex struct. Check the [complex-example](./complex-example) and [Quick Example: case 3](#case-3---extend-a-struct-from-a-crate). +The following are more specific scenarios to help you learn the details: +- A project with config from environments, files, and command line: you can use `Patch` to keep your config organized. Please check this [template](https://github.com/yanganto/ConfigTemplate). +- A project extended from another project, where only some fields of the config differ. You can use the `catalyst` feature to expose the base struct in a build script with `Substrate`, then a `Catalyst` struct can bind to it and produce a complex struct. Check the [complex-example](./complex-example) and [Quick Example: case 3](#case-3---extend-a-struct-from-a-crate). ## Quick Example + #### Case 1 - Patch on a Config -Deriving `Patch` on a struct will generate a struct similar to the original one, but with all fields wrapped in an `Option`. +Deriving `Patch` on a struct generates a struct similar to the original one, but with all fields wrapped in an `Option`. An instance of such a patch struct can be applied onto the original struct, replacing values only if they are set to `Some`, leaving them unchanged otherwise. ```rust use struct_patch::Patch; @@ -53,13 +54,13 @@ fn patch_json() { let patch: ItemPatch = serde_json::from_str(data).unwrap(); item.apply(patch); - // You can do + // You can do // `let new_item = item << patch;` // For multiple patches, // you can do this // `let new_item = item << patch_1 << patch_2;` - // or make an aggregated one, but please make sure the patch fields do not conflict, else will panic + // or make an aggregated one, but please make sure the patch fields do not conflict, else it will panic // ``` // let overall_patch = patch_1 + patch_2 + patch_3; // let new_item = item << overall_patch; @@ -77,7 +78,7 @@ fn patch_json() { ``` #### Case 2 - Fill up on a Config -Deriving `Filler` on a struct will generate a struct similar to the original one with the field with `Option`. Unlike `Patch`, the `Filler` only work on the empty fields of instance. +Deriving `Filler` on a struct generates a struct similar to the original one, keeping only the fields that can be filled (`Option`, collections, `extendable`, or `empty_value` fields). Unlike `Patch`, the `Filler` only works on empty fields of the instance. ```rust use struct_patch::Filler; @@ -94,63 +95,66 @@ let mut item = Item { list: Vec::new(), }; -let filler_1 = ItemFiller{ maybe_field_int: Some(7), list: Vec::new() }; +let filler_1 = ItemFiller { maybe_field_int: Some(7), list: Vec::new() }; item.apply(filler_1); assert_eq!(item.maybe_field_int, Some(7)); -let filler_2 = ItemFiller{ maybe_field_int: Some(100), list: Vec::new() }; +let filler_2 = ItemFiller { maybe_field_int: Some(100), list: Vec::new() }; -// The field is not empty, so the filler has not effect. +// The field is not empty, so the filler has no effect. item.apply(filler_2); assert_eq!(item.maybe_field_int, Some(7)); -let filler_3 = ItemFiller{ maybe_field_int: Some(100), list: vec![7] }; +let filler_3 = ItemFiller { maybe_field_int: Some(100), list: vec![7] }; item.apply(filler_3); assert_eq!(item.maybe_field_int, Some(7)); assert_eq!(item.list, vec![7]); -``` +``` #### Case 3 - Extend a struct from a crate -Deriving `Substrate` on a struct will help you expose the field information, and you can easy to expose in build.rs of other crate. -Deriving `Catalyst` on can read the field information of Substrate and generate a new Complex struct. -The overall behavior likes [chemical catalysts](https://en.wikipedia.org/wiki/Enzyme_catalysis), a catalyst **bind** on a substrate to form a complex struct, which has all fields from substrate and catalyst. -Also, a complex can **decouple** without clone and return a catalyst and substrate. Check the [complex-example](./complex-example/catalyst/src/lib.rs). -In the future, we may have a sub feature with catalyst, such that it will provide no memory moving decouple, but need unsafe code. +Deriving `Substrate` on a struct exposes the field information so that other crates can access it in a `build.rs`. +Deriving `Catalyst` reads the field information of a `Substrate` and generates a new complex struct. +In the other words, the catalyst is a struct with extra fields that the developer writes down in the downstream crate. The complex is a generated struct that combines the substrate's fields with the catalyst's extra fields. The overall behavior is like [chemical catalysts](https://en.wikipedia.org/wiki/Enzyme_catalysis): a catalyst **binds** onto a substrate to form a complex struct, which has all fields from both. +A complex can also **decouple** without cloning, returning the original catalyst and substrate. Check the [complex-example](./complex-example/catalyst/src/lib.rs). +With the `unsafe` feature, `bind` and `decouple` use `ManuallyDrop` + `ptr::read` to avoid memory moves, and `__substrate_new` uses `MaybeUninit` + `ptr::write` while `__substrate_unpack` uses `ManuallyDrop` + `ptr::read`, such that the copy will be less. + +In terms of crate dependencies, the crate using `Substrate` is **upstream** (a dependency), and the crate using `Catalyst` is **downstream** (it depends on the substrate crate). The downstream crate calls `Substrate::expose()` in its `build.rs` to read the field information at compile time, then uses `#[catalyst(bind = ...)]` to generate the complex struct. ```rust -/// In $dependency_crate/src/lib.rs +/// In the substrate crate (src/lib.rs) use struct_patch::Substrate; + #[derive(Substrate)] pub struct Base { pub field_bool: bool, pub field_string: String, } -/// In $main_crate/src/build.rs +/// In the catalyst crate (build.rs) use struct_patch::Substrate; fn main() { - $dependency_crate::Base::expose(); + substrate::Base::expose(); } -/// In $main_crate/src/lib.rs +/// In the catalyst crate (src/lib.rs) use struct_patch::Catalyst; #[derive(Catalyst)] -#[catalyst(bind = substrate::Base)] +#[catalyst(bind = Base)] struct Amyloid { pub extra_bool: bool, pub extra_option: Option, } -// Now AmyloidComplex is generated +// Now AmyloidComplex is generated: // struct AmyloidComplex { // pub field_bool: bool, // pub field_string: String, // pub extra_bool: bool, // pub extra_option: Option, -//} -``` +// } +``` #### Case 4 - Avoid double-`Option` for `Option>` fields By default, deriving `Patch` wraps every field in an `Option`, so a field typed @@ -185,65 +189,82 @@ item.apply(ItemPatch { tags: Some(vec![]) }); assert_eq!(item.tags, Some(vec![])); ``` -## Documentation and Examples -You can modify the patch structure by defining `#[patch(...)]`, `#[filler(...)]` or `#[complex(...)]`(catalyst feature), `#[catalyst(...)]`(catalyst feature) attributes on the original struct or fields. -Two macros are provided for the catalyst feature because we need to handle the behaviors of two structs simultaneously — the catalyst itself and the product (complex). In general, the complex macro takes precedence over the catalyst macro when any conflict arises. +## Attributes + +You can customize the generated structs by defining `#[patch(...)]`, `#[filler(...)]`, `#[complex(...)]` (catalyst feature), or `#[catalyst(...)]` (catalyst feature) attributes on the original struct or its fields. +Two attribute namespaces are provided for the catalyst feature because we need to handle the behaviors of two structs simultaneously — the catalyst itself and the product (complex). In general, the `complex` macro takes precedence over the `catalyst` macro when any conflict arises. + +### Struct attributes -Struct attributes: - `#[patch(name = "...")]`: change the name of the generated patch struct. - `#[patch(attribute(...))]`: add attributes to the generated patch struct. - `#[patch(attribute(derive(...)))]`: add derives to the generated patch struct. -- `#[catalyst(bind = "...")]`: decide the base structure. (catalyst feature) -- `#[catalyst(keep_field_attribute)]`: All field attributes from a substrate or catalyst will be passed through to the complex, unless an override is explicitly specified for that field. (catalyst feature) -- `#[catalyst(exclude_field_attributes = ["..."])]`: When `keep_field_attribute` is used, specifies attribute names to exclude from being passed through to the complex struct fields. For example, `exclude_field_attributes = ["serde"]` strips all `#[serde(...)]` field attributes from the substrate before they reach the complex. (catalyst feature) -- `#[complex(override_field_attribute("$substrate_field_name", ...))]`: override the complex field attribute, for example `serde(default = "default_str").` (catalyst feature) -- `#[complex(name = "...")]`: change the name of the generated complex struct. (catalyst feature) -- `#[complex(attribute(...))]`: add attributes to the generated complex struct. (catalyst feature) - -Field attributes: +- `#[filler(attribute(...))]`: add attributes to the generated filler struct. +- `#[catalyst(bind = "...")]`: specify the base (substrate) structure. (catalyst feature) +- `#[catalyst(keep_field_attribute)]`: pass all field attributes from a substrate or catalyst through to the complex, unless an override is explicitly specified for that field. (catalyst feature) +- `#[catalyst(exclude_field_attributes = ["..."])]`: when `keep_field_attribute` is used, specifies attribute names to exclude from being passed through to the complex struct fields. For example, `exclude_field_attributes = ["serde"]` strips all `#[serde(...)]` field attributes from the substrate before they reach the complex. (catalyst feature) +- `#[complex(override_field_attribute("$substrate_field_name", ...))]`: override a complex field attribute, for example `serde(default = "default_str")`. (catalyst feature) +- `#[complex(name = "...")]`: change the name of the generated complex struct. (catalyst feature) +- `#[complex(attribute(...))]`: add attributes to the generated complex struct. (catalyst feature) + +### Field attributes + - `#[patch(skip)]`: skip the field in the generated patch struct. - `#[patch(name = "...")]`: change the type of the field in the generated patch struct. - `#[patch(attribute(...))]`: add attributes to the field in the generated patch struct. - `#[patch(attribute(derive(...)))]`: add derives to the field in the generated patch struct. -- `#[patch(empty_value = ...)]`: define a value as empty, so the corresponding field of patch will not wrapped by Option, and apply patch when the field is empty. +- `#[patch(empty_value = ...)]`: define a value as empty, so the corresponding field of the patch will not be wrapped by `Option`, and the patch is applied when the field differs from the empty value. - `#[patch(skip_wrap)]`: keep the field type as-is in the patch struct (no extra `Option` wrapping). Useful when the field is already `Option<...>` (for example `Option>`) and you do not want a double-`Option` in the patch. With `skip_wrap`, `None` in the patch means "no change" and `Some(v)` sets the field to `Some(v)` (including `Some(vec![])` to clear the vector). Cannot be combined with `empty_value`. -- `#[filler(extendable)]`: use the field of the struct for filler, the struct needs implement `Default`, `Extend`, `IntoIterator` and `is_empty`. -- `#[filler(empty_value = ...)]`: define a value as empty, so the corresponding field of Filler will be applied, even the field is not `Option` or `Extendable`. +- `#[patch(nesting)]`: treat the field as a nested patchable struct. The inner struct must also derive `Patch`. Requires the `nesting` feature. +- `#[patch(addable)]`: allow conflicting patches to add their values together with the `+` operator instead of panicking. Requires the `op` feature. +- `#[patch(add = fn)]`: like `addable`, but use the specified function to combine values. Requires the `op` feature. +- `#[filler(extendable)]`: use the field as an extendable collection for the filler. The field type needs to implement `Default`, `Extend`, `IntoIterator`, and have an `is_empty` method. +- `#[filler(empty_value = ...)]`: define a value as empty, so the corresponding field of the filler will be applied even when the field is not `Option` or `extendable`. +- `#[filler(addable)]`: allow conflicting fillers to add/extend their values together with the `+` operator instead of panicking. Requires the `op` feature. - `#[complex(attribute(...))]`: add attributes to the field in the generated complex struct. (catalyst feature) -Please check the [traits][doc-traits] of document to learn more. - -The [examples][examples] demo following scenarios. -- diff two instances for a patch -- create a patch from json string -- rename the patch structure -- check a patch is empty or not -- add attribute to patch struct -- show option field behavior -- show operators about patches -- show example with serde crates, ex: `humantime_serde` for duration -- show a patch nesting other patch -- show filler with all possible types +Please check the [traits documentation][doc-traits] to learn more. + +## Examples + +The [examples][examples] demonstrate the following scenarios: +- diff two instances for a patch (`diff.rs`) +- create a patch from a JSON string (`json.rs`) +- rename the patch structure (`rename-patch-struct.rs`) +- check whether a patch is empty (`status.rs`) +- add attributes to a patch struct (`patch-attr.rs`) +- show option field behavior (`option.rs`) +- show operators on patches (`op.rs`) +- show example with serde crates, e.g. `humantime_serde` for durations (`time.rs`) +- show a patch nesting another patch (`nesting.rs`) +- show filler with all possible types (`filler.rs`) +- show operators on fillers (`filler-op.rs`) +- show `skip_wrap` field behavior (`instance.rs`) +- use `Patch` with `clap` for command-line config (`clap.rs`) ## Features -This crate also includes the following optional features: -- `status`(default): implements the `Status` trait for the patch struct, which provides the `is_empty` method. -- `op` (default): provide operators `<<` between instance and patch/filler, and `+` for patches/fillers - - default: when there is a field conflict between patches/fillers, `+` will add together if the `#[patch(addable)]`, `#[patch(add=fn)]` or `#[filler(addable)]` is provided, else it will panic. -- `merge` (optional): implements the `Merge` trait for the patch struct, which provides the `merge` method, and `<<` (if `op` feature enabled) between patches. -- `std`(optional): - - `box`: implements the `Patch>` trait for `T` where `T` implements `Patch

`. - This let you patch a boxed (or not) struct with a boxed patch. - - `option`: implements the `Patch>` trait for `Option` where `T` implements `Patch

`, please take a look at the example to learn more. - - default: `T` needs to implement `From

`. When patching on None, it will based on `from

` to cast T, and this let you patch structs containing fields with optional values. - - `none_as_default`: `T` needs to implement `Default`. When patching on None, it will patch on a default instance, and this also let you patch structs containing fields with optional values. - - `keep_none`: When patching on None, it is still None. -- `nesting`(optional): allow a inner field with `Patch` derive and `#[patch(nesting)]` attribute + +This crate includes the following optional features: +- `status` *(default)*: implements the `Status` trait for the patch struct, which provides the `is_empty` method. +- `op` *(default)*: provides the `<<` operator between an instance and a patch/filler, and the `+` operator for patches/fillers. + - By default, when there is a field conflict between patches/fillers, `+` will add them together if `#[patch(addable)]`, `#[patch(add = fn)]`, or `#[filler(addable)]` is provided; otherwise it will panic. +- `merge` *(optional)*: implements the `Merge` trait for the patch struct, which provides the `merge` method, and `<<` (if `op` is enabled) between patches. +- `alloc` *(optional)*: enables `alloc` support for `no_std` + alloc environments. +- `std` *(optional)*: enables `std`-dependent features (implies `box` and `option`). +- `box` *(optional)*: implements the `Patch>` trait for `T` where `T` implements `Patch

`. + This lets you patch a boxed (or unboxed) struct with a boxed patch. +- `option` *(optional)*: implements the `Patch>` trait for `Option` where `T` implements `Patch

`. Please take a look at the example to learn more. + - default behavior: `T` needs to implement `From

`. When patching on `None`, it converts the patch into `T` via `From

`, letting you patch structs containing fields with optional values. + - `none_as_default` *(optional)*: `T` needs to implement `Default`. When patching on `None`, it patches on a default instance. Mutually exclusive with `keep_none`. + - `keep_none` *(optional)*: when patching on `None`, it stays `None`. Mutually exclusive with `none_as_default`. +- `nesting` *(optional)*: allows a field to use `Patch` derive with the `#[patch(nesting)]` attribute. +- `catalyst` *(optional)*: enables the `Substrate`, `Catalyst`, and `Complex` derive macros for extending a struct with fields from another crate. +- `unsafe` *(optional)*: uses `ManuallyDrop` + `ptr::read` / `MaybeUninit` + `ptr::write` in the generated `bind`, `decouple`, `__substrate_new`, and `__substrate_unpack` to avoid memory moves. Only meaningful with the `catalyst` feature. [crates-badge]: https://img.shields.io/crates/v/struct-patch.svg [crate-url]: https://crates.io/crates/struct-patch [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg -[mit-url]: https://github.com/yanganto/struct-patch/blob/readme/LICENSE +[mit-url]: https://github.com/yanganto/struct-patch/blob/main/LICENSE [doc-badge]: https://img.shields.io/badge/docs-rs-orange.svg [doc-url]: https://docs.rs/struct-patch/ [doc-traits]: https://docs.rs/struct-patch/latest/struct_patch/traits/trait.Patch.html#container-attributes diff --git a/complex-example/Cargo.toml b/complex-example/Cargo.toml index b038366..7fbdfcb 100644 --- a/complex-example/Cargo.toml +++ b/complex-example/Cargo.toml @@ -9,7 +9,7 @@ struct-patch = { path = "../lib", features = ["catalyst"] } [workspace.package] authors = ["Antonio Yang "] -version = "0.12.6" +version = "0.12.7" edition = "2021" categories = ["development-tools"] keywords = ["struct", "patch", "macro", "derive", "overlay"] diff --git a/complex-example/catalyst/Cargo.toml b/complex-example/catalyst/Cargo.toml index f36d943..06c40f2 100644 --- a/complex-example/catalyst/Cargo.toml +++ b/complex-example/catalyst/Cargo.toml @@ -17,6 +17,9 @@ substrate = { path = "../substrate" } serde = { version = "1", features = ["derive"] } toml = "1.1.2+spec-1.1.0" +[features] +unsafe = ["struct-patch/unsafe"] + [build-dependencies] struct-patch.workspace = true substrate = { path = "../substrate" } diff --git a/derive/Cargo.toml b/derive/Cargo.toml index ef1e6db..8a79f0d 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -26,6 +26,7 @@ op = [] merge = [] nesting = [] catalyst = [ "syn-serde" ] +unsafe = [] [dev-dependencies] pretty_assertions_sorted = "1.2.3" diff --git a/derive/src/catalyst.rs b/derive/src/catalyst.rs index 43229e8..87cfc3f 100644 --- a/derive/src/catalyst.rs +++ b/derive/src/catalyst.rs @@ -76,16 +76,40 @@ impl Catalyst { catalyst_fields.push(Field::from_cat_ast(field.clone())); } + #[cfg(feature = "unsafe")] + let complex_ident = Ident::new("complex", Span::call_site()); + #[cfg(feature = "unsafe")] + let catalyst_ident = Ident::new("catalyst", Span::call_site()); + + #[cfg(feature = "unsafe")] + let catalyst_ptr_read_fields = catalyst_fields + .iter() + .map(|f| f.to_ptr_read_stream(&complex_ident)) + .collect::>>()?; + + #[cfg(feature = "unsafe")] + let substrate_ptr_read_fields = substrate_fields + .iter() + .map(|f| f.to_ptr_read_arg_stream(&complex_ident)) + .collect::>>()?; + + #[cfg(feature = "unsafe")] + let catalyst_bind_ptr_read_fields = catalyst_fields + .iter() + .map(|f| f.to_ptr_read_stream(&catalyst_ident)) + .collect::>>()?; + let complex_fields = raw_complex_fields .iter() .map(|f| f.to_token_stream(*keep_field_attribute, override_field_attributes, exclude_field_attributes)) .collect::>>()?; + #[cfg(not(feature = "unsafe"))] let unpack_complex_fields = raw_complex_fields .iter() .map(|f| f.to_unpack_stream()) .collect::>>()?; - + #[cfg(not(feature = "unsafe"))] let catalyst_fields = catalyst_fields .iter() .map(|f| f.to_unpack_stream()) @@ -104,44 +128,93 @@ impl Catalyst { } }) .collect::>(); - let catalyst_impl = quote! { - #[automatically_derived] - impl struct_patch::traits::Catalyst < #substrate_name, #complex_struct_name > for #struct_name { - fn bind(self, s: #substrate_name) -> #complex_struct_name { - let ( - #(#substrate_fields)* - ) = s.__substrate_unpack(); - let #struct_name { - #(#catalyst_fields)* - } = self; - #complex_struct_name { - #(#unpack_complex_fields)* + + let catalyst_impl = { + #[cfg(feature = "unsafe")] + quote! { + #[automatically_derived] + impl struct_patch::traits::Catalyst < #substrate_name, #complex_struct_name > for #struct_name { + fn bind(self, s: #substrate_name) -> #complex_struct_name { + let ( + #(#substrate_fields)* + ) = s.__substrate_unpack(); + let catalyst = std::mem::ManuallyDrop::new(self); + unsafe { + #complex_struct_name { + #(#substrate_fields)* + #(#catalyst_bind_ptr_read_fields)* + } + } + } + } + } + #[cfg(not(feature = "unsafe"))] + quote! { + #[automatically_derived] + impl struct_patch::traits::Catalyst < #substrate_name, #complex_struct_name > for #struct_name { + fn bind(self, s: #substrate_name) -> #complex_struct_name { + let ( + #(#substrate_fields)* + ) = s.__substrate_unpack(); + let #struct_name { + #(#catalyst_fields)* + } = self; + #complex_struct_name { + #(#unpack_complex_fields)* + } } } } }; + + let decouple_impl = { + #[cfg(feature = "unsafe")] + quote! { + #[automatically_derived] + impl struct_patch::traits::Complex < #struct_name, #substrate_name > for #complex_struct_name { + fn decouple(self) -> (#struct_name, #substrate_name) { + let complex = std::mem::ManuallyDrop::new(self); + unsafe { + ( + #struct_name { + #(#catalyst_ptr_read_fields)* + }, + #substrate_name::__substrate_new( + #(#substrate_ptr_read_fields)* + ) + ) + } + } + } + } + #[cfg(not(feature = "unsafe"))] + quote! { + #[automatically_derived] + impl struct_patch::traits::Complex < #struct_name, #substrate_name > for #complex_struct_name { + fn decouple(self) -> (#struct_name, #substrate_name) { + let #complex_struct_name { + #(#unpack_complex_fields)* + } = self; + ( + #struct_name { + #(#catalyst_fields)* + }, + #substrate_name::__substrate_new( + #(#substrate_fields)* + ) + ) + } + } + } + }; + let complex_impl = quote! { #(#mapped_attributes)* #visibility struct #complex_struct_name #generics { #(#complex_fields)* } - #[automatically_derived] - impl struct_patch::traits::Complex < #struct_name, #substrate_name > for #complex_struct_name { - fn decouple(self) -> (#struct_name, #substrate_name) { - let #complex_struct_name { - #(#unpack_complex_fields)* - } = self; - ( - #struct_name { - #(#catalyst_fields)* - }, - #substrate_name::__substrate_new( - #(#substrate_fields)* - ) - ) - } - } + #decouple_impl }; Ok(quote! { @@ -348,6 +421,34 @@ impl Field { }) } + /// Generate the token stream for reading a field via ptr::read for struct construction + #[cfg(feature = "unsafe")] + pub fn to_ptr_read_stream(&self, source: &Ident) -> Result { + let Field { + ident, + ty: _, + attributes: _, + attrs: _, + } = self; + Ok(quote! { + #ident: std::ptr::read(&#source.#ident), + }) + } + + /// Generate the token stream for reading a field via ptr::read as a function argument + #[cfg(feature = "unsafe")] + pub fn to_ptr_read_arg_stream(&self, source: &Ident) -> Result { + let Field { + ident, + ty: _, + attributes: _, + attrs: _, + } = self; + Ok(quote! { + std::ptr::read(&#source.#ident), + }) + } + /// Parse the Catalyst struct field pub fn from_cat_ast( syn::Field { diff --git a/derive/src/substrate.rs b/derive/src/substrate.rs index 42efc9c..dd53091 100644 --- a/derive/src/substrate.rs +++ b/derive/src/substrate.rs @@ -25,6 +25,76 @@ impl Substrate { let active_site = json::to_string(fields); + let substrate_impl_inner = { + #[cfg(feature = "unsafe")] + { + quote! { + #[automatically_derived] + impl #struct_name { + #[allow(clippy::too_many_arguments)] + /// Support `decouple` of Complex with private fields, a new function generated by Substrate macro + pub fn __substrate_new( + #( #field_idents: #field_tys, )* + ) -> #struct_name { + unsafe { + use std::mem::MaybeUninit; + let mut substrate = MaybeUninit::<#struct_name>::uninit(); + #( std::ptr::write( + std::ptr::addr_of_mut!((*substrate.as_mut_ptr()).#field_idents), + #field_idents, + ); )* + substrate.assume_init() + } + } + + #[allow(clippy::too_many_arguments)] + /// Support `bind` of Catalyst with private fields, an unpack function generated by Substrate macro + pub fn __substrate_unpack( + self + ) -> ( + #( #field_tys, )* + ) { + let substrate = std::mem::ManuallyDrop::new(self); + unsafe { + ( + #( std::ptr::read(&substrate.#field_idents), )* + ) + } + } + } + } + } + #[cfg(not(feature = "unsafe"))] + { + quote! { + #[automatically_derived] + impl #struct_name { + #[allow(clippy::too_many_arguments)] + /// Support `decouple` of Complex with private fields, a new function generated by Substrate macro + pub fn __substrate_new( + #( #field_idents: #field_tys, )* + ) -> #struct_name { + #struct_name { + #( #field_idents, )* + } + } + + #[allow(clippy::too_many_arguments)] + /// Support `bind` of Catalyst with private fields, an unpack function generated by Substrate macro + pub fn __substrate_unpack( + self + ) -> ( + #( #field_tys, )* + ) { + ( + #( self.#field_idents, )* + ) + } + } + } + } + }; + Ok(quote! { #[automatically_derived] impl struct_patch::traits::Substrate for #struct_name { @@ -36,30 +106,7 @@ impl Substrate { } } - #[automatically_derived] - impl #struct_name { - #[allow(clippy::too_many_arguments)] - /// Support `decouple` of Complex with private fields, a new function generated by Substrate macro - pub fn __substrate_new( - #( #field_idents: #field_tys, )* - ) -> #struct_name { - #struct_name { - #( #field_idents, )* - } - } - - #[allow(clippy::too_many_arguments)] - /// Support `bind` of Catalyst with private fields, an unpack function generated by Substrate macro - pub fn __substrate_unpack( - self - ) -> ( - #( #field_tys, )* - ) { - ( - #( self.#field_idents, )* - ) - } - } + #substrate_impl_inner }) } /// Parse the substrate struct diff --git a/flake.nix b/flake.nix index 390d5d2..3f1a745 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,9 @@ cd complex-example cargo test -p substrate cargo test -p catalyst + + echo "Run catatyst test with unsafe features" + cargo test -p catalyst --features unsafe ''; in with pkgs; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 0564e25..dfecae2 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,7 +12,7 @@ readme.workspace = true rust-version.workspace = true [dependencies] -struct-patch-derive = { version = "=0.12.6", path = "../derive" } +struct-patch-derive = { version = "=0.12.7", path = "../derive" } [dev-dependencies] serde_json = "1.0" @@ -46,3 +46,6 @@ keep_none = ["option"] catalyst = [ "struct-patch-derive/catalyst" ] +unsafe = [ + "struct-patch-derive/unsafe" +] diff --git a/no-std-examples/Cargo.toml b/no-std-examples/Cargo.toml index 9c68913..89e4bb0 100644 --- a/no-std-examples/Cargo.toml +++ b/no-std-examples/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "no-std-examples" authors = ["Antonio Yang "] -version = "0.12.6" +version = "0.12.7" edition = "2021" license = "MIT"