Repository: dalek-cryptography/curve25519-dalek (language: rust)

Issue:
Failed backend override - Warning or Error
In implementing the backend overrides over automatic selection:
- #531 

I noticed that if someone tries to override with `--cfg curve25519_dalek_backend="simd"` where the simd override is not possible e.g. in a case of target being wasm32-unknown-unknown - we would get "duplicate" cfg() during the compile time if we try to re-set it as "serial" via build script as a fallback with a warning.

By leaving the failed override as a mere warning - for every simd related gating we would have to include `all(not(curve25519_dalek_backend = "serial"), not(curve25519_dalek_backend = "fiat")` in negative sense ruling out every other possibility in every gate for simd backend.

Since vast majority of the people should not be using this override to begin with and this case should be considered a compile error I've left it to trigger a failure at build time where override fails - which also gets tested via the build test.

By leaving it as a compile error we can have more simplified positive gating - e.g. in simd case it would be mere `cfg(curve25519_dalek_backend = "simd")` instead of a litany of `all(not(every_other))` everywhere - manifest still has to have it that way since it gets evaluated before build.rs.

This also helps ensure somebody doesn't try to supply wrong overrides expecting the override to work in vast minority cases they would be using this.

This also should be forwards compatible given multiple `cfg(param=)` is not really well defined behaviour.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 45aa87b0f..77d38581c 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -11,7 +11,7 @@ env:
   RUSTFLAGS: '-D warnings'
 
 jobs:
-  test:
+  test-auto:
     runs-on: ubuntu-latest
     strategy:
       matrix:
@@ -38,10 +38,58 @@ jobs:
       - run: cargo test --target ${{ matrix.target }} --features digest
       - run: cargo test --target ${{ matrix.target }} --features rand_core
       - run: cargo test --target ${{ matrix.target }} --features serde
+
+  test-fiat:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          # 32-bit target
+          - target: i686-unknown-linux-gnu
+            deps: sudo apt update && sudo apt install gcc-multilib
+
+          # 64-bit target
+          - target: x86_64-unknown-linux-gnu
+    steps:
+      - uses: actions/checkout@v3
+      - uses: dtolnay/rust-toolchain@stable
+      - run: rustup target add ${{ matrix.target }}
+      - run: ${{ matrix.deps }}
       - env:
           RUSTFLAGS: '--cfg curve25519_dalek_backend="fiat"'
         run: cargo test --target ${{ matrix.target }}
 
+  test-serial:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          # 32-bit target
+          - target: i686-unknown-linux-gnu
+            deps: sudo apt update && sudo apt install gcc-multilib
+
+          # 64-bit target
+          - target: x86_64-unknown-linux-gnu
+    steps:
+      - uses: actions/checkout@v3
+      - uses: dtolnay/rust-toolchain@stable
+      - run: rustup target add ${{ matrix.target }}
+      - run: ${{ matrix.deps }}
+      - env:
+          RUSTFLAGS: '--cfg curve25519_dalek_backend="serial"'
+        run: cargo test --target ${{ matrix.target }}
+
+  build-script:
+    name: Test Build Script
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: dtolnay/rust-toolchain@master
+        with:
+          toolchain: stable
+          targets: wasm32-unknown-unknown,x86_64-unknown-linux-gnu,i686-unknown-linux-gnu
+      - run: bash tests/build_tests.sh
+
   build-nostd:
     name: Build on no_std target (thumbv7em-none-eabi)
     runs-on: ubuntu-latest
@@ -55,8 +103,8 @@ jobs:
       - run: cargo build --target thumbv7em-none-eabi --release
       - run: cargo build --target thumbv7em-none-eabi --release --features serde
 
-  test-simd-native:
-    name: Test simd backend (native)
+  test-simd-nightly:
+    name: Test simd backend (nightly)
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v3
@@ -66,11 +114,12 @@ jobs:
         #   1) build all of the x86_64 SIMD code,
         #   2) run all of the SIMD-specific tests that the test runner supports,
         #   3) run all of the normal tests using the best available SIMD backend.
+        # This should automatically pick up the simd backend in a x84_64 runner
         RUSTFLAGS: '-C target_cpu=native'
-      run: cargo test --features simd --target x86_64-unknown-linux-gnu
+      run: cargo test --target x86_64-unknown-linux-gnu
 
-  test-simd-avx2:
-    name: Test simd backend (avx2)
+  test-simd-stable:
+    name: Test simd backend (stable)
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v3
@@ -78,8 +127,10 @@ jobs:
     - env:
         # This will run AVX2-specific tests and run all of the normal tests
         # with the AVX2 backend, even if the runner supports AVX512.
+        # This should automatically pick up the simd backend in a x86_64 runner
+        # It should pick AVX2 due to stable toolchain used since AVX512 requires nigthly
         RUSTFLAGS: '-C target_feature=+avx2'
-      run: cargo test --no-default-features --features alloc,precomputed-tables,zeroize,simd_avx2 --target x86_64-unknown-linux-gnu
+      run: cargo test --no-default-features --features alloc,precomputed-tables,zeroize --target x86_64-unknown-linux-gnu
 
   build-docs:
     name: Build docs
diff --git a/.gitignore b/.gitignore
index 7acc1af24..6eea85527 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
+*/target/*
 target
 Cargo.lock
-
+*/Cargo.lock
+build*.txt
 *~
 \#*
 .\#*
diff --git a/Cargo.toml b/Cargo.toml
index 33bbb29a5..72952b9bd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,7 +53,6 @@ digest = { version = "0.10", default-features = false, optional = true }
 subtle = { version = "2.3.0", default-features = false }
 serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] }
 zeroize = { version = "1", default-features = false, optional = true }
-unsafe_target_feature = { version = "= 0.1.1", optional = true }
 
 [target.'cfg(target_arch = "x86_64")'.dependencies]
 cpufeatures = "0.2.6"
@@ -62,20 +61,13 @@ cpufeatures = "0.2.6"
 fiat-crypto = "0.1.19"
 
 [features]
-default = ["alloc", "precomputed-tables", "zeroize", "simd"]
+default = ["alloc", "precomputed-tables", "zeroize"]
 alloc = ["zeroize?/alloc"]
 precomputed-tables = []
 legacy_compatibility = []
 
-# Whether to allow the use of the AVX2 SIMD backend.
-simd_avx2 = ["unsafe_target_feature"]
-
-# Whether to allow the use of the AVX512 SIMD backend.
-# (Note: This requires Rust nightly; on Rust stable this feature will be ignored.)
-simd_avx512 = ["unsafe_target_feature"]
-
-# A meta-feature to allow all SIMD backends to be used.
-simd = ["simd_avx2", "simd_avx512"]
+[target.'cfg(all(not(curve25519_dalek_backend = "fiat"), not(curve25519_dalek_backend = "serial"), target_arch = "x86_64"))'.dependencies]
+curve25519-dalek-derive = { version = "0.1", path = "curve25519-dalek-derive" }
 
 [profile.dev]
 opt-level = 2
diff --git a/README.md b/README.md
index 12100691d..cd83e9253 100644
--- a/README.md
+++ b/README.md
@@ -53,9 +53,6 @@ curve25519-dalek = "4.0.0-rc.2"
 | `alloc`            |    ✓     | Enables Edwards and Ristretto multiscalar multiplication, batch scalar inversion, and batch Ristretto double-and-compress. Also enables `zeroize`. |
 | `zeroize`          |    ✓     | Enables [`Zeroize`][zeroize-trait] for all scalar and curve point types. |
 | `precomputed-tables` |    ✓     | Includes precomputed basepoint multiplication tables. This speeds up `EdwardsPoint::mul_base` and `RistrettoPoint::mul_base` by ~4x, at the cost of ~30KB added to the code size. |
