Pre-RFC: #[target_feature(...)] in trait methods

  • Feature Name: target_feature in trait methods
  • Start Date: 2020-12-21
  • RFC PR: none yet
  • Rust Issue: none yet

Summary

This RFC builds on RFC 2045 (target_feature) and RFC 2396 (target feature 1.1) by:

  • Allowing trait methods to have #[target_feature] attributes
  • Expanding #[target_feature] 1.1's relaxed safety rules to include calls to and from the newly-allowed #[target_feature] trait methods.

Motivation

Imagine we're writing an AVX library with the goal of being generic over f32 and f64. We can start by creating a trait called AvxVector with some methods common to both f32 and f64 vectors:

pub trait AvxVector {
    unsafe fn add(left: Self, right: Self) -> Self;
}
impl AvxVector for __m256 {
    unsafe fn add(left: Self, right: Self) -> Self {
        _mm256_add_ps(left, right)
    }
}
impl AvxVector for __m256d {
    unsafe fn add(left: Self, right: Self) -> Self {
        _mm256_add_pd(left, right)
    }
}

Now, users of our library can compute a sum of f32 and f64 vectors with the same code:

#[target_feature(enable = "avx")]
unsafe fn do_work() {
    let left32 : __m256 = ...;
    let right32 : __m256 = ...;
    let sum32 = unsafe { AvxVector::add(left32, right32) }

    let left64 : __m256d = ...;
    let right64 : __m256d = ...;
    let sum64 = unsafe { AvxVector::add(left64, right64) }
}

Everything so far compiles on stable Rust, but there's a major flaw: Despite every single function being marked unsafe, absolutely nothing unsafe is happening here! RFC 2396 (target feature 1.1) improves the situation for standalone functions by allowing calls to a #[target_feature(...)] function to be safe as long as the caller also has #[target_feature(...)] with the same features.

However, that change doesn't apply here, because #[target_feature(...)] isn't allowed on trait methods. If it was allowed, this example could be written with no unsafe whatsoever.

Guide-level explanation

Currently, we don't allow #[target_feature] to appear on trait methods:

pub trait AvxVector {
    #[target_feature(enable = "avx")]
    fn add(left: Self, right: Self) -> Self;
}
error: attribute should be applied to a function
 --> src\lib.rs:6:5
  |
6 |     #[target_feature(enable = "avx")]
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 |     fn add(left: Self, right: Self) -> Self;
  |     ---------------------------------------- not a function

With this RFC, the error above is no longer an error, and the trait definition comples successfully. To ensure consistency between trait definitions and trait impls, we require that the #[target_feature] declaration is identical between the trait definition and the trait impl. Some examples:

pub trait AvxVector {
    #[target_feature(enable = "avx")]
    fn add(left: Self, right: Self) -> Self;

    fn sub(left: Self, right: Self) -> Self;

    #[target_feature(enable = "avx")]
    fn mul(left: Self, right: Self) -> Self;

    #[target_feature(enable = "avx")]
    fn div(left: Self, right: Self) -> Self;
}

impl AvxVector for __m256 {
    // ERROR: AvxVector::add has a #[target_feature], but this impl doesn't
    fn add(left: Self, right: Self) -> Self {
        _mm256_add_ps(left, right)
    }

    // ERROR: AvxVector::sub doesn't have any target features, but this impl does
    #[target_feature(enable = "avx")]
    fn sub(left: Self, right: Self) -> Self {
        _mm256_sub_ps(left, right)
    }

    // ERROR: This impl's target features must exactly match AvxVector::mul's target features
    #[target_feature(enable = "avx", enable = "avx2")]
    fn mul(left: Self, right: Self) -> Self {
        _mm256_mul_ps(left, right)
    }

    // OK
    #[target_feature(enable = "avx")]
    fn div(left: Self, right: Self) -> Self {
        _mm256_div_ps(left, right)
    }
}

Calling trait methods with target features would work exactly like the relaxed rules in RFC 2396 (target feature 1.1): Safe #[target_feature] trait methods can be called without an unsafe {} block only from functions and trait methods that have at least the exact same set of #[target_feature]s. Some examples:

pub trait AvxVector {
    #[target_feature(enable = "avx")]
    fn add(left: Self, right: Self) -> Self;

    #[target_feature(enable = "avx", enable = "fma")]
    fn mul_add(left: Self, right: Self, add: Self) -> Self;
}

// This function does not have any target feature:
fn meow() {
    let abc1 : __m256 = ...;
    let abc2 : __m256 = ...;
    let abc3 : __m256 = ...;

    AvxVector::add(abc1, abc2); // ERROR (unsafe block required, because target features don't match)
    unsafe { AvxVector::add(abc1, abc2) }; // OK

    AvxVector::mul_add(abc1, abc2, abc3); // ERROR (unsafe block required, because target features don't match)
    unsafe { AvxVector::mul_add(abc1, abc2, abc3) }; // OK
}

// This function has the AVX target feature, but not the FMA target feature
#[target_feature(enable = "avx")]
fn bark() {
    let abc1 : __m256 = ...;
    let abc2 : __m256 = ...;
    let abc3 : __m256 = ...;

    AvxVector::add(abc1, abc2); // OK, because target features match

    AvxVector::mul_add(abc1, abc2, abc3); // ERROR (unsafe block required, because target features don't match)
    unsafe { AvxVector::mul_add(abc1, abc2, abc3) }; // OK
}

// This function has both AVX and FMA target features
#[target_feature(enable = "avx", enable = "fma")]
fn moo() {
    let abc1 : __m256 = ...;
    let abc2 : __m256 = ...;
    let abc3 : __m256 = ...;

    AvxVector::add(abc1, abc2); // OK, because target features match

    AvxVector::mul_add(abc1, abc2, abc3); // OK, because target features match
}

This change resolves the problem raised in the motivation, because moo() can now be implemented entirely in safe code.

Reference-level explanation

This RFC proposes changes to the language with respect to RFC 2045 (target_feature) and RFC 2396 (target feature 1.1):

  • Allow trait methods to have #[target_feature] attributes.
    • If a method in a trait definition has a #[target_feature] attribute, all impls of that trait method must have exactly matching #[target_feature] attributes.
    • If a method in a trait definition has no #[target_feature] attribute, its impls may not have it either.
  • Expand RFC 2396 (target feature 1.1)'s' relaxed safety rules to include calls to the newly-allowed #[target_feature] trait methods:
    • A safe function may only call a safe #[target_feature] trait method without unsafe blocks if the calling function has a #[target_feature] attribute with a superset of the trait method's features.

Drawbacks

Developers would be required to duplicate entries in target_feature attributes between trait methods and trait impls, making refactoring more difficult.

Rationale and alternatives

Prior art

Unresolved questions

3 Likes

What do you think? Is this change feasible? Is it complete enough to make a good RFC? This will be my first RFC, so I could use advice on how to make it more thorough, conform to the style of other RFCs, etc.

Seems reasonable to me.

I think it might be OK to allow the implementation of a trait to omit the attribute, if it doesn't actually use the attribute. (The caller of the trait method must still use the attribute or use unsafe.)

2 Likes

RFC: https://github.com/rust-lang/rfcs/pull/3042

1 Like