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`
`u64`
`avx2`
`avx512` | -| `fiat` | Formally verified field arithmetic from [fiat-crypto] | `fiat_u32`
`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(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 ) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +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 { + let lit = input.parse()?; + input.parse::()?; + let attr = input.parse()?; + + Ok(Conditional { lit, attr }) + } +} + +impl syn::parse::Parse for SpecializeArg { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::conditional) { + input.parse::()?; + + 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); + +impl syn::parse::Parse for SpecializeArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + 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>, +) -> 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)>, +) -> 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 { + ::#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/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(b: u32) -> u32 { + N - b +} + +#[unsafe_target_feature("sse2")] +fn function_with_where_clause(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(self) -> u32 { + self.a - N + } + + #[cfg(feature = "dummy")] + fn member_function_with_cfg() {} +} + +struct StructWithGenerics +where + T: Copy + core::ops::Sub, +{ + a: T, +} + +#[unsafe_target_feature("sse2")] +impl StructWithGenerics +where + T: Copy + core::ops::Sub, +{ + #[inline] + fn member_function(&self, b: T) -> T::Output { + self.a - b + } +} + +struct StructWithGenericsNoWhere { + a: T, +} + +#[unsafe_target_feature("sse2")] +impl StructWithGenericsNoWhere { + #[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/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::(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::(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::( 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::( 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 #![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; 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"