-| `simd_avx2`        |    ✓     | Allows the AVX2 SIMD backend to be used, if available. |
-| `simd_avx512`      |    ✓     | Allows the AVX512 SIMD backend to be used, if available. |
-| `simd`             |    ✓     | Allows every SIMD backend to be used, if available. |
 | `rand_core`        |          | Enables `Scalar::random` and `RistrettoPoint::random`. This is an optional dependency whose version is not subject to SemVer. See [below](#public-api-semver-exemptions) for more details. |
 | `digest`           |          | Enables `RistrettoPoint::{from_hash, hash_from_bytes}` and `Scalar::{from_hash, hash_from_bytes}`. This is an optional dependency whose version is not subject to SemVer. See [below](#public-api-semver-exemptions) for more details. |
 | `serde`            |          | Enables `serde` serialization/deserialization for all the point and scalar types. |
@@ -90,25 +87,27 @@ latest breaking changes in high level are below:
 
 This release also does a lot of dependency updates and relaxations to unblock upstream build issues.
 
-### 4.0.0 - Open Breaking Changes
+# Backends
 
-See tracking issue: [curve25519-dalek/issues/521](https://github.com/dalek-cryptography/curve25519-dalek/issues/521)
+Curve arithmetic is implemented and used by one of the following backends:
 
-# Backends
+| Backend  | Selection | Implementation                                                | Bits / Word sizes |
+| :---     | :---      | :---                                                          | :---              |
+| `serial` | Automatic | An optimized, non-parllel implementation                      | `32` and `64`     |
+| `fiat`   | Manual    | Formally verified field arithmetic from [fiat-crypto]         | `32` and `64`     |
+| `simd`   | Automatic | Intel AVX2 / AVX512 IFMA accelerated backend                  | `64` only         |
 
-Curve arithmetic is implemented and used by selecting one of the following backends:
+At runtime, `curve25519-dalek` selects an arithmetic backend from the set of backends it was compiled to support. For Intel x86-64 targets, unless otherwise specified, it will build itself with `simd` support, and default to `serial` at runtime if the appropriate CPU features aren't detected. See [SIMD backend] for more details.
 
-| Backend            | Implementation                                                | Target backends             |
-| :---               | :---                                                          | :---                        |
-| `[default]`        | Automatic runtime backend selection (either serial or SIMD)   | `u32` <br/> `u64` <br/> `avx2` <br/> `avx512` |
-| `fiat`             | Formally verified field arithmetic from [fiat-crypto]         | `fiat_u32` <br/> `fiat_u64` |
+In the future, `simd` backend may be extended to cover more instruction sets. This change will be non-breaking as this is considered as implementation detail.
 
-To choose a backend other than the `[default]` backend, set the
-environment variable:
+## Manual Backend Override
+
+You can force the crate to compile with specific backend support, e.g., `serial` for x86-64 targets to save code size, or `fiat` to force the runtime to use verified code. To do this, set the environment variable:
 ```sh
 RUSTFLAGS='--cfg curve25519_dalek_backend="BACKEND"'
 ```
-where `BACKEND` is `fiat`. Equivalently, you can write to
+Equivalently, you can write to
 `~/.cargo/config`:
 ```toml
 [build]
@@ -117,53 +116,49 @@ rustflags = ['--cfg=curve25519_dalek_backend="BACKEND"']
 More info [here](https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags).
 
 Note for contributors: The target backends are not entirely independent of each
-other. The SIMD backend directly depends on parts of the the `u64` backend to
+other. The [SIMD backend] directly depends on parts of the serial backend to
 function.
 
-## Word size for serial backends
+## Bits / Word size
 
-`curve25519-dalek` will automatically choose the word size for the `[default]`
-and `fiat` serial backends, based on the build target. For example, building
-for a 64-bit machine, the default `u64` target backend is automatically chosen
-when the `[default]` backend is selected, and `fiat_u64` is chosen when the
-`fiat backend is selected.
+`curve25519-dalek` will automatically choose the word size for the `fiat` and
+`serial` backends, based on the build target.
+For example, building for a 64-bit machine, the default 64 bit word size is
+automatically chosen when either the `serial` or `fiat` backend is selected.
 
-Backend word size can be overridden for `[default]` and `fiat` by setting the
+In some targets it might be required to override the word size for better
+performance.
+Backend word size can be overridden for `serial` and `fiat` by setting the
 environment variable:
 ```sh
 RUSTFLAGS='--cfg curve25519_dalek_bits="SIZE"'
 ```
-where `SIZE` is `32` or `64`. As in the above section, this can also be placed
+`SIZE` is `32` or `64`. As in the above section, this can also be placed
 in `~/.cargo/config`.
 
-**NOTE:** Using a word size of 32 will automatically disable SIMD support.
+Note: The [SIMD backend] requires a word size of 64 bits. Attempting to set bits=32 and backend=`simd` will yield a compile error.
 
 ### Cross-compilation
 
-Because backend selection is done by target, cross-compiling will select the
-correct word size automatically. For example, on an x86-64 Linux machine,
-`curve25519-dalek` will use the `u32` target backend if the following is run:
+Because backend selection is done by target, cross-compiling will select the correct word size automatically. For example, if a x86-64 Linux machine runs the following commands, `curve25519-dalek` will be compiled with the 32-bit `serial` backend.
 ```console
 $ sudo apt install gcc-multilib # (or whatever package manager you use)
 $ rustup target add i686-unknown-linux-gnu
 $ cargo build --target i686-unknown-linux-gnu
 ```
 
-## SIMD target backends
+## SIMD backend
 
-The SIMD target backend selection is done automatically at runtime depending
-on the available CPU features, provided the appropriate feature flag is enabled.
+The specific SIMD backend (AVX512 / AVX2 / `serial` default) is selected automatically at runtime, depending on the currently available CPU features, and whether Rust nightly is being used for compilation. The precise conditions are specified below.
 
-You can also specify an appropriate `-C target_feature` to build a binary
-which assumes the required SIMD instructions are always available.
+For a given CPU feature, you can also specify an appropriate `-C target_feature` to build a binary which assumes the required SIMD instructions are always available. Don't do this if you don't have a good reason.
 
-| Backend | Feature flag  | `RUSTFLAGS`                               | Requires nightly? |
-| :---    | :---          | :---                                      | :---              |
-| avx2    | `simd_avx2`   | `-C target_feature=+avx2`                 | no                |
-| avx512  | `simd_avx512` | `-C target_feature=+avx512ifma,+avx512vl` | yes               |
+| Backend | `RUSTFLAGS`                               | Requires nightly? |
+| :---    | :---                                      | :---              |
+| avx2    | `-C target_feature=+avx2`                 | no                |
+| avx512  | `-C target_feature=+avx512ifma,+avx512vl` | yes               |
 
-The AVX512 backend requires Rust nightly. When compiled on a non-nightly
-compiler it will always be disabled.
+If compiled on a non-nightly compiler, `curve25519-dalek` will not include AVX512 code, and therefore will never select it at runtime.
 
 # Documentation
 
@@ -326,3 +321,4 @@ contributions.
 [semver]: https://semver.org/spec/v2.0.0.html
 [rngcorestd]: https://github.com/rust-random/rand/tree/7aa25d577e2df84a5156f824077bb7f6bdf28d97/rand_core#crate-features
 [zeroize-trait]: https://docs.rs/zeroize/latest/zeroize/trait.Zeroize.html
+[SIMD backend]: #simd-backend
diff --git a/build.rs b/build.rs
index 04f4d9ca3..92d2802cd 100644
--- a/build.rs
+++ b/build.rs
@@ -3,6 +3,7 @@
 #![deny(clippy::unwrap_used, dead_code)]
 
 #[allow(non_camel_case_types)]
+#[derive(PartialEq, Debug)]
 enum DalekBits {
     Dalek32,
     Dalek64,
@@ -34,6 +35,38 @@ fn main() {
         // so for those we want to apply the `#[allow(unused_unsafe)]` attribute to get rid of that warning.
         println!("cargo:rustc-cfg=allow_unused_unsafe");
     }
+
+    let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH") {
+        Ok(arch) => arch,
+        _ => "".to_string(),
+    };
+
+    // Backend overrides / defaults
+    let curve25519_dalek_backend =
+        match std::env::var("CARGO_CFG_CURVE25519_DALEK_BACKEND").as_deref() {
+            Ok("fiat") => "fiat",
+            Ok("serial") => "serial",
+            Ok("simd") => {
+                // simd can only be enabled on x86_64 & 64bit target_pointer_width
+                match is_capable_simd(&target_arch, curve25519_dalek_bits) {
+                    true => "simd",
+                    // If override is not possible this must result to compile error
+                    // See: issues/532
+                    false => panic!("Could not override curve25519_dalek_backend to simd"),
+                }
+            }
+            // default between serial / simd (if potentially capable)
+            _ => match is_capable_simd(&target_arch, curve25519_dalek_bits) {
+                true => "simd",
+                false => "serial",
+            },
+        };
+    println!("cargo:rustc-cfg=curve25519_dalek_backend=\"{curve25519_dalek_backend}\"");
+}
+
+// Is the target arch & curve25519_dalek_bits potentially simd capable ?
+fn is_capable_simd(arch: &str, bits: DalekBits) -> bool {
+    arch == "x86_64" && bits == DalekBits::Dalek64
 }
 
 // Deterministic cfg(curve25519_dalek_bits) when this is not explicitly set.
diff --git a/curve25519-dalek-derive/Cargo.toml b/curve25519-dalek-derive/Cargo.toml
new file mode 100644
index 000000000..11c875ebf
--- /dev/null
+++ b/curve25519-dalek-derive/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "curve25519-dalek-derive"
+version = "0.1.0"
+edition = "2021"
+
+repository = "https://github.com/dalek-cryptography/curve25519-dalek"
+homepage = "https://github.com/dalek-cryptography/curve25519-dalek"
+documentation = "https://docs.rs/curve25519-dalek-derive"
+license = "MIT/Apache-2.0"
+readme = "README.md"
+description = "curve25519-dalek Derives"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1.0.53"
+quote = "1.0.26"
+syn = { version = "2.0.8", features = ["full"] }
diff --git a/curve25519-dalek-derive/README.md b/curve25519-dalek-derive/README.md
new file mode 100644
index 000000000..7f52d440d
--- /dev/null
+++ b/curve25519-dalek-derive/README.md
@@ -0,0 +1,191 @@
+# A more convenient `#[target_feature]` replacement
+
+To get good performance out of SIMD everything on the SIMD codepath must be inlined.
+With how SIMD is currently implemented in Rust one of two things have to be true for
+a function using SIMD to be inlinable: (and this includes the SIMD intrinsics themselves)
+
+   a) The whole program has to be compiled with the relevant `-C target-cpu` or `-C target-feature` flags.
+
+   b) SIMD support must be automatically detected at runtime, and every function on the SIMD codepath must be marked with `#[target_feature]`.
+
+Both have their downsides. Setting the `target-cpu` or `target-features` makes the resulting binary
+incompatible with older CPUs, while using `#[target_feature]` is incredibly inconvenient.
+
+This crate is meant to make `#[target_feature]` less painful to use.
+
+## Problems with `#[target_feature]`
+
+When we're not compiling with the relevant `target-cpu`/`target-feature` flags everything on
+the SIMD codepath must be marked with the `#[target_feature]` attribute. This is not a problem
+when all of your SIMD code is neatly encapsulated inside of a single function, but once you start
+to build out more elaborate abstractions it starts to become painful to use.
+
+  * It can only be used on `unsafe` functions, so everything on your SIMD codepath now has to be `unsafe`.
+
+    In theory this is nice - these functions require the relevant SIMD instructions to be present at runtime,
+    so calling them without checking is obviously unsafe! But in practice this is rarely what you want. When
+    you build an abstraction over SIMD code you usually want to assume that *internally* within your module
+    all of the necessary SIMD instructions are available, and you only want to check this at the boundaries
+    when you're first entering your module. You do *not* want to infect everything *inside* of the module with
+    `unsafe` since you've already checked this invariant at the module's API boundary.
+
+  * It cannot be used on non-`unsafe` trait methods.
+
+    If you're implementing a trait, say for example `std::ops::Add`, then you cannot mark the method `unsafe`
+    unless the original trait also has it marked as `unsafe`, and usually it doesn't.
+
+  * It makes it impossible to abstract over a given SIMD instruction set using a trait.
+
+    For example, let's assume you want to abstract over which SIMD instructions you use using a trait in the following way:
+
+    ```rust
+    trait Backend {
+        unsafe fn sum(input: &[u32]) -> u32;
+    }
+
+    struct AVX;
+    impl Backend for AVX {
+        #[target_feature(enable = "avx")]
+        unsafe fn sum(xs: &[u32]) -> u32 {
+            // ...
+            todo!();
+        }
+    }
+
+    struct AVX2;
+    impl Backend for AVX2 {
+        #[target_feature(enable = "avx2")]
+        unsafe fn sum(xs: &[u32]) -> u32 {
+            // ...
+            todo!();
+        }
+    }
+
+    // And now you want a have function which calls into that trait:
+    unsafe fn do_calculations<B>(xs: &[u32]) -> u32 where B: Backend {
+        let value = B::sum(xs);
+        // ...do some more calculations here...
+        value
+    }
+    ```
+
+    We have a problem here. This has to be marked with `#[target_feature]`, and that has to specify the concrete
+    feature flag for a given SIMD instruction set, but this function is generic so we can't do that!
+
+## How does this crate make it better?
+
+### You can now mark safe functions with `#[target_feature]`
+
+This crate exposes an `#[unsafe_target_feature]` macro which works just like `#[target_feature]` except
+it moves the `unsafe` from the function prototype into the macro name, and can be used on safe functions.
+
+```rust,compile_fail
+// ERROR: `#[target_feature(..)]` can only be applied to `unsafe` functions
+#[target_feature(enable = "avx2")]
+fn func() {}
+```
+
+```rust
+// It works, but must be `unsafe`
+#[target_feature(enable = "avx2")]
+unsafe fn func() {}
+```
+
+```rust
+use curve25519_dalek_derive::unsafe_target_feature;
+
+// No `unsafe` on the function itself!
+#[unsafe_target_feature("avx2")]
+fn func() {}
+```
+
+It can also be used to mark functions inside of impls:
+
+```rust,compile_fail
+struct S;
+
+impl core::ops::Add for S {
+    type Output = S;
+    // ERROR: method `add` has an incompatible type for trait
+    #[target_feature(enable = "avx2")]
+    unsafe fn add(self, rhs: S) -> S {
+        S
+    }
+}
+```
+
+```rust
+use curve25519_dalek_derive::unsafe_target_feature;
+
+struct S;
+
+#[unsafe_target_feature("avx2")]
+impl core::ops::Add for S {
+    type Output = S;
+    // No `unsafe` on the function itself!
+    fn add(self, rhs: S) -> S {
+        S
+    }
+}
+
+```
+
+### You can generate specialized copies of a module for each target feature
+
+```rust
+use curve25519_dalek_derive::unsafe_target_feature_specialize;
+
+#[unsafe_target_feature_specialize("sse2", "avx2", conditional("avx512ifma", nightly))]
+mod simd {
+    #[for_target_feature("sse2")]
+    pub const CONSTANT: u32 = 1;
+
+    #[for_target_feature("avx2")]
+    pub const CONSTANT: u32 = 2;
+
+    #[for_target_feature("avx512ifma")]
+    pub const CONSTANT: u32 = 3;
+
+    pub fn func() { /* ... */ }
+}
+
+fn entry_point() {
+    #[cfg(nightly)]
+    if std::is_x86_feature_detected!("avx512ifma") {
+        return simd_avx512ifma::func();
+    }
+
+    if std::is_x86_feature_detected!("avx2") {
+        return simd_avx2::func();
+    }
+
+    if std::is_x86_feature_detected!("sse2") {
+        return simd_sse2::func();
+    }
+
+    unimplemented!();
+}
+```
+
+## How to use `#[unsafe_target_feature]`?
+
+  - Can be used on `fn`s, `impl`s and `mod`s.
+  - When used on a function will only apply to that function; it won't apply to any nested functions, traits, mods, etc.
+  - When used on an `impl` will only apply to all of the functions directly defined inside of that `impl`.
+  - When used on a `mod` will only apply to all of the `fn`s and `impl`s directly defined inside of that `mod`.
+  - Cannot be used on methods which use `self` or `Self`; instead use it on the `impl` in which the method is defined.
+
+## License
+
+Licensed under either of
+
+  * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
+  * MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
+
+at your option.
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
+dual licensed as above, without any additional terms or conditions.
diff --git a/curve25519-dalek-derive/src/lib.rs b/curve25519-dalek-derive/src/lib.rs
new file mode 100644
index 000000000..53877493e
--- /dev/null
+++ b/curve25519-dalek-derive/src/lib.rs
@@ -0,0 +1,466 @@
+#![doc = include_str!("../README.md")]
+
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use syn::spanned::Spanned;
+
+macro_rules! unsupported_if_some {
+    ($value:expr) => {
+        if let Some(value) = $value {
+            return syn::Error::new(value.span(), "unsupported by #[unsafe_target_feature(...)]")
+                .into_compile_error()
+                .into();
+        }
+    };
+}
+
+macro_rules! unsupported {
+    ($value: expr) => {
+        return syn::Error::new(
+            $value.span(),
+            "unsupported by #[unsafe_target_feature(...)]",
+        )
+        .into_compile_error()
+        .into()
+    };
+}
+
+mod kw {
+    syn::custom_keyword!(conditional);
+}
+
+enum SpecializeArg {
+    LitStr(syn::LitStr),
+    Conditional(Conditional),
+}
+
+impl SpecializeArg {
+    fn lit(&self) -> &syn::LitStr {
+        match self {
+            SpecializeArg::LitStr(lit) => lit,
+            SpecializeArg::Conditional(conditional) => &conditional.lit,
+        }
+    }
+
+    fn condition(&self) -> Option<&TokenStream2> {
+        match self {
+            SpecializeArg::LitStr(..) => None,
+            SpecializeArg::Conditional(conditional) => Some(&conditional.attr),
+        }
+    }
+}
+
+struct Conditional {
+    lit: syn::LitStr,
+    attr: TokenStream2,
+}
+
+impl syn::parse::Parse for Conditional {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let lit = input.parse()?;
+        input.parse::<syn::Token![,]>()?;
+        let attr = input.parse()?;
+
+        Ok(Conditional { lit, attr })
+    }
+}
+
+impl syn::parse::Parse for SpecializeArg {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let lookahead = input.lookahead1();
+        if lookahead.peek(kw::conditional) {
+            input.parse::<kw::conditional>()?;
+
+            let content;
+            syn::parenthesized!(content in input);
+
+            let conditional = content.parse()?;
+            Ok(SpecializeArg::Conditional(conditional))
+        } else {
+            Ok(SpecializeArg::LitStr(input.parse()?))
+        }
+    }
+}
+
+struct SpecializeArgs(syn::punctuated::Punctuated<SpecializeArg, syn::Token![,]>);
+
+impl syn::parse::Parse for SpecializeArgs {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(Self(syn::punctuated::Punctuated::parse_terminated(input)?))
+    }
+}
+
+#[proc_macro_attribute]
+pub fn unsafe_target_feature(attributes: TokenStream, input: TokenStream) -> TokenStream {
+    let attributes = syn::parse_macro_input!(attributes as syn::LitStr);
+    let item = syn::parse_macro_input!(input as syn::Item);
+    process_item(&attributes, item, true)
+}
+
+#[proc_macro_attribute]
+pub fn unsafe_target_feature_specialize(
+    attributes: TokenStream,
+    input: TokenStream,
+) -> TokenStream {
+    let attributes = syn::parse_macro_input!(attributes as SpecializeArgs);
+    let item_mod = syn::parse_macro_input!(input as syn::ItemMod);
+
+    let mut out = Vec::new();
+    for attributes in attributes.0 {
+        let features: Vec<_> = attributes
+            .lit()
+            .value()
+            .split(",")
+            .map(|feature| feature.replace(" ", ""))
+            .collect();
+        let name = format!("{}_{}", item_mod.ident, features.join("_"));
+        let ident = syn::Ident::new(&name, item_mod.ident.span());
+        let mut attrs = item_mod.attrs.clone();
+        if let Some(condition) = attributes.condition() {
+            attrs.push(syn::Attribute {
+                pound_token: Default::default(),
+                style: syn::AttrStyle::Outer,
+                bracket_token: Default::default(),
+                meta: syn::Meta::List(syn::MetaList {
+                    path: syn::Ident::new("cfg", attributes.lit().span()).into(),
+                    delimiter: syn::MacroDelimiter::Paren(Default::default()),
+                    tokens: condition.clone(),
+                }),
+            });
+        }
+
+        let item_mod = process_mod(
+            attributes.lit(),
+            syn::ItemMod {
+                attrs,
+                ident,
+                ..item_mod.clone()
+            },
+            Some(features),
+        );
+
+        out.push(item_mod);
+    }
+
+    quote::quote! {
+        #(#out)*
+    }
+    .into()
+}
+
+fn process_item(attributes: &syn::LitStr, item: syn::Item, strict: bool) -> TokenStream {
+    match item {
+        syn::Item::Fn(function) => process_function(attributes, function, None),
+        syn::Item::Impl(item_impl) => process_impl(attributes, item_impl),
+        syn::Item::Mod(item_mod) => process_mod(attributes, item_mod, None).into(),
+        item => {
+            if strict {
+                unsupported!(item)
+            } else {
+                quote::quote! { #item }.into()
+            }
+        }
+    }
+}
+
+fn process_mod(
+    attributes: &syn::LitStr,
+    mut item_mod: syn::ItemMod,
+    spec_features: Option<Vec<String>>,
+) -> TokenStream2 {
+    if let Some((_, ref mut content)) = item_mod.content {
+        'next_item: for item in content {
+            if let Some(ref spec_features) = spec_features {
+                match item {
+                    syn::Item::Const(syn::ItemConst { ref mut attrs, .. })
+                    | syn::Item::Enum(syn::ItemEnum { ref mut attrs, .. })
+                    | syn::Item::ExternCrate(syn::ItemExternCrate { ref mut attrs, .. })
+                    | syn::Item::Fn(syn::ItemFn { ref mut attrs, .. })
+                    | syn::Item::ForeignMod(syn::ItemForeignMod { ref mut attrs, .. })
+                    | syn::Item::Impl(syn::ItemImpl { ref mut attrs, .. })
+                    | syn::Item::Macro(syn::ItemMacro { ref mut attrs, .. })
+                    | syn::Item::Mod(syn::ItemMod { ref mut attrs, .. })
+                    | syn::Item::Static(syn::ItemStatic { ref mut attrs, .. })
+                    | syn::Item::Struct(syn::ItemStruct { ref mut attrs, .. })
+                    | syn::Item::Trait(syn::ItemTrait { ref mut attrs, .. })
+                    | syn::Item::TraitAlias(syn::ItemTraitAlias { ref mut attrs, .. })
+                    | syn::Item::Type(syn::ItemType { ref mut attrs, .. })
+                    | syn::Item::Union(syn::ItemUnion { ref mut attrs, .. })
+                    | syn::Item::Use(syn::ItemUse { ref mut attrs, .. }) => {
+                        let mut index = 0;
+                        while index < attrs.len() {
+                            let attr = &attrs[index];
+                            if matches!(attr.style, syn::AttrStyle::Outer) {
+                                match attr.meta {
+                                    syn::Meta::List(ref list)
+                                        if is_path_eq(&list.path, "for_target_feature") =>
+                                    {
+                                        let feature: syn::LitStr = match list.parse_args() {
+                                            Ok(feature) => feature,
+                                            Err(error) => {
+                                                return error.into_compile_error();
+                                            }
+                                        };
+
+                                        let feature = feature.value();
+                                        if !spec_features
+                                            .iter()
+                                            .any(|enabled_feature| feature == *enabled_feature)
+                                        {
+                                            *item = syn::Item::Verbatim(Default::default());
+                                            continue 'next_item;
+                                        }
+
+                                        attrs.remove(index);
+                                        continue;
+                                    }
+                                    _ => {}
+                                }
+                            }
+
+                            index += 1;
+                            continue;
+                        }
+                    }
+                    _ => {
+                        unsupported!(item_mod);
+                    }
+                }
+            }
+
+            *item = syn::Item::Verbatim(
+                process_item(
+                    attributes,
+                    std::mem::replace(item, syn::Item::Verbatim(Default::default())),
+                    false,
+                )
+                .into(),
+            );
+        }
+    }
+
+    quote::quote! {
+        #item_mod
+    }
+}
+
+fn process_impl(attributes: &syn::LitStr, mut item_impl: syn::ItemImpl) -> TokenStream {
+    unsupported_if_some!(item_impl.defaultness);
+    unsupported_if_some!(item_impl.unsafety);
+
+    let mut items = Vec::new();
+    for item in item_impl.items.drain(..) {
+        match item {
+            syn::ImplItem::Fn(function) => {
+                unsupported_if_some!(function.defaultness);
+                let function = syn::ItemFn {
+                    attrs: function.attrs,
+                    vis: function.vis,
+                    sig: function.sig,
+                    block: Box::new(function.block),
+                };
+                let output_item = process_function(
+                    attributes,
+                    function,
+                    Some((item_impl.generics.clone(), item_impl.self_ty.clone())),
+                );
+                items.push(syn::ImplItem::Verbatim(output_item.into()));
+            }
+            item => items.push(item),
+        }
+    }
+
+    item_impl.items = items;
+    quote::quote! {
+        #item_impl
+    }
+    .into()
+}
+
+fn is_path_eq(path: &syn::Path, ident: &str) -> bool {
+    let segments: Vec<_> = ident.split("::").collect();
+    path.segments.len() == segments.len()
+        && path
+            .segments
+            .iter()
+            .zip(segments.iter())
+            .all(|(segment, expected)| segment.ident == expected && segment.arguments.is_none())
+}
+
+fn process_function(
+    attributes: &syn::LitStr,
+    function: syn::ItemFn,
+    outer: Option<(syn::Generics, Box<syn::Type>)>,
+) -> TokenStream {
+    if function.sig.unsafety.is_some() {
+        return quote::quote! {
+            #[target_feature(enable = #attributes)]
+            #function
+        }
+        .into();
+    }
+
+    unsupported_if_some!(function.sig.constness);
+    unsupported_if_some!(function.sig.asyncness);
+    unsupported_if_some!(function.sig.abi);
+    unsupported_if_some!(function.sig.variadic);
+
+    let function_visibility = function.vis;
+    let function_name = function.sig.ident;
+    let function_return = function.sig.output;
+    let function_inner_name =
+        syn::Ident::new(&format!("_impl_{}", function_name), function_name.span());
+    let function_args = function.sig.inputs;
+    let function_body = function.block;
+    let mut function_call_args = Vec::new();
+    let mut function_args_outer = Vec::new();
+    let mut function_args_inner = Vec::new();
+    for (index, arg) in function_args.iter().enumerate() {
+        match arg {
+            syn::FnArg::Receiver(receiver) => {
+                unsupported_if_some!(receiver.attrs.first());
+                unsupported_if_some!(receiver.colon_token);
+
+                if outer.is_none() {
+                    return syn::Error::new(receiver.span(), "unsupported by #[unsafe_target_feature(...)]; put the attribute on the outer `impl`").into_compile_error().into();
+                }
+
+                function_args_inner.push(syn::FnArg::Receiver(receiver.clone()));
+                function_args_outer.push(syn::FnArg::Receiver(receiver.clone()));
+                function_call_args.push(syn::Ident::new("self", receiver.self_token.span()));
+            }
+            syn::FnArg::Typed(ty) => {
+                unsupported_if_some!(ty.attrs.first());
+
+                match &*ty.pat {
+                    syn::Pat::Ident(pat_ident) => {
+                        unsupported_if_some!(pat_ident.attrs.first());
+
+                        function_args_inner.push(arg.clone());
+                        function_args_outer.push(syn::FnArg::Typed(syn::PatType {
+                            attrs: Vec::new(),
+                            pat: Box::new(syn::Pat::Ident(syn::PatIdent {
+                                attrs: Vec::new(),
+                                by_ref: None,
+                                mutability: None,
+                                ident: pat_ident.ident.clone(),
+                                subpat: None,
+                            })),
+                            colon_token: ty.colon_token,
+                            ty: ty.ty.clone(),
+                        }));
+                        function_call_args.push(pat_ident.ident.clone());
+                    }
+                    syn::Pat::Wild(pat_wild) => {
+                        unsupported_if_some!(pat_wild.attrs.first());
+
+                        let ident = syn::Ident::new(
+                            &format!("__arg_{}__", index),
+                            pat_wild.underscore_token.span(),
+                        );
+                        function_args_inner.push(arg.clone());
+                        function_args_outer.push(syn::FnArg::Typed(syn::PatType {
+                            attrs: Vec::new(),
+                            pat: Box::new(syn::Pat::Ident(syn::PatIdent {
+                                attrs: Vec::new(),
+                                by_ref: None,
+                                mutability: None,
+                                ident: ident.clone(),
+                                subpat: None,
+                            })),
+                            colon_token: ty.colon_token,
+                            ty: ty.ty.clone(),
+                        }));
+                        function_call_args.push(ident);
+                    }
+                    _ => unsupported!(arg),
+                }
+            }
+        }
+    }
+
+    let mut maybe_inline = quote::quote! {};
+    let mut maybe_outer_attributes = Vec::new();
+    let mut maybe_cfg = quote::quote! {};
+    for attribute in function.attrs {
+        match &attribute.meta {
+            syn::Meta::Path(path) if is_path_eq(path, "inline") => {
+                maybe_inline = quote::quote! { #[inline] };
+            }
+            syn::Meta::Path(path) if is_path_eq(path, "test") => {
+                maybe_outer_attributes.push(attribute);
+                maybe_cfg = quote::quote! { #[cfg(target_feature = #attributes)] };
+            }
+            syn::Meta::List(syn::MetaList { path, tokens, .. })
+                if is_path_eq(path, "inline") && tokens.to_string() == "always" =>
+            {
+                maybe_inline = quote::quote! { #[inline] };
+            }
+            syn::Meta::NameValue(syn::MetaNameValue { path, .. }) if is_path_eq(path, "doc") => {
+                maybe_outer_attributes.push(attribute);
+            }
+            syn::Meta::List(syn::MetaList { path, .. })
+                if is_path_eq(path, "cfg")
+                    || is_path_eq(path, "allow")
+                    || is_path_eq(path, "deny") =>
+            {
+                maybe_outer_attributes.push(attribute);
+            }
+            syn::Meta::Path(path) if is_path_eq(path, "rustfmt::skip") => {
+                maybe_outer_attributes.push(attribute);
+            }
+            _ => unsupported!(attribute),
+        }
+    }
+
+    let (fn_impl_generics, fn_ty_generics, fn_where_clause) =
+        function.sig.generics.split_for_impl();
+    let fn_call_generics = fn_ty_generics.as_turbofish();
+
+    if let Some((generics, self_ty)) = outer {
+        let (outer_impl_generics, outer_ty_generics, outer_where_clause) =
+            generics.split_for_impl();
+        let trait_ident =
+            syn::Ident::new(&format!("__Impl_{}__", function_name), function_name.span());
+        let item_trait = quote::quote! {
+            #[allow(non_camel_case_types)]
+            trait #trait_ident #outer_impl_generics #outer_where_clause {
+                unsafe fn #function_inner_name #fn_impl_generics (#(#function_args_outer),*) #function_return #fn_where_clause;
+            }
+        };
+
+        let item_trait_impl = quote::quote! {
+            impl #outer_impl_generics #trait_ident #outer_ty_generics for #self_ty #outer_where_clause {
+                #[target_feature(enable = #attributes)]
+                #maybe_inline
+                unsafe fn #function_inner_name #fn_impl_generics (#(#function_args_inner),*) #function_return #fn_where_clause #function_body
+            }
+        };
+
+        quote::quote! {
+            #[inline(always)]
+            #(#maybe_outer_attributes)*
+            #function_visibility fn #function_name #fn_impl_generics (#(#function_args_outer),*) #function_return #fn_where_clause {
+                #item_trait
+                #item_trait_impl
+                unsafe {
+                    <Self as #trait_ident #outer_ty_generics> ::#function_inner_name #fn_call_generics (#(#function_call_args),*)
+                }
+            }
+        }.into()
+    } else {
+        quote::quote! {
+            #[inline(always)]
+            #maybe_cfg
+            #(#maybe_outer_attributes)*
+            #function_visibility fn #function_name #fn_impl_generics (#(#function_args_outer),*) #function_return #fn_where_clause {
+                #[target_feature(enable = #attributes)]
+                #maybe_inline
+                unsafe fn #function_inner_name #fn_impl_generics (#(#function_args_inner),*) #function_return #fn_where_clause #function_body
+                unsafe {
+                    #function_inner_name #fn_call_generics (#(#function_call_args),*)
+                }
+            }
+        }.into()
+    }
+}
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 18c8c2251..4424e0a53 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -39,41 +39,21 @@ use crate::Scalar;
 
 pub mod serial;
 
