lang | compiler (typesystem | simd | ffi)
simd_ffiThis RFC allows using SIMD types in C FFI.
The architecture-specific SIMD types provided in core::arch cannot currently
be used in C FFI. That is, Rust programs cannot interface with C libraries that
use these in their APIs.
One notable example would be calling into vectorized libm implementations
like sleef, libmvec, or Intel's SVML. The packed_simd crate
relies on C FFI with these fundamental libraries to offer competitive
performance.
Consider the following example (playground):
extern "C" fn foo(x: __m256);
fn main() {
unsafe {
union U { v: __m256, a: [u64; 4] }
foo(U { a: [0; 4] }.v);
}
}
In this example, a 256-bit wide vector type, __m256, is passed to an extern "C" function via C FFI. Is the behavior of passing __m256 to the C function
defined?
That depends on both the platform and how the Rust program was compiled!
First, let's make the platform concrete and assume that it follows the x64 SysV ABI which states:
3.2.1 Registers and the Stack Frame
Intel AVX (Advanced Vector Extensions) provides 16 256-bit wide AVX registers (
%ymm0-%ymm15). The lower 128-bits of%ymm0-%ymm15are aliased to the respective 128b-bit SSE registers (%xmm0-%xmm15). For purposes of parameter passing and function return,%xmmNand%ymmNrefer to the same register. Only one of them can be used at the same time.3.2.3 Parameter Passing
SSE The class consists of types that fit into a vector register.
SSEUP The class consists of types that fit into a vector register and can be passed and returned in the upper bytes of it.
Second, in C, the __m256 type is only available if the current translation
unit is being compiled with AVX enabled.
Back to the example: __m256 is a 256-bit wide vector type, that is, wider than
128-bit, but it can be passed through a vector register using the lower and
upper 128-bits of a 256-bit wide register, and in C, if __m256 can be used,
these registers are always available.
That is, the C ABI requires two things:
__m256 via a 256-bit wide registerfoo has the #[target_feature(enable = "avx")] attribute !And this is where things went wrong: in Rust, __m256 is always available
independently of whether AVX is available or not1,
but we haven't specified how we are actually compiling our Rust program above:
if we compile it with AVX globally enabled, e.g., via -C target-feature=+avx, then the behavior of calling foo is defined because
__m256 will be passed to C in a single 256-bit wide register, which is what
the C ABI requires.
if we compile our program without AVX enabled, then the Rust program cannot
use 256-bit wide registers because they are not available, so independently of
how __m256 will be passed to C, it won't be passed in a 256-bit wide
register, and the behavior is undefined because of an ABI mismatch.
1: its layout is currently unspecified but that is not relevant for this issue - what matters is that 256-bit registers are not available and therefore they cannot be used.
You might be wondering: why is __m256 available even if AVX is not
available? The reason is that we want to use __m256 in some parts of
Rust's programs even if AVX is not globally enabled, and currently we don't
have great infrastructure for conditionally allowing it in some parts of the
program and not others.
Ideally, one should only be able to use __m256 and operations on it if AVX
is available, and this is exactly what this RFC proposes for using vector types
in C FFI: to always require #[target_feature(enable = X)] in C FFI functions
using SIMD types, where "unblocking" the use of each type requires some
particular feature to be enabled, e.g., avx or avx2 in the case of __m256.
That is, the compiler would reject the example above with an error:
error[E1337]: `__m256` on C FFI requires `#[target_feature(enable = "avx")]`
--> src/main.rs:7:15
|
7 | fn foo(x: __m256) -> __m256;
| ^^^^^^
And the following program would always have defined behavior (playground):
#[target_feature(enable = "avx")]
extern "C" fn foo(x: __m256) -> __m256;
fn main() {
unsafe {
#[repr(C)] union U { v: __m256, a: [u64; 4] }
if is_x86_feature_detected!("avx") {
// note: this operation is used here for readability
// but its behavior is currently unspecified (see note above).
let vec = U { a: [0; 4] }.v;
foo(vec);
}
}
}
independently of the -C target-features used globally to compile the whole
binary. Note that:
extern "C" foo is compiled with AVX enabled, so foo takes an __m256
like the C ABI expectsfoo is guarded with an is_x86_feature_detected, that is, foo
will only be called if AVX is available at run-timeextern function, Rust has to adapt these. Architecture-specific vector types require #[target_feature]s to be FFI safe.
That is, they are only safely usable as part of the signature of extern
functions if the function has certain #[target_feature]s enabled.
Which #[target_feature]s must be enabled depends on the vector types being
used.
For the stable architecture-specific vector types the following target features must be enabled:
x86/x86_64:
__m128, __m128i, __m128d: "sse"__m256, __m256i, __m256d: "avx"Future stabilizations of architecture-specific vector types must specify the
target features required to use them in extern functions.
None.
This is an adhoc solution to the problem, but sufficient for FFI purposes.
In the future, we might want to stabilize some of the following vector types. This section explores which target features would they require:
x86/x86_64:
__m64: mmx__m512, __m512i, __m512f: "avx512f"arm: neonaarch64: neonppc64: altivec / vsxwasm32: simd128Instead of using #[target_feature] we could allow vector types on C FFI only
behind #[cfg(target_feature)], e.g., via something like the portability check.
This would not allow calling C FFI functions with vector types conditionally on, e.g., run-time feature detection.
In C, the architecture specific vector types are only available if the required target features are enabled at compile-time.
__m128 on C FFI when the avx feature
is enabled? Does that change the calling convention and make doing so unsafe ?
We could extend this RFC to also require that to use certain types certain
features must be disabled.