C compatible complex types using traits

I didn't say anything about the ABI representation of arrays since array's don't have an ABI since arrays aren't first class types in C.

Anyway, yes, I agree with your suggestion here.

This is getting off topic but Sys-V only specifies floating-point _Complex types (x86_64; i386).

Arrays do have ABI in C. Not directly, but for two pointers to be compatible under C, the pointee types need to satisfy one of these conditions:

  • Both are compatible types
  • Both are structure types
  • Both are union types
  • One is void and the other is a character type.

If T[2] and _Complex T are not compatible types (which your reference does not necessarily impl, as I mentioned), then neither are T(*)[2] and _Complex T*

If this only works for impl ComplexSingle, then I don't see why it should be a trait at all, rather than just being a ComplexSingle type.

The simplest thing here might just be a lang item (or two) in core somewhere. Maybe either

  • as a "full" rust type core::num::Complex<T> which is an improper_ctype for anything except T = f32 or T = f64
  • as simple ffi-focused core::ffi::{Complex32, Complex64}

Are there any cases where it being a repr, as opposed to just a type in core, would be helpful?

Would there be, for example, any advantage to let people write this?

#[repr(complex)]
pub struct Polar<T> {
    r: T,
    theta: T
}

Though if we wanted to lean into "has the same ABI as an array type", then repr(array) might be more general. It would be useful for things like #[repr(array)] struct Color { r: u8, g: u8, b: u8 } too, where people want to safe transmute from &[Color] to &[u8].

Either or would be fine. A generic type in core with the same abi considerations I specified could be reasonable as well.

That's not the concern. In C ABI terms, the layout of struct { T real; T imag; };, _Complex T, and T[2] in memory are all effectively the same. (I'm not sure if the C specification actually gives this requirement, but I believe all of the ABIs that exist in practice adhere to it). However, these are different types, and where they are not used in relation to memory, they may be different. And the big, important difference is in function call ABIs.

void foo(_Complex T);, void foo(struct {T real; T imag; }), and void foo(T[2]) do not have the same guarantee. Some commentary on differences:

  • i386 returns _Complex float in %edx:%eax (unlike returning a struct), but doesn't for double or long double (x86_64 makes long double the odd one out, but Rust doesn't support that datatype, so it doesn't matter)
  • Some ARM ABIs use memory for struct outputs but multiple registers for _Complex
  • PowerPC apparently seems to use memory for struct inputs but multiple registers for _Complex
  • There's several ABIs where float is handled differently than double.

(I wrote a script that compares the assembly of a function taking a complex value as a struct versus _Complex variant for each of the targets that Rust supports).

4 Likes

I don't think the trait approach would provide advantages commensurate with the added complexity it would introduce.

I think it makes sense to introduce structures in core that are compatible with float and double _Complex values in C, and then make those structures FFI-safe. I do think those structures should live in core, and then the compiler and standard library can jointly ensure that they're ABI-compatible with C for the target.

If some target defines the use of _Complex for additional types beyond float and double, we can add another struct accordingly.

But I don't think we need to abstract that over multiple potential types. FFI code can convert to and from the FFI types.

6 Likes

It seems that there are multiple "solutions":

  1. My traits approach:
  • Advantage: Very flexible
  • Disadvantage: Needs new language items, unessesarily compicated, adds a lot of hidden control flow
  1. The repr(C_complex) approach. (Assuming repr(C_complex) to follow InfernoDeity's specification)
  • Advantage: Easy to create new types.
  • Disadvantage: Extern declarations must choose, which implementation of complex to support. Slighly abusive about the meaning of repr (because we influence the calling convention, not the memory representation)
  1. The [T; 2] approach.
  • Advantage: No need to introduce new constructs, just fix the behavior inside the compiler.
  • Disadvantage: Potentially confusing function signature. I don't like specifying [T;2] differently from e.g. [T;3].
  1. The (T,T) approach.
  • Advantage: No need to introduce new constructs, just fix the behavior inside the compiler.
  • Disadvantage: Potentially confusing function signature. (T,T) would only be ABI compatible with _Complex T NOT memory compatible. (Makes things potentialy difficult internally)
  1. Merge num_complex into core
  • Advantage: Straight forward API.
  • Disadvantage: We must either pull the entier num_complex crate into core (difficult in particular if one considers that even floats have a impl in std).
  1. FFI-focues type in core.
  • Advantage: Straight forward API.
  • Disadvantage: Potentially confusing. How limited should this type be? Introduces complexity: Instead of having only the num_complex::Complex<T>, we now also have core::ffi::{Complex32, Complex64}. Now we have two compeating complex types, both with their own particular advantages.

I personally think that 2. or 6. are probably the best choices. I have personally like the idea a lot, that complex numbers are defined in num_complex rather them std, and that they are generic.

I would probably go with solution 6. I would probably use the signature (for Complex<f64>):

#[rep(transparent)]
pub struct F64ComplexPair {
    pub data: [f64;2]
}

And define the type to be a "two component array of f64, that is ABI and memory compatible to the _Complex double C data type. ", rather then "Complex number". It should primarily implement a impl From<[f64;2]> for F64ComplexPair and impl From<F64ComplexPair> for [f64;2].

1 Like

FWIW, I would happily add From implementations between such FFI types and num_complex. IMO, it would not be a big deal for FFI calls to make those conversions as part of a safe wrapper function.

I'd drop the "Pair" from the name, but otherwise that looks great to me.

I'll note that any solution here involves new language guarantees, so "new language items" isn't really a meaningful downside. From an overall complexity perspective, two lang items in option 6 is arguably the easiest one -- it makes the new rule only affect those two types, it clearly can't affect any existing code, and there's no need to define all sorts of extra edge cases like "what happens if you put repr(c_complex) on struct Foo(i32, f64);?"

(I still don't think the trait version is the one we should pick, but needing new language items isn't why.)

2 Likes

#[repr(transparent)] already set that precedent.

1 Like

Yes I agree with you. Counting lang_items isn't a good qualifier. From the complexity point of view, it is probably the most complex solution, as we would introduce a rather lengthy description, on what happens if you use a impl ComplexT as a parameter and the behavior is more unique them in the other cases (adding new repr() and types with special behavior for rather niche applications feals less out of the ordinary.)

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.