-#[cfg(all(
-    target_arch = "x86_64",
-    any(feature = "simd_avx2", all(feature = "simd_avx512", nightly)),
-    curve25519_dalek_bits = "64",
-    not(curve25519_dalek_backend = "fiat")
-))]
+#[cfg(curve25519_dalek_backend = "simd")]
 pub mod vector;
 
 #[derive(Copy, Clone)]
 enum BackendKind {
-    #[cfg(all(
-        target_arch = "x86_64",
-        feature = "simd_avx2",
-        curve25519_dalek_bits = "64",
-        not(curve25519_dalek_backend = "fiat")
-    ))]
+    #[cfg(curve25519_dalek_backend = "simd")]
     Avx2,
-    #[cfg(all(
-        target_arch = "x86_64",
-        all(feature = "simd_avx512", nightly),
-        curve25519_dalek_bits = "64",
-        not(curve25519_dalek_backend = "fiat")
-    ))]
+    #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
     Avx512,
     Serial,
 }
 
 #[inline]
 fn get_selected_backend() -> BackendKind {
-    #[cfg(all(
-        target_arch = "x86_64",
-        all(feature = "simd_avx512", nightly),
-        curve25519_dalek_bits = "64",
-        not(curve25519_dalek_backend = "fiat")
-    ))]
+    #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
     {
         cpufeatures::new!(cpuid_avx512, "avx512ifma", "avx512vl");
         let token_avx512: cpuid_avx512::InitToken = cpuid_avx512::init();
@@ -82,12 +62,7 @@ fn get_selected_backend() -> BackendKind {
         }
     }
 
