C compatible complex types using traits

Currently we have the followring situation:

  • The standard library doesn't specifiy complex types.
  • Operator overloading makes it very easy to specify complex types in libraries
  • Different Libaries define their own Version of Complex types
  • The widely use num-complex crates definies a generic Complex
  • It is very easy to design a complex number type compatible to C in memory. I believe all of these are valid points that should be upheld.

However currently there are no C ABI compatible complex types in Rust, that can be used to call functions e.g. from cmath.h

One solution would be to introduce special traits and impl Trait parameters are translated into C-ABI compatible parameter passes.

This means:

  1. Introduce two new traits, with lang-items into the core library:
#[lang = "complex_single"]
pub trait ComplexSingle {
    fn into_array(self) -> [f32; 2];
    fn from_array([f32; 2]) -> Self;
}

#[lang = "complex_double"]
pub trait ComplexDouble {
    fn into_array(self) -> [f64; 2];
    fn from_array([f64; 2]) -> Self;
}
  1. Extend the extern "C" calling convention: Whenever an impl ComplexSingle parameter is encountered, Rust will use ComplexSingle::into_array() to convert the Complex Number into a [f32; 2] and pass it ABI compatible to the _Complex float C data type (The [0] entry specifies real, the [1] entry the imaginary part). When impl ComplexSingle is used as return value, the return value is interpreted as a [f32; 2] that is then passed to ComplexSingle::from_array() before being returned to user code. In a similar fashion impl ComplexDouble is used for parameters and return values ABI compatible to the _Complex double C data type.

A C function:

double _Complex csin(double _Complex z);

can be declared in Rust using:

use core::ffi::ComplexDouble;

extern "C" {
    csin(impl ComplexDouble) -> impl ComplexDouble;
}
1 Like

Why not just use a struct? I personally dislike the idea of Rust implicitly generating additional function calls. Explicit is better than implicit, IMO. When I call csin(complex) it should be exactly that.

Personally I don't think this is any simpler than just introducing a new ComplexF32/ComplexF64 type.

For prior discussions, there's RFC 793

Alternatively, the C standard requires complex types to have "the same representation and alignment requirements as an array type containing exactly two elements of the corresponding real type." Since arrays aren't first-class types in C, it means Rust should have some flexibility in how it interprets the ABI of [f32; 2]/[f64; 2]. Rust could just declare these two types as having the same ABI as float _Complex/double _Complex.

4 Likes

Given that you concur that this is already the case, why does the standard library need to be involved, let alone core? It sounds much more like a reason to define simple transparent wrapers for them in libc.

1 Like

Current rust code doesn't give a return value of [f64; 2] equivalent ABI to a _Complex double, but it does for (f64, f64) (at least on x86-64 ABI).

But that could be changed since [f32; 2] and [f64; 2] currently aren't FFI-safe. They could be declared FFI-safe with _Complex's ABI.

(f64, f64) isn't FFI-safe either unless you make it #[repr(C)], but then it has the ABI of a struct, which differs from the ABI of _Complex on some platforms. I suppose you could declare the #[repr(Rust)] representation FFI-safe and give it _Complex's ABI, but I personally prefer making the array type FFI-safe and _Complex-compatible.

1 Like

Pointers to them are, and may not necessarily be compatible.

My $0.02 is that rust should either define a standard library type, or a custom repr (perhaps the latter be used to (possibly unstably) implement the former). In any case, it should make room for extensions, like _Complex int, though I agree that _Complex float and _Complex double are much more useful. On a semi-related note, I do have the opinion that rust should provide some method to access the platform long double type.

Note: One benefit to an actual type, signatures accepting/returning the type are explicit.

2 Likes

How come, since [f64; 2] should quite literally have the same object representation as an array of two elements of the base type as required from the complex type? The array should be C-compatible while tuples types have no layout guarantee from Rust's point of view. What am I overlooking?

But we're not talking about pointers. Rust's [f32; 2] already has the exact same size and layout of C's float _Complex. Changing the ABI of [f32; 2] will have no impact on pointers.

Having equivalent representation and alignment does not imply they are compatible types (the converse of that is true, compatible types have equivalent representation, size, and alignment requirements). I don't believe _Complex T and T[2] are required to be compatible, nor the same of pointers thereof.

They are required to be compatible. Previously I quoted the part of the C standard that says so. Here's the paragraph:

Each complex type has the same representation and alignment requirements as an array type containing exactly two elements of the corresponding real type; the first element is equal to the real part, and the second element to the imaginary part, of the complex number.

2 Likes

C doesn't really have array arguments -- they pass only pointers to the first element. You also can't return arrays by value at all.

https://en.cppreference.com/w/cpp/language/array#Array-to-pointer_decay

When an array type is used in a function parameter list, it is transformed to the corresponding pointer type: int f(int a[2]) and int f(int* a) declare the same function.

This makes traits a poor way of representing this, because I can always do this:

struct Yolo;
impl ComplexSingle for Yolo {
    fn into_array(self) -> [f32; 2] { [0.0; 2] }
    fn from_array(_: [f32; 2]) -> Self { Self }
}
impl ComplexDouble for Yolo {
    fn into_array(self) -> [f64; 2] { [0.0; 2] }
    fn from_array(_: [f64; 2]) -> Self { Self }
}

And then what calling convention would it use?

If the two things need to be disjoint, it should be something that's already disjoint in the language -- probably a type. (Maybe could be a repr(complex), but that seems overly complicated, since it's unclear that multiple types having this ABI would be valuable.)

1 Like

The one used in the signature of the declared extern function.

Because while they are memory compatible, they are not ABI compatible.

This is true for the in memory representation, but it doesn't say anything about the ABI representation. However I agree. We could decide that [f32; 2] and [f64; 2] are passed just like _Complex float and _Complex double in all extern "C" functions. Meaning that the Rust signature of csin would be

extern "C" {
    csin(a: [f64;2]) -> [f64;2];
}

and libaries need to wrap these types (just like c string and raw pointers). Instead of [f64;2] one could also choose (f64, f64), I guess. Maybe that would be even better

One advantage of a repr would be the ability to interact with compiler-specific extensions, as I mentioned. I believe the Sys-V ABI does specify the abi of _Complex <int type>, so being able to interact with such types may be beneficial.

A big dissadvantage with a repr(complex), in my opinion is covering the followring case:

#[rep(complex)]
pub struct Complex<T> {
    re: T,
    im: T
}

How do you pass, e.g. Complex<char> to a function? Is this case undefined? Is it defined to behave like rep(C). What happens if C decides to add a _Complex int type?

Currently, the abi for char is not defined, I believe. However, if it was defined as compatible with char32_t, then it would depend. If _Complex char32_t is valid under the applicable C abi, then that type shall have the same abi as _Complex char32_t, otherwise, the abi is implementation-defined.

The way I'd specify repr(complex) is as follows:

Given a struct F, that contains a member of type [T;2] and any number of other members with size 0 and alignment 1, declared repr(complex) has the same layout as [T;2]. Given T is a type which has corresponding abi with some type defined in C or by the C standard T', if the C standard or the platform C abi defines the type _Complex T', F has corresponding abi with _Complex T'. Otherwise, the abi of F is implementation-defined.

The same specification could be used with a standard library type.

1 Like