-    #[cfg(all(
-        target_arch = "x86_64",
-        feature = "simd_avx2",
-        curve25519_dalek_bits = "64",
-        not(curve25519_dalek_backend = "fiat")
-    ))]
+    #[cfg(curve25519_dalek_backend = "simd")]
     {
         cpufeatures::new!(cpuid_avx2, "avx2");
         let token_avx2: cpuid_avx2::InitToken = cpuid_avx2::init();
@@ -110,10 +85,10 @@ where
     use crate::traits::VartimeMultiscalarMul;
 
     match get_selected_backend() {
-        #[cfg(all(target_arch = "x86_64", feature = "simd_avx2", curve25519_dalek_bits = "64", not(curve25519_dalek_backend = "fiat")))]
+        #[cfg(curve25519_dalek_backend = "simd")]
         BackendKind::Avx2 =>
             self::vector::scalar_mul::pippenger::spec_avx2::Pippenger::optional_multiscalar_mul::<I, J>(scalars, points),
-        #[cfg(all(target_arch = "x86_64", all(feature = "simd_avx512", nightly), curve25519_dalek_bits = "64", not(curve25519_dalek_backend = "fiat")))]
+        #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
         BackendKind::Avx512 =>
             self::vector::scalar_mul::pippenger::spec_avx512ifma_avx512vl::Pippenger::optional_multiscalar_mul::<I, J>(scalars, points),
         BackendKind::Serial =>
@@ -123,19 +98,9 @@ where
 
 #[cfg(feature = "alloc")]
 pub(crate) enum VartimePrecomputedStraus {
-    #[cfg(all(
-        target_arch = "x86_64",
-        feature = "simd_avx2",
-        curve25519_dalek_bits = "64",
-        not(curve25519_dalek_backend = "fiat")
-    ))]
+    #[cfg(curve25519_dalek_backend = "simd")]
     Avx2(self::vector::scalar_mul::precomputed_straus::spec_avx2::VartimePrecomputedStraus),
-    #[cfg(all(
-        target_arch = "x86_64",
-        all(feature = "simd_avx512", nightly),
-        curve25519_dalek_bits = "64",
-        not(curve25519_dalek_backend = "fiat")
-    ))]
+    #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
     Avx512ifma(
         self::vector::scalar_mul::precomputed_straus::spec_avx512ifma_avx512vl::VartimePrecomputedStraus,
     ),
@@ -152,10 +117,10 @@ impl VartimePrecomputedStraus {
         use crate::traits::VartimePrecomputedMultiscalarMul;
 
         match get_selected_backend() {
-            #[cfg(all(target_arch = "x86_64", feature = "simd_avx2", curve25519_dalek_bits = "64", not(curve25519_dalek_backend = "fiat")))]
+            #[cfg(curve25519_dalek_backend = "simd")]
             BackendKind::Avx2 =>
                 VartimePrecomputedStraus::Avx2(self::vector::scalar_mul::precomputed_straus::spec_avx2::VartimePrecomputedStraus::new(static_points)),
-            #[cfg(all(target_arch = "x86_64", all(feature = "simd_avx512", nightly), curve25519_dalek_bits = "64", not(curve25519_dalek_backend = "fiat")))]
+            #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
             BackendKind::Avx512 =>
                 VartimePrecomputedStraus::Avx512ifma(self::vector::scalar_mul::precomputed_straus::spec_avx512ifma_avx512vl::VartimePrecomputedStraus::new(static_points)),
             BackendKind::Serial =>
@@ -179,23 +144,13 @@ impl VartimePrecomputedStraus {
         use crate::traits::VartimePrecomputedMultiscalarMul;
 
         match self {
-            #[cfg(all(
-                target_arch = "x86_64",
-                feature = "simd_avx2",
-                curve25519_dalek_bits = "64",
-                not(curve25519_dalek_backend = "fiat")
-            ))]
+            #[cfg(curve25519_dalek_backend = "simd")]
             VartimePrecomputedStraus::Avx2(inner) => inner.optional_mixed_multiscalar_mul(
                 static_scalars,
                 dynamic_scalars,
                 dynamic_points,
             ),
-            #[cfg(all(
-                target_arch = "x86_64",
-                all(feature = "simd_avx512", nightly),
-                curve25519_dalek_bits = "64",
-                not(curve25519_dalek_backend = "fiat")
-            ))]
+            #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
             VartimePrecomputedStraus::Avx512ifma(inner) => inner.optional_mixed_multiscalar_mul(
                 static_scalars,
                 dynamic_scalars,
@@ -222,23 +177,13 @@ where
     use crate::traits::MultiscalarMul;
 
     match get_selected_backend() {
-        #[cfg(all(
-            target_arch = "x86_64",
-            feature = "simd_avx2",
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(curve25519_dalek_backend = "simd")]
         BackendKind::Avx2 => {
             self::vector::scalar_mul::straus::spec_avx2::Straus::multiscalar_mul::<I, J>(
                 scalars, points,
             )
         }
-        #[cfg(all(
-            target_arch = "x86_64",
-            all(feature = "simd_avx512", nightly),
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
         BackendKind::Avx512 => {
             self::vector::scalar_mul::straus::spec_avx512ifma_avx512vl::Straus::multiscalar_mul::<
                 I,
@@ -262,23 +207,13 @@ where
     use crate::traits::VartimeMultiscalarMul;
 
     match get_selected_backend() {
-        #[cfg(all(
-            target_arch = "x86_64",
-            feature = "simd_avx2",
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(curve25519_dalek_backend = "simd")]
         BackendKind::Avx2 => {
             self::vector::scalar_mul::straus::spec_avx2::Straus::optional_multiscalar_mul::<I, J>(
                 scalars, points,
             )
         }
-        #[cfg(all(
-            target_arch = "x86_64",
-            all(feature = "simd_avx512", nightly),
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
         BackendKind::Avx512 => {
             self::vector::scalar_mul::straus::spec_avx512ifma_avx512vl::Straus::optional_multiscalar_mul::<
                 I,
@@ -296,19 +231,9 @@ where
 /// Perform constant-time, variable-base scalar multiplication.
 pub fn variable_base_mul(point: &EdwardsPoint, scalar: &Scalar) -> EdwardsPoint {
     match get_selected_backend() {
-        #[cfg(all(
-            target_arch = "x86_64",
-            feature = "simd_avx2",
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(curve25519_dalek_backend = "simd")]
         BackendKind::Avx2 => self::vector::scalar_mul::variable_base::spec_avx2::mul(point, scalar),
-        #[cfg(all(
-            target_arch = "x86_64",
-            all(feature = "simd_avx512", nightly),
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
         BackendKind::Avx512 => {
             self::vector::scalar_mul::variable_base::spec_avx512ifma_avx512vl::mul(point, scalar)
         }
@@ -320,19 +245,9 @@ pub fn variable_base_mul(point: &EdwardsPoint, scalar: &Scalar) -> EdwardsPoint
 #[allow(non_snake_case)]
 pub fn vartime_double_base_mul(a: &Scalar, A: &EdwardsPoint, b: &Scalar) -> EdwardsPoint {
     match get_selected_backend() {
-        #[cfg(all(
-            target_arch = "x86_64",
-            feature = "simd_avx2",
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(curve25519_dalek_backend = "simd")]
         BackendKind::Avx2 => self::vector::scalar_mul::vartime_double_base::spec_avx2::mul(a, A, b),
-        #[cfg(all(
-            target_arch = "x86_64",
-            all(feature = "simd_avx512", nightly),
-            curve25519_dalek_bits = "64",
-            not(curve25519_dalek_backend = "fiat")
-        ))]
+        #[cfg(all(curve25519_dalek_backend = "simd", nightly))]
         BackendKind::Avx512 => {
             self::vector::scalar_mul::vartime_double_base::spec_avx512ifma_avx512vl::mul(a, A, b)
         }
diff --git a/src/backend/vector/avx2/edwards.rs b/src/backend/vector/avx2/edwards.rs
index 7bb58b1ee..56d0835bb 100644
--- a/src/backend/vector/avx2/edwards.rs
+++ b/src/backend/vector/avx2/edwards.rs
@@ -41,7 +41,7 @@ use core::ops::{Add, Neg, Sub};
 use subtle::Choice;
 use subtle::ConditionallySelectable;
 
-use unsafe_target_feature::unsafe_target_feature;
+use curve25519_dalek_derive::unsafe_target_feature;
 
 use crate::edwards;
 use crate::window::{LookupTable, NafLookupTable5};
diff --git a/src/backend/vector/avx2/field.rs b/src/backend/vector/avx2/field.rs
index bdb55efa5..b593cdc5b 100644
--- a/src/backend/vector/avx2/field.rs
+++ b/src/backend/vector/avx2/field.rs
@@ -48,7 +48,7 @@ use crate::backend::vector::avx2::constants::{
     P_TIMES_16_HI, P_TIMES_16_LO, P_TIMES_2_HI, P_TIMES_2_LO,
 };
 
-use unsafe_target_feature::unsafe_target_feature;
+use curve25519_dalek_derive::unsafe_target_feature;
 
 /// Unpack 32-bit lanes into 64-bit lanes:
 /// ```ascii,no_run
diff --git a/src/backend/vector/ifma/edwards.rs b/src/backend/vector/ifma/edwards.rs
index ccfe092c8..f8605fe52 100644
--- a/src/backend/vector/ifma/edwards.rs
+++ b/src/backend/vector/ifma/edwards.rs
@@ -16,7 +16,7 @@ use core::ops::{Add, Neg, Sub};
 use subtle::Choice;
 use subtle::ConditionallySelectable;
 
-use unsafe_target_feature::unsafe_target_feature;
+use curve25519_dalek_derive::unsafe_target_feature;
 
 use crate::edwards;
 use crate::window::{LookupTable, NafLookupTable5};
diff --git a/src/backend/vector/ifma/field.rs b/src/backend/vector/ifma/field.rs
index 5928e14a2..fa8ce2dd9 100644
--- a/src/backend/vector/ifma/field.rs
+++ b/src/backend/vector/ifma/field.rs
@@ -16,7 +16,7 @@ use core::ops::{Add, Mul, Neg};
 
 use crate::backend::serial::u64::field::FieldElement51;
 
-use unsafe_target_feature::unsafe_target_feature;
+use curve25519_dalek_derive::unsafe_target_feature;
 
 /// A wrapper around `vpmadd52luq` that works on `u64x4`.
 #[unsafe_target_feature("avx512ifma,avx512vl")]
diff --git a/src/backend/vector/mod.rs b/src/backend/vector/mod.rs
index d720f4acb..2839dca45 100644
--- a/src/backend/vector/mod.rs
+++ b/src/backend/vector/mod.rs
@@ -14,10 +14,9 @@
 #[allow(missing_docs)]
 pub mod packed_simd;
 
-#[cfg(feature = "simd_avx2")]
 pub mod avx2;
 
-#[cfg(all(feature = "simd_avx512", nightly))]
+#[cfg(nightly)]
 pub mod ifma;
 
 pub mod scalar_mul;
diff --git a/src/backend/vector/packed_simd.rs b/src/backend/vector/packed_simd.rs
index 6ab5dcc9c..fe83b1865 100644
--- a/src/backend/vector/packed_simd.rs
+++ b/src/backend/vector/packed_simd.rs
@@ -11,7 +11,7 @@
 //!           by the callers of this code.
 use core::ops::{Add, AddAssign, BitAnd, BitAndAssign, BitXor, BitXorAssign, Sub};
 
-use unsafe_target_feature::unsafe_target_feature;
+use curve25519_dalek_derive::unsafe_target_feature;
 
 macro_rules! impl_shared {
     (
diff --git a/src/backend/vector/scalar_mul/pippenger.rs b/src/backend/vector/scalar_mul/pippenger.rs
index b00cb87c5..099f4f5e7 100644
--- a/src/backend/vector/scalar_mul/pippenger.rs
+++ b/src/backend/vector/scalar_mul/pippenger.rs
@@ -9,9 +9,9 @@
 
 #![allow(non_snake_case)]
 
-#[unsafe_target_feature::unsafe_target_feature_specialize(
-    conditional("avx2", feature = "simd_avx2"),
-    conditional("avx512ifma,avx512vl", all(feature = "simd_avx512", nightly))
+#[curve25519_dalek_derive::unsafe_target_feature_specialize(
+    "avx2",
+    conditional("avx512ifma,avx512vl", nightly)
 )]
 pub mod spec {
 
diff --git a/src/backend/vector/scalar_mul/precomputed_straus.rs b/src/backend/vector/scalar_mul/precomputed_straus.rs
index 8c45c29cf..515b4040c 100644
--- a/src/backend/vector/scalar_mul/precomputed_straus.rs
+++ b/src/backend/vector/scalar_mul/precomputed_straus.rs
@@ -11,9 +11,9 @@
 
 #![allow(non_snake_case)]
 
-#[unsafe_target_feature::unsafe_target_feature_specialize(
-    conditional("avx2", feature = "simd_avx2"),
-    conditional("avx512ifma,avx512vl", all(feature = "simd_avx512", nightly))
+#[curve25519_dalek_derive::unsafe_target_feature_specialize(
+    "avx2",
+    conditional("avx512ifma,avx512vl", nightly)
 )]
 pub mod spec {
 
diff --git a/src/backend/vector/scalar_mul/straus.rs b/src/backend/vector/scalar_mul/straus.rs
index 046bcd14c..413e6fd9a 100644
--- a/src/backend/vector/scalar_mul/straus.rs
+++ b/src/backend/vector/scalar_mul/straus.rs
@@ -11,9 +11,9 @@
 
 #![allow(non_snake_case)]
 
-#[unsafe_target_feature::unsafe_target_feature_specialize(
-    conditional("avx2", feature = "simd_avx2"),
-    conditional("avx512ifma,avx512vl", all(feature = "simd_avx512", nightly))
+#[curve25519_dalek_derive::unsafe_target_feature_specialize(
+    "avx2",
+    conditional("avx512ifma,avx512vl", nightly)
 )]
 pub mod spec {
 
@@ -22,6 +22,7 @@ pub mod spec {
     use core::borrow::Borrow;
     use core::cmp::Ordering;
 
+    #[cfg(feature = "zeroize")]
     use zeroize::Zeroizing;
 
     #[for_target_feature("avx2")]
@@ -67,12 +68,13 @@ pub mod spec {
                 .map(|s| s.borrow().as_radix_16())
                 .collect();
             // Pass ownership to a `Zeroizing` wrapper
-            let scalar_digits = Zeroizing::new(scalar_digits_vec);
+            #[cfg(feature = "zeroize")]
+            let scalar_digits_vec = Zeroizing::new(scalar_digits_vec);
 
             let mut Q = ExtendedPoint::identity();
             for j in (0..64).rev() {
                 Q = Q.mul_by_pow_2(4);
-                let it = scalar_digits.iter().zip(lookup_tables.iter());
+                let it = scalar_digits_vec.iter().zip(lookup_tables.iter());
                 for (s_i, lookup_table_i) in it {
                     // Q = Q + s_{i,j} * P_i
                     Q = &Q + &lookup_table_i.select(s_i[j]);
diff --git a/src/backend/vector/scalar_mul/variable_base.rs b/src/backend/vector/scalar_mul/variable_base.rs
index 2da479926..9f924f286 100644
--- a/src/backend/vector/scalar_mul/variable_base.rs
+++ b/src/backend/vector/scalar_mul/variable_base.rs
@@ -1,8 +1,8 @@
 #![allow(non_snake_case)]
 
-#[unsafe_target_feature::unsafe_target_feature_specialize(
-    conditional("avx2", feature = "simd_avx2"),
-    conditional("avx512ifma,avx512vl", all(feature = "simd_avx512", nightly))
+#[curve25519_dalek_derive::unsafe_target_feature_specialize(
+    "avx2",
+    conditional("avx512ifma,avx512vl", nightly)
 )]
 pub mod spec {
 
diff --git a/src/backend/vector/scalar_mul/vartime_double_base.rs b/src/backend/vector/scalar_mul/vartime_double_base.rs
index 191572bb1..ea2af8ad4 100644
--- a/src/backend/vector/scalar_mul/vartime_double_base.rs
+++ b/src/backend/vector/scalar_mul/vartime_double_base.rs
@@ -11,9 +11,9 @@
 
 #![allow(non_snake_case)]
 
-#[unsafe_target_feature::unsafe_target_feature_specialize(
-    conditional("avx2", feature = "simd_avx2"),
-    conditional("avx512ifma,avx512vl", all(feature = "simd_avx512", nightly))
+#[curve25519_dalek_derive::unsafe_target_feature_specialize(
+    "avx2",
+    conditional("avx512ifma,avx512vl", nightly)
 )]
 pub mod spec {
 
diff --git a/src/diagnostics.rs b/src/diagnostics.rs
new file mode 100644
index 000000000..d5becef67
--- /dev/null
+++ b/src/diagnostics.rs
@@ -0,0 +1,25 @@
+//! Build time diagnostics
+
+// auto is assumed or selected
+#[cfg(curve25519_dalek_backend = "auto")]
+compile_error!("curve25519_dalek_backend is 'auto'");
+
+// fiat was overriden
+#[cfg(curve25519_dalek_backend = "fiat")]
+compile_error!("curve25519_dalek_backend is 'fiat'");
+
+// serial was assumed or overriden
+#[cfg(curve25519_dalek_backend = "serial")]
+compile_error!("curve25519_dalek_backend is 'serial'");
+
+// simd was assumed over overriden
+#[cfg(curve25519_dalek_backend = "simd")]
+compile_error!("curve25519_dalek_backend is 'simd'");
+
+// 32 bits target_pointer_width was assumed or overriden
+#[cfg(curve25519_dalek_bits = "32")]
+compile_error!("curve25519_dalek_bits is '32'");
+
+// 64 bits target_pointer_width was assumed or overriden
+#[cfg(curve25519_dalek_bits = "64")]
+compile_error!("curve25519_dalek_bits is '64'");
diff --git a/src/lib.rs b/src/lib.rs
index f4d1d8223..98d79ae1f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,12 +10,9 @@
 // - Henry de Valence <hdevalence@hdevalence.ca>
 
 #![no_std]
+#![cfg_attr(all(curve25519_dalek_backend = "simd", nightly), feature(stdsimd))]
 #![cfg_attr(
-    all(target_arch = "x86_64", feature = "simd_avx512", nightly),
-    feature(stdsimd)
-)]
-#![cfg_attr(
-    all(target_arch = "x86_64", feature = "simd_avx512", nightly),
+    all(curve25519_dalek_backend = "simd", nightly),
     feature(avx512_target_feature)
 )]
 #![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg, doc_cfg_hide))]
@@ -92,3 +89,7 @@ pub(crate) mod window;
 pub use crate::{
     edwards::EdwardsPoint, montgomery::MontgomeryPoint, ristretto::RistrettoPoint, scalar::Scalar,
 };
+
+// Build time diagnostics for validation
+#[cfg(curve25519_dalek_diagnostics = "build")]
+mod diagnostics;


Test patch:
diff --git a/curve25519-dalek-derive/tests/tests.rs b/curve25519-dalek-derive/tests/tests.rs
new file mode 100644
index 000000000..2ccd237e6
--- /dev/null
+++ b/curve25519-dalek-derive/tests/tests.rs
@@ -0,0 +1,151 @@
+#![allow(dead_code)]
+#![allow(unused_imports)]
+
+use curve25519_dalek_derive::{unsafe_target_feature, unsafe_target_feature_specialize};
+
+#[unsafe_target_feature("sse2")]
+/// A doc comment.
+fn function(a: u32, b: u32) -> u32 {
+    a - b
+}
+
+#[unsafe_target_feature("sse2")]
+fn function_with_const_arg<const N: u32>(b: u32) -> u32 {
+    N - b
+}
+
+#[unsafe_target_feature("sse2")]
+fn function_with_where_clause<T>(a: T, b: T) -> T::Output
+where
+    T: Copy + core::ops::Sub,
+{
+    a - b
+}
+
+#[unsafe_target_feature("sse2")]
+#[cfg(feature = "dummy")]
+fn function_with_cfg() {}
+
+#[unsafe_target_feature("sse2")]
+#[rustfmt::skip]
+fn function_with_rustfmt_skip() {}
+
+struct Struct {
+    a: u32,
+}
+
+#[unsafe_target_feature("sse2")]
+impl Struct {
+    #[allow(unused_mut)]
+    fn member_function(&self, mut b: u32) -> u32 {
+        self.a - b
+    }
+
+    fn member_function_with_const_arg<const N: u32>(self) -> u32 {
+        self.a - N
+    }
+
+    #[cfg(feature = "dummy")]
+    fn member_function_with_cfg() {}
+}
+
+struct StructWithGenerics<T>
+where
+    T: Copy + core::ops::Sub,
+{
+    a: T,
+}
+
+#[unsafe_target_feature("sse2")]
+impl<T> StructWithGenerics<T>
+where
+    T: Copy + core::ops::Sub,
+{
+    #[inline]
+    fn member_function(&self, b: T) -> T::Output {
+        self.a - b
+    }
+}
+
+struct StructWithGenericsNoWhere<T: Copy + core::ops::Sub> {
+    a: T,
+}
+
+#[unsafe_target_feature("sse2")]
+impl<T: Copy + core::ops::Sub> StructWithGenericsNoWhere<T> {
+    #[inline(always)]
+    fn member_function(&self, b: T) -> T::Output {
+        self.a - b
+    }
+}
+
+#[unsafe_target_feature("sse2")]
+#[allow(dead_code)]
+impl<'a> From<&'a Struct> for () {
+    fn from(_: &'a Struct) -> Self {
+        ()
+    }
+}
+
+#[unsafe_target_feature("sse2")]
+mod inner {
+    fn inner_function(a: u32, b: u32) -> u32 {
+        a - b
+    }
+}
+
+#[unsafe_target_feature_specialize("sse2", "avx2", conditional("avx512ifma", disabled))]
+mod inner_spec {
+    use std;
+
+    #[for_target_feature("sse2")]
+    const CONST: u32 = 1;
+
+    #[for_target_feature("avx2")]
+    const CONST: u32 = 2;
+
+    pub fn spec_function(a: u32, b: u32) -> u32 {
+        a - b - CONST
+    }
+
+    #[for_target_feature("sse2")]
+    const IS_AVX2: bool = false;
+
+    #[for_target_feature("avx2")]
+    const IS_AVX2: bool = true;
+
+    #[test]
+    fn test_specialized() {
+        assert!(!IS_AVX2);
+    }
+
+    #[cfg(test)]
+    mod tests {
+        #[test]
+        fn test_specialized_inner() {
+            assert!(!super::IS_AVX2);
+        }
+    }
+}
+
+#[unsafe_target_feature("sse2")]
+#[test]
+fn test_sse2_only() {}
+
+#[unsafe_target_feature("avx2")]
+#[test]
+fn test_avx2_only() {
+    compile_error!();
+}
+
+#[test]
+fn test_function() {
+    assert_eq!(function(10, 3), 7);
+    assert_eq!(function_with_where_clause(10, 3), 7);
+    assert_eq!(function_with_const_arg::<10>(3), 7);
+    assert_eq!(Struct { a: 10 }.member_function(3), 7);
+    assert_eq!(StructWithGenerics { a: 10 }.member_function(3), 7);
+    assert_eq!(StructWithGenericsNoWhere { a: 10 }.member_function(3), 7);
+    assert_eq!(inner_spec_sse2::spec_function(10, 3), 6);
+    assert_eq!(inner_spec_avx2::spec_function(10, 3), 5);
+}
diff --git a/tests/build_tests.sh b/tests/build_tests.sh
new file mode 100755
index 000000000..ac6e7819d
--- /dev/null
+++ b/tests/build_tests.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+
+function match_and_report() {
+    PATTERN=$1
+    FILE=$2
+
+    if grep -q "$PATTERN" "$FILE"; then
+      echo build OK "$FILE" : "$PATTERN"
+    else
+      echo build ERROR "$FILE" : "$PATTERN"
+      echo ">>>>>>>>>>>>>>>>>>>>>>>>>>"
+      cat "$FILE"
+      echo "<<<<<<<<<<<<<<<<<<<<<<<<<<"
+      exit 1
+    fi
+}
+
+# Assuming naively 64 bit host
+cargo clean
+OUT=build_1.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\"" cargo build > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'simd'" "$OUT"
+match_and_report "curve25519_dalek_bits is '64'" "$OUT"
+
+# Override to 32 bits assuming naively 64 bit build host
+cargo clean
+OUT=build_2.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\" --cfg curve25519_dalek_bits=\"32\"" cargo build > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'serial'" "$OUT"
+match_and_report "curve25519_dalek_bits is '32'" "$OUT"
+
+# Override to 64 bits on 32 bit target
+cargo clean
+OUT=build_3.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\" --cfg curve25519_dalek_bits=\"64\"" cargo build --target i686-unknown-linux-gnu > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'serial'" "$OUT"
+match_and_report "curve25519_dalek_bits is '64'" "$OUT"
+
+# 32 bit target default
+cargo clean
+OUT=build_4.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\"" cargo build --target i686-unknown-linux-gnu > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'serial'" "$OUT"
+match_and_report "curve25519_dalek_bits is '32'" "$OUT"
+
+# wasm 32 bit target default
+cargo clean
+OUT=build_5.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\"" cargo build --target wasm32-unknown-unknown > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'serial'" "$OUT"
+match_and_report "curve25519_dalek_bits is '32'" "$OUT"
+
+# wasm 32 bit target default
+# Attempted override w/ "simd" should result "serial" addition
+cargo clean
+OUT=build_5_1.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\" --cfg curve25519_dalek_backend=\"simd\"" cargo build --target wasm32-unknown-unknown > "$OUT" 2>&1
+# This overide must fail the compilation since "simd" is not available
+# See: issues/532
+match_and_report "Could not override curve25519_dalek_backend to simd" "$OUT"
+
+# fiat override with default 64 bit naive host assumption
+cargo clean
+OUT=build_6.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\" --cfg curve25519_dalek_backend=\"fiat\"" cargo build > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'fiat'" "$OUT"
+match_and_report "curve25519_dalek_bits is '64'" "$OUT"
+
+# fiat 32 bit override
+cargo clean
+OUT=build_7.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\" --cfg curve25519_dalek_backend=\"fiat\" --cfg curve25519_dalek_bits=\"32\"" cargo build > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'fiat'" "$OUT"
+match_and_report "curve25519_dalek_bits is '32'" "$OUT"
+
+# serial override with default 64 bit naive host assumption
+cargo clean
+OUT=build_8.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\" --cfg curve25519_dalek_backend=\"serial\"" cargo build > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'serial'" "$OUT"
+match_and_report "curve25519_dalek_bits is '64'" "$OUT"
+
+# serial 32 bit override
+cargo clean
+OUT=build_9.txt
+env RUSTFLAGS="--cfg curve25519_dalek_diagnostics=\"build\" --cfg curve25519_dalek_backend=\"serial\" --cfg curve25519_dalek_bits=\"32\"" cargo build > "$OUT" 2>&1
+match_and_report "curve25519_dalek_backend is 'serial'" "$OUT"
+match_and_report "curve25519_dalek_bits is '32'" "$OUT"